Initial commit.
This commit is contained in:
69
src/playlistparsers/asxiniparser.cpp
Normal file
69
src/playlistparsers/asxiniparser.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "asxiniparser.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include <QTextStream>
|
||||
#include <QtDebug>
|
||||
|
||||
AsxIniParser::AsxIniParser(CollectionBackendInterface *collection, QObject *parent)
|
||||
: ParserBase(collection, parent) {}
|
||||
|
||||
bool AsxIniParser::TryMagic(const QByteArray &data) const {
|
||||
return data.toLower().contains("[reference]");
|
||||
}
|
||||
|
||||
SongList AsxIniParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
|
||||
|
||||
SongList ret;
|
||||
|
||||
while (!device->atEnd()) {
|
||||
QString line = QString::fromUtf8(device->readLine()).trimmed();
|
||||
int equals = line.indexOf('=');
|
||||
QString key = line.left(equals).toLower();
|
||||
QString value = line.mid(equals + 1);
|
||||
|
||||
if (key.startsWith("ref")) {
|
||||
Song song = LoadSong(value, 0, dir);
|
||||
if (song.is_valid()) {
|
||||
ret << song;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void AsxIniParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, Playlist::Path path_type) const {
|
||||
|
||||
QTextStream s(device);
|
||||
s << "[Reference]" << endl;
|
||||
|
||||
int n = 1;
|
||||
for (const Song &song : songs) {
|
||||
s << "Ref" << n << "=" << URLOrFilename(song.url(), dir, path_type) << endl;
|
||||
++n;
|
||||
}
|
||||
|
||||
}
|
||||
43
src/playlistparsers/asxiniparser.h
Normal file
43
src/playlistparsers/asxiniparser.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef ASXINIPARSER_H
|
||||
#define ASXINIPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "parserbase.h"
|
||||
|
||||
class AsxIniParser : public ParserBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AsxIniParser(CollectionBackendInterface *collection, QObject *parent = nullptr);
|
||||
|
||||
QString name() const { return "ASX/INI"; }
|
||||
QStringList file_extensions() const { return QStringList() << "asxini"; }
|
||||
|
||||
bool TryMagic(const QByteArray &data) const;
|
||||
|
||||
SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const;
|
||||
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const;
|
||||
};
|
||||
|
||||
#endif // ASXINIPARSER_H
|
||||
155
src/playlistparsers/asxparser.cpp
Normal file
155
src/playlistparsers/asxparser.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "asxparser.h"
|
||||
#include "core/utilities.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QRegExp>
|
||||
#include <QUrl>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QtDebug>
|
||||
|
||||
ASXParser::ASXParser(CollectionBackendInterface *collection, QObject *parent)
|
||||
: XMLParser(collection, parent) {}
|
||||
|
||||
SongList ASXParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
|
||||
|
||||
// We have to load everything first so we can munge the "XML".
|
||||
QByteArray data = device->readAll();
|
||||
|
||||
// (thanks Amarok...)
|
||||
// ASX looks a lot like xml, but doesn't require tags to be case sensitive,
|
||||
// meaning we have to accept things like: <Abstract>...</abstract>
|
||||
// We use a dirty way to achieve this: we make all tags lower case
|
||||
QRegExp ex("(<[/]?[^>]*[A-Z]+[^>]*>)");
|
||||
ex.setCaseSensitivity(Qt::CaseInsensitive);
|
||||
int index = 0;
|
||||
while ((index = ex.indexIn(data, index)) != -1) {
|
||||
data.replace(ex.cap(1).toLocal8Bit(), ex.cap(1).toLower().toLocal8Bit());
|
||||
index += ex.matchedLength();
|
||||
}
|
||||
|
||||
// Some playlists have unescaped & characters in URLs :(
|
||||
ex.setPattern("(href\\s*=\\s*\")([^\"]+)\"");
|
||||
index = 0;
|
||||
while ((index = ex.indexIn(data, index)) != -1) {
|
||||
QString url = ex.cap(2);
|
||||
url.replace(QRegExp("&(?!amp;|quot;|apos;|lt;|gt;)"), "&");
|
||||
|
||||
QByteArray replacement = QString(ex.cap(1) + url + "\"").toLocal8Bit();
|
||||
data.replace(ex.cap(0).toLocal8Bit(), replacement);
|
||||
index += replacement.length();
|
||||
}
|
||||
|
||||
QBuffer buffer(&data);
|
||||
buffer.open(QIODevice::ReadOnly);
|
||||
|
||||
SongList ret;
|
||||
|
||||
QXmlStreamReader reader(&buffer);
|
||||
if (!Utilities::ParseUntilElement(&reader, "asx")) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
while (!reader.atEnd() && Utilities::ParseUntilElement(&reader, "entry")) {
|
||||
Song song = ParseTrack(&reader, dir);
|
||||
if (song.is_valid()) {
|
||||
ret << song;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
Song ASXParser::ParseTrack(QXmlStreamReader *reader, const QDir &dir) const {
|
||||
|
||||
QString title, artist, album, ref;
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
|
||||
switch (type) {
|
||||
case QXmlStreamReader::StartElement: {
|
||||
QStringRef name = reader->name();
|
||||
if (name == "ref") {
|
||||
ref = reader->attributes().value("href").toString();
|
||||
} else if (name == "title") {
|
||||
title = reader->readElementText();
|
||||
} else if (name == "author") {
|
||||
artist = reader->readElementText();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QXmlStreamReader::EndElement: {
|
||||
if (reader->name() == "entry") {
|
||||
goto return_song;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return_song:
|
||||
Song song = LoadSong(ref, 0, dir);
|
||||
|
||||
// Override metadata with what was in the playlist
|
||||
song.set_title(title);
|
||||
song.set_artist(artist);
|
||||
song.set_album(album);
|
||||
return song;
|
||||
|
||||
}
|
||||
|
||||
void ASXParser::Save(const SongList &songs, QIODevice *device, const QDir&, Playlist::Path path_type) const {
|
||||
|
||||
QXmlStreamWriter writer(device);
|
||||
writer.setAutoFormatting(true);
|
||||
writer.setAutoFormattingIndent(2);
|
||||
writer.writeStartDocument();
|
||||
{
|
||||
StreamElement asx("asx", &writer);
|
||||
writer.writeAttribute("version", "3.0");
|
||||
for (const Song &song : songs) {
|
||||
StreamElement entry("entry", &writer);
|
||||
writer.writeTextElement("title", song.title());
|
||||
{
|
||||
StreamElement ref("ref", &writer);
|
||||
writer.writeAttribute("href", song.url().toString());
|
||||
}
|
||||
if (!song.artist().isEmpty()) {
|
||||
writer.writeTextElement("author", song.artist());
|
||||
}
|
||||
}
|
||||
}
|
||||
writer.writeEndDocument();
|
||||
|
||||
}
|
||||
|
||||
bool ASXParser::TryMagic(const QByteArray &data) const {
|
||||
return data.toLower().contains("<asx");
|
||||
}
|
||||
46
src/playlistparsers/asxparser.h
Normal file
46
src/playlistparsers/asxparser.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef ASXPARSER_H
|
||||
#define ASXPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "xmlparser.h"
|
||||
|
||||
class ASXParser : public XMLParser {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ASXParser(CollectionBackendInterface *collection, QObject *parent = nullptr);
|
||||
|
||||
QString name() const { return "ASX"; }
|
||||
QStringList file_extensions() const { return QStringList() << "asx"; }
|
||||
|
||||
bool TryMagic(const QByteArray &data) const;
|
||||
|
||||
SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const;
|
||||
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const;
|
||||
|
||||
private:
|
||||
Song ParseTrack(QXmlStreamReader *reader, const QDir &dir) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
374
src/playlistparsers/cueparser.cpp
Normal file
374
src/playlistparsers/cueparser.cpp
Normal file
@@ -0,0 +1,374 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "cueparser.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/timeconstants.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDateTime>
|
||||
#include <QFileInfo>
|
||||
#include <QStringBuilder>
|
||||
#include <QRegExp>
|
||||
#include <QTextCodec>
|
||||
#include <QTextStream>
|
||||
#include <QtDebug>
|
||||
|
||||
const char *CueParser::kFileLineRegExp = "(\\S+)\\s+(?:\"([^\"]+)\"|(\\S+))\\s*(?:\"([^\"]+)\"|(\\S+))?";
|
||||
const char *CueParser::kIndexRegExp = "(\\d{2,3}):(\\d{2}):(\\d{2})";
|
||||
|
||||
const char *CueParser::kPerformer = "performer";
|
||||
const char *CueParser::kTitle = "title";
|
||||
const char *CueParser::kSongWriter = "songwriter";
|
||||
const char *CueParser::kFile = "file";
|
||||
const char *CueParser::kTrack = "track";
|
||||
const char *CueParser::kIndex = "index";
|
||||
const char *CueParser::kAudioTrackType = "audio";
|
||||
const char *CueParser::kRem = "rem";
|
||||
const char *CueParser::kGenre = "genre";
|
||||
const char *CueParser::kDate = "date";
|
||||
const char *CueParser::kDisc = "discnumber";
|
||||
|
||||
CueParser::CueParser(CollectionBackendInterface *collection, QObject *parent)
|
||||
: ParserBase(collection, parent) {}
|
||||
|
||||
SongList CueParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
|
||||
SongList ret;
|
||||
|
||||
QTextStream text_stream(device);
|
||||
text_stream.setCodec(QTextCodec::codecForUtfText(device->peek(1024), QTextCodec::codecForName("UTF-8")));
|
||||
|
||||
QString dir_path = dir.absolutePath();
|
||||
// read the first line already
|
||||
QString line = text_stream.readLine();
|
||||
|
||||
QList<CueEntry> entries;
|
||||
int files = 0;
|
||||
|
||||
// -- whole file
|
||||
while (!text_stream.atEnd()) {
|
||||
|
||||
QString album_artist;
|
||||
QString album;
|
||||
QString album_composer;
|
||||
QString file;
|
||||
QString file_type;
|
||||
QString genre;
|
||||
QString date;
|
||||
QString disc;
|
||||
|
||||
// -- FILE section
|
||||
do {
|
||||
QStringList splitted = SplitCueLine(line);
|
||||
|
||||
// uninteresting or incorrect line
|
||||
if (splitted.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString line_name = splitted[0].toLower();
|
||||
QString line_value = splitted[1];
|
||||
|
||||
// PERFORMER
|
||||
if (line_name == kPerformer) {
|
||||
|
||||
album_artist = line_value;
|
||||
|
||||
// TITLE
|
||||
}
|
||||
else if (line_name == kTitle) {
|
||||
|
||||
album = line_value;
|
||||
|
||||
// SONGWRITER
|
||||
}
|
||||
else if (line_name == kSongWriter) {
|
||||
|
||||
album_composer = line_value;
|
||||
|
||||
// FILE
|
||||
}
|
||||
else if (line_name == kFile) {
|
||||
|
||||
file = QDir::isAbsolutePath(line_value) ? line_value : dir.absoluteFilePath(line_value);
|
||||
|
||||
if (splitted.size() > 2) {
|
||||
file_type = splitted[2];
|
||||
}
|
||||
|
||||
// REM
|
||||
}
|
||||
else if (line_name == kRem) {
|
||||
if (splitted.size() < 3) {
|
||||
break;
|
||||
}
|
||||
|
||||
// REM GENRE
|
||||
if (line_value.toLower() == kGenre) {
|
||||
genre = splitted[2];
|
||||
|
||||
// REM DATE
|
||||
} else if (line_value.toLower() == kDate) {
|
||||
date = splitted[2];
|
||||
|
||||
// REM DISC
|
||||
} else if (line_value.toLower() == kDisc) {
|
||||
disc = splitted[2];
|
||||
}
|
||||
|
||||
// end of the header -> go into the track mode
|
||||
} else if (line_name == kTrack) {
|
||||
|
||||
files++;
|
||||
break;
|
||||
}
|
||||
|
||||
// just ignore the rest of possible field types for now...
|
||||
} while (!(line = text_stream.readLine()).isNull());
|
||||
|
||||
if(line.isNull()) {
|
||||
qLog(Warning) << "the .cue file from " << dir_path << " defines no tracks!";
|
||||
return ret;
|
||||
}
|
||||
|
||||
// if this is a data file, all of it's tracks will be ignored
|
||||
bool valid_file = file_type.compare("BINARY", Qt::CaseInsensitive) && file_type.compare("MOTOROLA", Qt::CaseInsensitive);
|
||||
|
||||
QString track_type;
|
||||
QString index;
|
||||
QString artist;
|
||||
QString composer;
|
||||
QString title;
|
||||
|
||||
// TRACK section
|
||||
do {
|
||||
QStringList splitted = SplitCueLine(line);
|
||||
|
||||
// uninteresting or incorrect line
|
||||
if (splitted.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString line_name = splitted[0].toLower();
|
||||
QString line_value = splitted[1];
|
||||
QString line_additional = splitted.size() > 2 ? splitted[2].toLower() : "";
|
||||
|
||||
if (line_name == kTrack) {
|
||||
|
||||
// the beginning of another track's definition - we're saving the current one
|
||||
// for later (if it's valid of course)
|
||||
// please note that the same code is repeated just after this 'do-while' loop
|
||||
if(valid_file && !index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) {
|
||||
entries.append(CueEntry(file, index, title, artist, album_artist, album, composer, album_composer, genre, date, disc));
|
||||
}
|
||||
|
||||
// clear the state
|
||||
track_type = index = artist = title = "";
|
||||
|
||||
if (!line_additional.isEmpty()) {
|
||||
track_type = line_additional;
|
||||
}
|
||||
|
||||
}
|
||||
else if (line_name == kIndex) {
|
||||
|
||||
// we need the index's position field
|
||||
if (!line_additional.isEmpty()) {
|
||||
|
||||
// if there's none "01" index, we'll just take the first one
|
||||
// also, we'll take the "01" index even if it's the last one
|
||||
if (line_value == "01" || index.isEmpty()) {
|
||||
|
||||
index = line_additional;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (line_name == kPerformer) {
|
||||
artist = line_value;
|
||||
}
|
||||
else if (line_name == kTitle) {
|
||||
title = line_value;
|
||||
}
|
||||
else if (line_name == kSongWriter) {
|
||||
composer = line_value;
|
||||
// end of track's for the current file -> parse next one
|
||||
}
|
||||
else if (line_name == kFile) {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// just ignore the rest of possible field types for now...
|
||||
} while (!(line = text_stream.readLine()).isNull());
|
||||
|
||||
// we didn't add the last song yet...
|
||||
if(valid_file && !index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) {
|
||||
entries.append(CueEntry(file, index, title, artist, album_artist, album, composer, album_composer, genre, date, disc));
|
||||
}
|
||||
}
|
||||
|
||||
QDateTime cue_mtime = QFileInfo(playlist_path).lastModified();
|
||||
|
||||
// finalize parsing songs
|
||||
for (int i = 0; i < entries.length(); i++) {
|
||||
CueEntry entry = entries.at(i);
|
||||
|
||||
Song song = LoadSong(entry.file, IndexToMarker(entry.index), dir);
|
||||
|
||||
// cue song has mtime equal to qMax(media_file_mtime, cue_sheet_mtime)
|
||||
if (cue_mtime.isValid()) {
|
||||
song.set_mtime(qMax(cue_mtime.toTime_t(), song.mtime()));
|
||||
}
|
||||
song.set_cue_path(playlist_path);
|
||||
|
||||
// overwrite the stuff, we may have read from the file or collection, using
|
||||
// the current .cue metadata
|
||||
|
||||
// set track number only in single-file mode
|
||||
if (files == 1) {
|
||||
song.set_track(i + 1);
|
||||
}
|
||||
|
||||
// the last TRACK for every FILE gets it's 'end' marker from the media file's
|
||||
// length
|
||||
if(i + 1 < entries.size() && entries.at(i).file == entries.at(i + 1).file) {
|
||||
// incorrect indices?
|
||||
if (!UpdateSong(entry, entries.at(i + 1).index, &song)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// incorrect index?
|
||||
if (!UpdateLastSong(entry, &song)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ret << song;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// This and the kFileLineRegExp do most of the "dirty" work, namely: splitting the raw .cue
|
||||
// line into logical parts and getting rid of all the unnecessary whitespaces and quoting.
|
||||
QStringList CueParser::SplitCueLine(const QString &line) const {
|
||||
|
||||
QRegExp line_regexp(kFileLineRegExp);
|
||||
if (!line_regexp.exactMatch(line.trimmed())) {
|
||||
return QStringList();
|
||||
}
|
||||
|
||||
// let's remove the empty entries while we're at it
|
||||
return line_regexp.capturedTexts().filter(QRegExp(".+")).mid(1, -1);
|
||||
|
||||
}
|
||||
|
||||
// Updates the song with data from the .cue entry. This one mustn't be used for the
|
||||
// last song in the .cue file.
|
||||
bool CueParser::UpdateSong(const CueEntry &entry, const QString &next_index, Song *song) const {
|
||||
|
||||
qint64 beginning = IndexToMarker(entry.index);
|
||||
qint64 end = IndexToMarker(next_index);
|
||||
|
||||
// incorrect indices (we won't be able to calculate beginning or end)
|
||||
if (beginning == -1 || end == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// believe the CUE: Init() forces validity
|
||||
song->Init(entry.title, entry.PrettyArtist(), entry.album, beginning, end);
|
||||
song->set_albumartist(entry.album_artist);
|
||||
song->set_composer(entry.PrettyComposer());
|
||||
song->set_genre(entry.genre);
|
||||
song->set_year(entry.date.toInt());
|
||||
song->set_disc(entry.disc.toInt());
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
// Updates the song with data from the .cue entry. This one must be used only for the
|
||||
// last song in the .cue file.
|
||||
bool CueParser::UpdateLastSong(const CueEntry &entry, Song *song) const {
|
||||
|
||||
qint64 beginning = IndexToMarker(entry.index);
|
||||
|
||||
// incorrect index (we won't be able to calculate beginning)
|
||||
if (beginning == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// believe the CUE and force validity (like UpdateSong() does)
|
||||
song->set_valid(true);
|
||||
|
||||
song->set_title(entry.title);
|
||||
song->set_artist(entry.PrettyArtist());
|
||||
song->set_album(entry.album);
|
||||
song->set_albumartist(entry.album_artist);
|
||||
song->set_genre(entry.genre);
|
||||
song->set_year(entry.date.toInt());
|
||||
song->set_composer(entry.PrettyComposer());
|
||||
song->set_disc(entry.disc.toInt());
|
||||
|
||||
// we don't do anything with the end here because it's already set to
|
||||
// the end of the media file (if it exists)
|
||||
song->set_beginning_nanosec(beginning);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
qint64 CueParser::IndexToMarker(const QString &index) const {
|
||||
|
||||
QRegExp index_regexp(kIndexRegExp);
|
||||
if (!index_regexp.exactMatch(index)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
QStringList splitted = index_regexp.capturedTexts().mid(1, -1);
|
||||
qlonglong frames = splitted.at(0).toLongLong() * 60 * 75 + splitted.at(1).toLongLong() * 75 + splitted.at(2).toLongLong();
|
||||
return (frames * kNsecPerSec) / 75;
|
||||
|
||||
}
|
||||
|
||||
void CueParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, Playlist::Path path_type) const {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Looks for a track starting with one of the .cue's keywords.
|
||||
bool CueParser::TryMagic(const QByteArray &data) const {
|
||||
|
||||
QStringList splitted = QString::fromUtf8(data.constData()).split('\n');
|
||||
|
||||
for (int i = 0; i < splitted.length(); i++) {
|
||||
QString line = splitted.at(i).trimmed();
|
||||
if (line.startsWith(kPerformer, Qt::CaseInsensitive) ||
|
||||
line.startsWith(kTitle, Qt::CaseInsensitive) ||
|
||||
line.startsWith(kFile, Qt::CaseInsensitive) ||
|
||||
line.startsWith(kTrack, Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
107
src/playlistparsers/cueparser.h
Normal file
107
src/playlistparsers/cueparser.h
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef CUEPARSER_H
|
||||
#define CUEPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "parserbase.h"
|
||||
|
||||
#include <QRegExp>
|
||||
|
||||
// This parser will try to detect the real encoding of a .cue file but there's
|
||||
// a great chance it will fail so it's probably best to assume that the parser
|
||||
// is UTF compatible only.
|
||||
class CueParser : public ParserBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static const char *kFileLineRegExp;
|
||||
static const char *kIndexRegExp;
|
||||
|
||||
static const char *kPerformer;
|
||||
static const char *kTitle;
|
||||
static const char *kSongWriter;
|
||||
static const char *kFile;
|
||||
static const char *kTrack;
|
||||
static const char *kIndex;
|
||||
static const char *kAudioTrackType;
|
||||
static const char *kRem;
|
||||
static const char *kGenre;
|
||||
static const char *kDate;
|
||||
static const char *kDisc;
|
||||
|
||||
CueParser(CollectionBackendInterface *library, QObject *parent = nullptr);
|
||||
|
||||
QString name() const { return "CUE"; }
|
||||
QStringList file_extensions() const { return QStringList() << "cue"; }
|
||||
QString mime_type() const { return "application/x-cue"; }
|
||||
|
||||
bool TryMagic(const QByteArray &data) const;
|
||||
|
||||
SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const;
|
||||
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const;
|
||||
|
||||
private:
|
||||
// A single TRACK entry in .cue file.
|
||||
struct CueEntry {
|
||||
QString file;
|
||||
|
||||
QString index;
|
||||
|
||||
QString title;
|
||||
QString artist;
|
||||
QString album_artist;
|
||||
QString album;
|
||||
|
||||
QString composer;
|
||||
QString album_composer;
|
||||
|
||||
QString genre;
|
||||
QString date;
|
||||
QString disc;
|
||||
|
||||
QString PrettyArtist() const { return artist.isEmpty() ? album_artist : artist; }
|
||||
QString PrettyComposer() const { return composer.isEmpty() ? album_composer : composer; }
|
||||
|
||||
CueEntry(QString &file, QString &index, QString &title, QString &artist, QString &album_artist, QString &album, QString &composer, QString &album_composer, QString &genre, QString &date, QString &disc) {
|
||||
this->file = file;
|
||||
this->index = index;
|
||||
this->title = title;
|
||||
this->artist = artist;
|
||||
this->album_artist = album_artist;
|
||||
this->album = album;
|
||||
this->composer = composer;
|
||||
this->album_composer = album_composer;
|
||||
this->genre = genre;
|
||||
this->date = date;
|
||||
this->disc = disc;
|
||||
}
|
||||
};
|
||||
|
||||
bool UpdateSong(const CueEntry &entry, const QString &next_index, Song *song) const;
|
||||
bool UpdateLastSong(const CueEntry &entry, Song *song) const;
|
||||
|
||||
QStringList SplitCueLine(const QString &line) const;
|
||||
qint64 IndexToMarker(const QString &index) const;
|
||||
};
|
||||
|
||||
#endif // CUEPARSER_H
|
||||
141
src/playlistparsers/m3uparser.cpp
Normal file
141
src/playlistparsers/m3uparser.cpp
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "m3uparser.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/timeconstants.h"
|
||||
|
||||
#include "playlist/playlist.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QtDebug>
|
||||
|
||||
M3UParser::M3UParser(CollectionBackendInterface *collection, QObject *parent)
|
||||
: ParserBase(collection, parent) {}
|
||||
|
||||
SongList M3UParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
|
||||
|
||||
SongList ret;
|
||||
|
||||
M3UType type = STANDARD;
|
||||
Metadata current_metadata;
|
||||
|
||||
QString data = QString::fromUtf8(device->readAll());
|
||||
data.replace('\r', '\n');
|
||||
data.replace("\n\n", "\n");
|
||||
QByteArray bytes = data.toUtf8();
|
||||
QBuffer buffer(&bytes);
|
||||
buffer.open(QIODevice::ReadOnly);
|
||||
|
||||
QString line = QString::fromUtf8(buffer.readLine()).trimmed();
|
||||
if (line.startsWith("#EXTM3U")) {
|
||||
// This is in extended M3U format.
|
||||
type = EXTENDED;
|
||||
line = QString::fromUtf8(buffer.readLine()).trimmed();
|
||||
}
|
||||
|
||||
forever {
|
||||
if (line.startsWith('#')) {
|
||||
// Extended info or comment.
|
||||
if (type == EXTENDED && line.startsWith("#EXT")) {
|
||||
if (!ParseMetadata(line, ¤t_metadata)) {
|
||||
qLog(Warning) << "Failed to parse metadata: " << line;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!line.isEmpty()) {
|
||||
Song song = LoadSong(line, 0, dir);
|
||||
if (!current_metadata.title.isEmpty()) {
|
||||
song.set_title(current_metadata.title);
|
||||
}
|
||||
if (!current_metadata.artist.isEmpty()) {
|
||||
song.set_artist(current_metadata.artist);
|
||||
}
|
||||
if (current_metadata.length > 0) {
|
||||
song.set_length_nanosec(current_metadata.length);
|
||||
}
|
||||
ret << song;
|
||||
|
||||
current_metadata = Metadata();
|
||||
}
|
||||
if (buffer.atEnd()) {
|
||||
break;
|
||||
}
|
||||
line = QString::fromUtf8(buffer.readLine()).trimmed();
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
bool M3UParser::ParseMetadata(const QString &line, M3UParser::Metadata *metadata) const {
|
||||
|
||||
// Extended info, eg.
|
||||
// #EXTINF:123,Sample Artist - Sample title
|
||||
QString info = line.section(':', 1);
|
||||
QString l = info.section(',', 0, 0);
|
||||
bool ok = false;
|
||||
int length = l.toInt(&ok);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
metadata->length = length * kNsecPerSec;
|
||||
|
||||
QString track_info = info.section(',', 1);
|
||||
QStringList list = track_info.split(" - ");
|
||||
if (list.size() <= 1) {
|
||||
metadata->title = track_info;
|
||||
return true;
|
||||
}
|
||||
metadata->artist = list[0].trimmed();
|
||||
metadata->title = list[1].trimmed();
|
||||
return true;
|
||||
}
|
||||
|
||||
void M3UParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, Playlist::Path path_type) const {
|
||||
|
||||
device->write("#EXTM3U\n");
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(Playlist::kSettingsGroup);
|
||||
bool writeMetadata = s.value(Playlist::kWriteMetadata, true).toBool();
|
||||
s.endGroup();
|
||||
|
||||
for (const Song &song : songs) {
|
||||
if (song.url().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (writeMetadata) {
|
||||
QString meta = QString("#EXTINF:%1,%2 - %3\n")
|
||||
.arg(song.length_nanosec() / kNsecPerSec)
|
||||
.arg(song.artist())
|
||||
.arg(song.title());
|
||||
device->write(meta.toUtf8());
|
||||
}
|
||||
device->write(URLOrFilename(song.url(), dir, path_type).toUtf8());
|
||||
device->write("\n");
|
||||
}
|
||||
}
|
||||
|
||||
bool M3UParser::TryMagic(const QByteArray &data) const {
|
||||
return data.contains("#EXTM3U") || data.contains("#EXTINF");
|
||||
}
|
||||
72
src/playlistparsers/m3uparser.h
Normal file
72
src/playlistparsers/m3uparser.h
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef M3UPARSER_H
|
||||
#define M3UPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
#include "gtest/gtest_prod.h"
|
||||
|
||||
#include "parserbase.h"
|
||||
|
||||
class M3UParser : public ParserBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
M3UParser(CollectionBackendInterface *collection, QObject *parent = nullptr);
|
||||
|
||||
QString name() const { return "M3U"; }
|
||||
QStringList file_extensions() const { return QStringList() << "m3u" << "m3u8"; }
|
||||
QString mime_type() const { return "text/uri-list"; }
|
||||
|
||||
bool TryMagic(const QByteArray &data) const;
|
||||
|
||||
SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const;
|
||||
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const;
|
||||
|
||||
private:
|
||||
enum M3UType {
|
||||
STANDARD = 0,
|
||||
EXTENDED, // Includes extended info (track, artist, etc.)
|
||||
LINK, // Points to a directory.
|
||||
};
|
||||
|
||||
struct Metadata {
|
||||
Metadata() : length(-1) {}
|
||||
QString artist;
|
||||
QString title;
|
||||
qint64 length;
|
||||
};
|
||||
|
||||
bool ParseMetadata(const QString &line, Metadata *metadata) const;
|
||||
|
||||
FRIEND_TEST(M3UParserTest, ParsesMetadata);
|
||||
FRIEND_TEST(M3UParserTest, ParsesTrackLocation);
|
||||
FRIEND_TEST(M3UParserTest, ParsesTrackLocationRelative);
|
||||
FRIEND_TEST(M3UParserTest, ParsesTrackLocationHttp);
|
||||
#ifdef Q_OS_WIN32
|
||||
FRIEND_TEST(M3UParserTest, ParsesTrackLocationAbsoluteWindows);
|
||||
#endif // Q_OS_WIN32
|
||||
};
|
||||
|
||||
#endif // M3UPARSER_H
|
||||
112
src/playlistparsers/parserbase.cpp
Normal file
112
src/playlistparsers/parserbase.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "parserbase.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionquery.h"
|
||||
#include "collection/sqlrow.h"
|
||||
#include "playlist/playlist.h"
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
ParserBase::ParserBase(CollectionBackendInterface *collection, QObject *parent)
|
||||
: QObject(parent), collection_(collection) {}
|
||||
|
||||
void ParserBase::LoadSong(const QString &filename_or_url, qint64 beginning, const QDir &dir, Song *song) const {
|
||||
|
||||
if (filename_or_url.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString filename = filename_or_url;
|
||||
|
||||
if (filename_or_url.contains(QRegExp("^[a-z]{2,}:"))) {
|
||||
QUrl url(filename_or_url);
|
||||
if (url.scheme() == "file") {
|
||||
filename = url.toLocalFile();
|
||||
}
|
||||
//else {
|
||||
// song->set_url(QUrl::fromUserInput(filename_or_url));
|
||||
// song->set_filetype(Song::Type_Stream);
|
||||
// song->set_valid(true);
|
||||
// return;
|
||||
// }
|
||||
}
|
||||
|
||||
// Strawberry always wants / separators internally. Using
|
||||
// QDir::fromNativeSeparators() only works on the same platform the playlist
|
||||
// was created on/for, using replace() lets playlists work on any platform.
|
||||
filename = filename.replace('\\', '/');
|
||||
|
||||
// Make the path absolute
|
||||
if (!QDir::isAbsolutePath(filename)) {
|
||||
filename = dir.absoluteFilePath(filename);
|
||||
}
|
||||
|
||||
// Use the canonical path
|
||||
if (QFile::exists(filename)) {
|
||||
filename = QFileInfo(filename).canonicalFilePath();
|
||||
}
|
||||
|
||||
const QUrl url = QUrl::fromLocalFile(filename);
|
||||
|
||||
// Search in the collection
|
||||
Song collection_song;
|
||||
if (collection_) {
|
||||
collection_song = collection_->GetSongByUrl(url, beginning);
|
||||
}
|
||||
|
||||
// If it was found in the collection then use it, otherwise load metadata from
|
||||
// disk.
|
||||
if (collection_song.is_valid()) {
|
||||
*song = collection_song;
|
||||
}
|
||||
else {
|
||||
TagReaderClient::Instance()->ReadFileBlocking(filename, song);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Song ParserBase::LoadSong(const QString &filename_or_url, qint64 beginning, const QDir &dir) const {
|
||||
|
||||
Song song;
|
||||
LoadSong(filename_or_url, beginning, dir, &song);
|
||||
return song;
|
||||
|
||||
}
|
||||
|
||||
QString ParserBase::URLOrFilename(const QUrl &url, const QDir &dir, Playlist::Path path_type) const {
|
||||
|
||||
if (url.scheme() != "file") return url.toString();
|
||||
|
||||
const QString filename = url.toLocalFile();
|
||||
|
||||
if (path_type != Playlist::Path_Absolute && QDir::isAbsolutePath(filename)) {
|
||||
const QString relative = dir.relativeFilePath(filename);
|
||||
|
||||
if (!relative.startsWith("../") || path_type == Playlist::Path_Relative)
|
||||
return relative;
|
||||
}
|
||||
return filename;
|
||||
|
||||
}
|
||||
76
src/playlistparsers/parserbase.h
Normal file
76
src/playlistparsers/parserbase.h
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PARSERBASE_H
|
||||
#define PARSERBASE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QDir>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "playlist/playlist.h"
|
||||
|
||||
class CollectionBackendInterface;
|
||||
|
||||
class ParserBase : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ParserBase(CollectionBackendInterface *collection, QObject *parent = nullptr);
|
||||
|
||||
virtual QString name() const = 0;
|
||||
virtual QStringList file_extensions() const = 0;
|
||||
virtual QString mime_type() const { return QString(); }
|
||||
|
||||
virtual bool TryMagic(const QByteArray &data) const = 0;
|
||||
|
||||
// Loads all songs from playlist found at path 'playlist_path' in directory 'dir'.
|
||||
// The 'device' argument is an opened and ready to read from represantation of
|
||||
// this playlist.
|
||||
// This method might not return all of the songs found in the playlist. Any playlist
|
||||
// parser may decide to leave out some entries if it finds them incomplete or invalid.
|
||||
// This means that the final resulting SongList should be considered valid (at least
|
||||
// from the parser's point of view).
|
||||
virtual SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const = 0;
|
||||
virtual void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const = 0;
|
||||
|
||||
protected:
|
||||
// Loads a song. If filename_or_url is a URL (with a scheme other than
|
||||
// "file") then it is set on the song and the song marked as a stream.
|
||||
// If it is a filename or a file:// URL then it is made absolute and canonical
|
||||
// and set as a file:// url on the song. Also sets the song's metadata by
|
||||
// searching in the Collection, or loading from the file as a fallback.
|
||||
// This function should always be used when loading a playlist.
|
||||
Song LoadSong(const QString &filename_or_url, qint64 beginning, const QDir &dir) const;
|
||||
void LoadSong(const QString &filename_or_url, qint64 beginning, const QDir &dir, Song *song) const;
|
||||
|
||||
// If the URL is a file:// URL then returns its path, absolute or relative to
|
||||
// the directory depending on the path_type option.
|
||||
// Otherwise returns the URL as is.
|
||||
// This function should always be used when saving a playlist.
|
||||
QString URLOrFilename(const QUrl &url, const QDir &dir, Playlist::Path path_type) const;
|
||||
|
||||
private:
|
||||
CollectionBackendInterface *collection_;
|
||||
};
|
||||
|
||||
#endif // PARSERBASE_H
|
||||
190
src/playlistparsers/playlistparser.cpp
Normal file
190
src/playlistparsers/playlistparser.cpp
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "asxparser.h"
|
||||
#include "asxiniparser.h"
|
||||
#include "cueparser.h"
|
||||
#include "m3uparser.h"
|
||||
#include "playlistparser.h"
|
||||
#include "plsparser.h"
|
||||
#include "wplparser.h"
|
||||
#include "xspfparser.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
const int PlaylistParser::kMagicSize = 512;
|
||||
|
||||
PlaylistParser::PlaylistParser(CollectionBackendInterface *collection, QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
|
||||
default_parser_ = new XSPFParser(collection, this);
|
||||
parsers_ << new M3UParser(collection, this);
|
||||
parsers_ << default_parser_;
|
||||
parsers_ << new PLSParser(collection, this);
|
||||
parsers_ << new ASXParser(collection, this);
|
||||
parsers_ << new AsxIniParser(collection, this);
|
||||
parsers_ << new CueParser(collection, this);
|
||||
parsers_ << new WplParser(collection, this);
|
||||
|
||||
}
|
||||
|
||||
QStringList PlaylistParser::file_extensions() const {
|
||||
|
||||
QStringList ret;
|
||||
|
||||
for (ParserBase *parser : parsers_) {
|
||||
ret << parser->file_extensions();
|
||||
}
|
||||
|
||||
qStableSort(ret);
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QStringList PlaylistParser::mime_types() const {
|
||||
|
||||
QStringList ret;
|
||||
|
||||
for (ParserBase *parser : parsers_) {
|
||||
if (!parser->mime_type().isEmpty()) ret << parser->mime_type();
|
||||
}
|
||||
|
||||
qStableSort(ret);
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QString PlaylistParser::filters() const {
|
||||
|
||||
QStringList filters;
|
||||
QStringList all_extensions;
|
||||
for (ParserBase *parser : parsers_) {
|
||||
filters << FilterForParser(parser, &all_extensions);
|
||||
}
|
||||
|
||||
filters.prepend(tr("All playlists (%1)").arg(all_extensions.join(" ")));
|
||||
|
||||
return filters.join(";;");
|
||||
|
||||
}
|
||||
|
||||
QString PlaylistParser::FilterForParser(const ParserBase *parser, QStringList *all_extensions) const {
|
||||
|
||||
QStringList extensions;
|
||||
for (const QString &extension : parser->file_extensions())
|
||||
extensions << "*." + extension;
|
||||
|
||||
if (all_extensions) *all_extensions << extensions;
|
||||
|
||||
return tr("%1 playlists (%2)").arg(parser->name(), extensions.join(" "));
|
||||
|
||||
}
|
||||
|
||||
QString PlaylistParser::default_extension() const {
|
||||
return default_parser_->file_extensions()[0];
|
||||
}
|
||||
|
||||
QString PlaylistParser::default_filter() const {
|
||||
return FilterForParser(default_parser_);
|
||||
}
|
||||
|
||||
ParserBase *PlaylistParser::ParserForExtension(const QString &suffix) const {
|
||||
|
||||
for (ParserBase *p : parsers_) {
|
||||
if (p->file_extensions().contains(suffix)) return p;
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
ParserBase *PlaylistParser::ParserForMimeType(const QString &mime_type) const {
|
||||
|
||||
for (ParserBase *p : parsers_) {
|
||||
if (!p->mime_type().isEmpty() &&
|
||||
(QString::compare(p->mime_type(), mime_type, Qt::CaseInsensitive) == 0))
|
||||
return p;
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
ParserBase *PlaylistParser::ParserForMagic(const QByteArray &data, const QString &mime_type) const {
|
||||
|
||||
for (ParserBase *p : parsers_) {
|
||||
if ((!mime_type.isEmpty() && mime_type == p->mime_type()) || p->TryMagic(data))
|
||||
return p;
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
SongList PlaylistParser::LoadFromFile(const QString &filename) const {
|
||||
|
||||
QFileInfo info(filename);
|
||||
|
||||
// Find a parser that supports this file extension
|
||||
ParserBase *parser = ParserForExtension(info.suffix());
|
||||
if (!parser) {
|
||||
qLog(Warning) << "Unknown filetype:" << filename;
|
||||
return SongList();
|
||||
}
|
||||
|
||||
// Open the file
|
||||
QFile file(filename);
|
||||
file.open(QIODevice::ReadOnly);
|
||||
|
||||
return parser->Load(&file, filename, info.absolutePath());
|
||||
|
||||
}
|
||||
|
||||
SongList PlaylistParser::LoadFromDevice(QIODevice *device, const QString &path_hint, const QDir &dir_hint) const {
|
||||
|
||||
// Find a parser that supports this data
|
||||
ParserBase *parser = ParserForMagic(device->peek(kMagicSize));
|
||||
if (!parser) {
|
||||
return SongList();
|
||||
}
|
||||
|
||||
return parser->Load(device, path_hint, dir_hint);
|
||||
|
||||
}
|
||||
|
||||
void PlaylistParser::Save(const SongList &songs, const QString &filename, Playlist::Path path_type) const {
|
||||
|
||||
QFileInfo info(filename);
|
||||
|
||||
// Find a parser that supports this file extension
|
||||
ParserBase *parser = ParserForExtension(info.suffix());
|
||||
if (!parser) {
|
||||
qLog(Warning) << "Unknown filetype:" << filename;
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the file
|
||||
QFile file(filename);
|
||||
file.open(QIODevice::WriteOnly);
|
||||
|
||||
return parser->Save(songs, &file, info.absolutePath(), path_type);
|
||||
|
||||
}
|
||||
67
src/playlistparsers/playlistparser.h
Normal file
67
src/playlistparsers/playlistparser.h
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLAYLISTPARSER_H
|
||||
#define PLAYLISTPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QObject>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "playlist/playlist.h"
|
||||
|
||||
class ParserBase;
|
||||
class CollectionBackendInterface;
|
||||
|
||||
class PlaylistParser : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PlaylistParser(CollectionBackendInterface *collection, QObject *parent = nullptr);
|
||||
|
||||
static const int kMagicSize;
|
||||
|
||||
QStringList file_extensions() const;
|
||||
QString filters() const;
|
||||
|
||||
QStringList mime_types() const;
|
||||
|
||||
QString default_extension() const;
|
||||
QString default_filter() const;
|
||||
|
||||
ParserBase *ParserForMagic(const QByteArray &data, const QString &mime_type = QString()) const;
|
||||
ParserBase *ParserForExtension(const QString &suffix) const;
|
||||
ParserBase *ParserForMimeType(const QString &mime) const;
|
||||
|
||||
SongList LoadFromFile(const QString &filename) const;
|
||||
SongList LoadFromDevice(QIODevice *device, const QString &path_hint = QString(), const QDir &dir_hint = QDir()) const;
|
||||
void Save(const SongList &songs, const QString &filename, Playlist::Path) const;
|
||||
|
||||
private:
|
||||
QString FilterForParser(const ParserBase *parser, QStringList *all_extensions = nullptr) const;
|
||||
|
||||
private:
|
||||
QList<ParserBase*> parsers_;
|
||||
ParserBase *default_parser_;
|
||||
};
|
||||
|
||||
#endif // PLAYLISTPARSER_H
|
||||
91
src/playlistparsers/plsparser.cpp
Normal file
91
src/playlistparsers/plsparser.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "plsparser.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/timeconstants.h"
|
||||
|
||||
#include <QTextStream>
|
||||
#include <QtDebug>
|
||||
|
||||
PLSParser::PLSParser(CollectionBackendInterface *collection, QObject *parent)
|
||||
: ParserBase(collection, parent) {}
|
||||
|
||||
SongList PLSParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
|
||||
|
||||
QMap<int, Song> songs;
|
||||
QRegExp n_re("\\d+$");
|
||||
|
||||
while (!device->atEnd()) {
|
||||
QString line = QString::fromUtf8(device->readLine()).trimmed();
|
||||
int equals = line.indexOf('=');
|
||||
QString key = line.left(equals).toLower();
|
||||
QString value = line.mid(equals + 1);
|
||||
|
||||
n_re.indexIn(key);
|
||||
int n = n_re.cap(0).toInt();
|
||||
|
||||
if (key.startsWith("file")) {
|
||||
Song song = LoadSong(value, 0, dir);
|
||||
|
||||
// Use the title and length we've already loaded if any
|
||||
if (!songs[n].title().isEmpty()) song.set_title(songs[n].title());
|
||||
if (songs[n].length_nanosec() != -1)
|
||||
song.set_length_nanosec(songs[n].length_nanosec());
|
||||
|
||||
songs[n] = song;
|
||||
}
|
||||
else if (key.startsWith("title")) {
|
||||
songs[n].set_title(value);
|
||||
}
|
||||
else if (key.startsWith("length")) {
|
||||
qint64 seconds = value.toLongLong();
|
||||
if (seconds > 0) {
|
||||
songs[n].set_length_nanosec(seconds * kNsecPerSec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return songs.values();
|
||||
|
||||
}
|
||||
|
||||
void PLSParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, Playlist::Path path_type) const {
|
||||
|
||||
QTextStream s(device);
|
||||
s << "[playlist]" << endl;
|
||||
s << "Version=2" << endl;
|
||||
s << "NumberOfEntries=" << songs.count() << endl;
|
||||
|
||||
int n = 1;
|
||||
for (const Song &song : songs) {
|
||||
s << "File" << n << "=" << URLOrFilename(song.url(), dir, path_type) << endl;
|
||||
s << "Title" << n << "=" << song.title() << endl;
|
||||
s << "Length" << n << "=" << song.length_nanosec() / kNsecPerSec << endl;
|
||||
++n;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool PLSParser::TryMagic(const QByteArray &data) const {
|
||||
return data.toLower().contains("[playlist]");
|
||||
}
|
||||
44
src/playlistparsers/plsparser.h
Normal file
44
src/playlistparsers/plsparser.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLSPARSER_H
|
||||
#define PLSPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "parserbase.h"
|
||||
|
||||
class PLSParser : public ParserBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PLSParser(CollectionBackendInterface *collection, QObject *parent = nullptr);
|
||||
|
||||
QString name() const { return "PLS"; }
|
||||
QStringList file_extensions() const { return QStringList() << "pls"; }
|
||||
QString mime_type() const { return "audio/x-scpls"; }
|
||||
|
||||
bool TryMagic(const QByteArray &data) const;
|
||||
|
||||
SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const;
|
||||
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const;
|
||||
};
|
||||
|
||||
#endif // PLSPARSER_H
|
||||
120
src/playlistparsers/wplparser.cpp
Normal file
120
src/playlistparsers/wplparser.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2013, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "wplparser.h"
|
||||
#include "core/utilities.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
WplParser::WplParser(CollectionBackendInterface *collection, QObject *parent)
|
||||
: XMLParser(collection, parent) {}
|
||||
|
||||
bool WplParser::TryMagic(const QByteArray &data) const {
|
||||
return data.contains("<?wpl") || data.contains("<smil>");
|
||||
}
|
||||
|
||||
SongList WplParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
|
||||
|
||||
SongList ret;
|
||||
|
||||
QXmlStreamReader reader(device);
|
||||
if (!Utilities::ParseUntilElement(&reader, "smil") || !Utilities::ParseUntilElement(&reader, "body")) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
while (!reader.atEnd() && Utilities::ParseUntilElement(&reader, "seq")) {
|
||||
ParseSeq(dir, &reader, &ret);
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void WplParser::ParseSeq(const QDir &dir, QXmlStreamReader *reader, SongList *songs) const {
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
switch (type) {
|
||||
case QXmlStreamReader::StartElement: {
|
||||
QStringRef name = reader->name();
|
||||
if (name == "media") {
|
||||
QStringRef src = reader->attributes().value("src");
|
||||
if (!src.isEmpty()) {
|
||||
Song song = LoadSong(src.toString(), 0, dir);
|
||||
if (song.is_valid()) {
|
||||
songs->append(song);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Utilities::ConsumeCurrentElement(reader);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QXmlStreamReader::EndElement: {
|
||||
if (reader->name() == "seq") {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void WplParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, Playlist::Path path_type) const {
|
||||
|
||||
QXmlStreamWriter writer(device);
|
||||
writer.setAutoFormatting(true);
|
||||
writer.setAutoFormattingIndent(2);
|
||||
writer.writeProcessingInstruction("wpl", "version=\"1.0\"");
|
||||
|
||||
StreamElement smil("smil", &writer);
|
||||
|
||||
{
|
||||
StreamElement head("head", &writer);
|
||||
WriteMeta("Generator", "Strawberry -- " STRAWBERRY_VERSION_DISPLAY, &writer);
|
||||
WriteMeta("ItemCount", QString::number(songs.count()), &writer);
|
||||
}
|
||||
|
||||
{
|
||||
StreamElement body("body", &writer);
|
||||
{
|
||||
StreamElement seq("seq", &writer);
|
||||
for (const Song &song : songs) {
|
||||
writer.writeStartElement("media");
|
||||
writer.writeAttribute("src", URLOrFilename(song.url(), dir, path_type));
|
||||
writer.writeEndElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WplParser::WriteMeta(const QString &name, const QString &content, QXmlStreamWriter *writer) const {
|
||||
|
||||
writer->writeStartElement("meta");
|
||||
writer->writeAttribute("name", name);
|
||||
writer->writeAttribute("content", content);
|
||||
writer->writeEndElement();
|
||||
|
||||
}
|
||||
46
src/playlistparsers/wplparser.h
Normal file
46
src/playlistparsers/wplparser.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2013, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef WPLPARSER_H
|
||||
#define WPLPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "xmlparser.h"
|
||||
|
||||
class WplParser : public XMLParser {
|
||||
public:
|
||||
WplParser(CollectionBackendInterface *collection, QObject *parent = nullptr);
|
||||
|
||||
QString name() const { return "WPL"; }
|
||||
QStringList file_extensions() const { return QStringList() << "wpl"; }
|
||||
QString mime_type() const { return "application/vnd.ms-wpl"; }
|
||||
|
||||
bool TryMagic(const QByteArray &data) const;
|
||||
|
||||
SongList Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const;
|
||||
void Save(const SongList &songs, QIODevice *device, const QDir &dir, Playlist::Path path_type = Playlist::Path_Automatic) const;
|
||||
|
||||
private:
|
||||
void ParseSeq(const QDir &dir, QXmlStreamReader *reader, SongList *songs) const;
|
||||
void WriteMeta(const QString &name, const QString &content, QXmlStreamWriter *writer) const;
|
||||
};
|
||||
|
||||
#endif // WPLPARSER_H
|
||||
33
src/playlistparsers/xmlparser.cpp
Normal file
33
src/playlistparsers/xmlparser.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "xmlparser.h"
|
||||
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QRegExp>
|
||||
#include <QUrl>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
XMLParser::XMLParser(CollectionBackendInterface *collection, QObject *parent)
|
||||
: ParserBase(collection, parent) {}
|
||||
52
src/playlistparsers/xmlparser.h
Normal file
52
src/playlistparsers/xmlparser.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef XMLPARSER_H
|
||||
#define XMLPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "parserbase.h"
|
||||
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
class QDomDocument;
|
||||
class QDomNode;
|
||||
|
||||
class XMLParser : public ParserBase {
|
||||
protected:
|
||||
XMLParser(CollectionBackendInterface *collection, QObject *parent);
|
||||
|
||||
class StreamElement {
|
||||
public:
|
||||
StreamElement(const QString& name, QXmlStreamWriter *stream) : stream_(stream) {
|
||||
stream->writeStartElement(name);
|
||||
}
|
||||
|
||||
~StreamElement() { stream_->writeEndElement(); }
|
||||
|
||||
private:
|
||||
QXmlStreamWriter *stream_;
|
||||
Q_DISABLE_COPY(StreamElement);
|
||||
};
|
||||
};
|
||||
|
||||
#endif
|
||||
198
src/playlistparsers/xspfparser.cpp
Normal file
198
src/playlistparsers/xspfparser.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "xspfparser.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/utilities.h"
|
||||
|
||||
#include "playlist/playlist.h"
|
||||
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QRegExp>
|
||||
#include <QUrl>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
XSPFParser::XSPFParser(CollectionBackendInterface *collection, QObject *parent)
|
||||
: XMLParser(collection, parent) {}
|
||||
|
||||
SongList XSPFParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
|
||||
SongList ret;
|
||||
|
||||
QXmlStreamReader reader(device);
|
||||
if (!Utilities::ParseUntilElement(&reader, "playlist") || !Utilities::ParseUntilElement(&reader, "trackList")) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
while (!reader.atEnd() && Utilities::ParseUntilElement(&reader, "track")) {
|
||||
Song song = ParseTrack(&reader, dir);
|
||||
if (song.is_valid()) {
|
||||
ret << song;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
Song XSPFParser::ParseTrack(QXmlStreamReader *reader, const QDir &dir) const {
|
||||
|
||||
QString title, artist, album, location;
|
||||
qint64 nanosec = -1;
|
||||
int track_num = -1;
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
switch (type) {
|
||||
case QXmlStreamReader::StartElement: {
|
||||
QStringRef name = reader->name();
|
||||
if (name == "location") {
|
||||
location = reader->readElementText();
|
||||
}
|
||||
else if (name == "title") {
|
||||
title = reader->readElementText();
|
||||
}
|
||||
else if (name == "creator") {
|
||||
artist = reader->readElementText();
|
||||
}
|
||||
else if (name == "album") {
|
||||
album = reader->readElementText();
|
||||
}
|
||||
else if (name == "duration") { // in milliseconds.
|
||||
const QString duration = reader->readElementText();
|
||||
bool ok = false;
|
||||
nanosec = duration.toInt(&ok) * kNsecPerMsec;
|
||||
if (!ok) {
|
||||
nanosec = -1;
|
||||
}
|
||||
}
|
||||
else if (name == "trackNum") {
|
||||
const QString track_num_str = reader->readElementText();
|
||||
bool ok = false;
|
||||
track_num = track_num_str.toInt(&ok);
|
||||
if (!ok || track_num < 1) {
|
||||
track_num = -1;
|
||||
}
|
||||
}
|
||||
else if (name == "image") {
|
||||
// TODO: Fetch album covers.
|
||||
}
|
||||
else if (name == "info") {
|
||||
// TODO: Do something with extra info?
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QXmlStreamReader::EndElement: {
|
||||
if (reader->name() == "track") {
|
||||
goto return_song;
|
||||
}
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return_song:
|
||||
Song song = LoadSong(location, 0, dir);
|
||||
|
||||
// Override metadata with what was in the playlist
|
||||
song.set_title(title);
|
||||
song.set_artist(artist);
|
||||
song.set_album(album);
|
||||
song.set_length_nanosec(nanosec);
|
||||
song.set_track(track_num);
|
||||
return song;
|
||||
|
||||
}
|
||||
|
||||
void XSPFParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, Playlist::Path path_type) const {
|
||||
|
||||
QFileInfo file;
|
||||
QXmlStreamWriter writer(device);
|
||||
writer.setAutoFormatting(true);
|
||||
writer.setAutoFormattingIndent(2);
|
||||
writer.writeStartDocument();
|
||||
StreamElement playlist("playlist", &writer);
|
||||
writer.writeAttribute("version", "1");
|
||||
writer.writeDefaultNamespace("http://xspf.org/ns/0/");
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(Playlist::kSettingsGroup);
|
||||
bool writeMetadata = s.value(Playlist::kWriteMetadata, true).toBool();
|
||||
s.endGroup();
|
||||
|
||||
StreamElement tracklist("trackList", &writer);
|
||||
for (const Song &song : songs) {
|
||||
QString filename_or_url = URLOrFilename(song.url(), dir, path_type).toUtf8();
|
||||
|
||||
StreamElement track("track", &writer);
|
||||
writer.writeTextElement("location", filename_or_url);
|
||||
|
||||
if (writeMetadata) {
|
||||
writer.writeTextElement("title", song.title());
|
||||
if (!song.artist().isEmpty()) {
|
||||
writer.writeTextElement("creator", song.artist());
|
||||
}
|
||||
if (!song.album().isEmpty()) {
|
||||
writer.writeTextElement("album", song.album());
|
||||
}
|
||||
if (song.length_nanosec() != -1) {
|
||||
writer.writeTextElement(
|
||||
"duration", QString::number(song.length_nanosec() / kNsecPerMsec));
|
||||
}
|
||||
if (song.track() > 0) {
|
||||
writer.writeTextElement("trackNum", QString::number(song.track()));
|
||||
}
|
||||
|
||||
QString art = song.art_manual().isEmpty() ? song.art_automatic()
|
||||
: song.art_manual();
|
||||
// Ignore images that are in our resource bundle.
|
||||
if (!art.startsWith(":") && !art.isEmpty()) {
|
||||
QString art_filename;
|
||||
if (!art.contains("://")) {
|
||||
art_filename = art;
|
||||
} else if (QUrl(art).scheme() == "file") {
|
||||
art_filename = QUrl(art).toLocalFile();
|
||||
}
|
||||
|
||||
if (!art_filename.isEmpty() && !(art_filename == "(embedded)")) {
|
||||
// Make this filename relative to the directory we're saving the
|
||||
// playlist.
|
||||
QUrl url = QUrl(art_filename);
|
||||
url.setScheme("file"); // Need to explicitly set this.
|
||||
art_filename = URLOrFilename(url, dir, path_type).toUtf8();
|
||||
} else {
|
||||
// Just use whatever URL was in the Song.
|
||||
art_filename = art;
|
||||
}
|
||||
|
||||
writer.writeTextElement("image", art_filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
writer.writeEndDocument();
|
||||
|
||||
}
|
||||
|
||||
bool XSPFParser::TryMagic(const QByteArray &data) const {
|
||||
return data.contains("<playlist") && data.contains("<trackList");
|
||||
}
|
||||
51
src/playlistparsers/xspfparser.h
Normal file
51
src/playlistparsers/xspfparser.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef XSPFPARSER_H
|
||||
#define XSPFPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "xmlparser.h"
|
||||
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
class QDomDocument;
|
||||
class QDomNode;
|
||||
|
||||
class XSPFParser : public XMLParser {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
XSPFParser(CollectionBackendInterface *collection, QObject *parent = nullptr);
|
||||
|
||||
QString name() const { return "XSPF"; }
|
||||
QStringList file_extensions() const { return QStringList() << "xspf"; }
|
||||
|
||||
bool TryMagic(const QByteArray &data) const;
|
||||
|
||||
SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const;
|
||||
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const;
|
||||
|
||||
private:
|
||||
Song ParseTrack(QXmlStreamReader *reader, const QDir &dir) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user