1 /*
2 * Strawberry Music Player
3 * This file was part of Clementine.
4 * Copyright 2010, David Sansome <me@davidsansome.com>
5 *
6 * Strawberry is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * Strawberry is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
18 *
19 */
20
21 #include <QtGlobal>
22 #include <QObject>
23 #include <QIODevice>
24 #include <QDir>
25 #include <QFileInfo>
26 #include <QByteArray>
27 #include <QVariant>
28 #include <QString>
29 #include <QUrl>
30 #include <QSettings>
31 #include <QXmlStreamReader>
32 #include <QXmlStreamWriter>
33
34 #include "core/timeconstants.h"
35 #include "core/utilities.h"
36 #include "playlist/playlist.h"
37 #include "playlistparsers/xmlparser.h"
38 #include "xspfparser.h"
39
40 class CollectionBackendInterface;
41
XSPFParser(CollectionBackendInterface * collection,QObject * parent)42 XSPFParser::XSPFParser(CollectionBackendInterface *collection, QObject *parent)
43 : XMLParser(collection, parent) {}
44
Load(QIODevice * device,const QString & playlist_path,const QDir & dir,const bool collection_search) const45 SongList XSPFParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_search) const {
46
47 Q_UNUSED(playlist_path);
48
49 SongList ret;
50
51 QXmlStreamReader reader(device);
52 if (!Utilities::ParseUntilElement(&reader, "playlist") || !Utilities::ParseUntilElement(&reader, "trackList")) {
53 return ret;
54 }
55
56 while (!reader.atEnd() && Utilities::ParseUntilElement(&reader, "track")) {
57 Song song = ParseTrack(&reader, dir, collection_search);
58 if (song.is_valid()) {
59 ret << song;
60 }
61 }
62 return ret;
63
64 }
65
ParseTrack(QXmlStreamReader * reader,const QDir & dir,const bool collection_search) const66 Song XSPFParser::ParseTrack(QXmlStreamReader *reader, const QDir &dir, const bool collection_search) const {
67
68 QString title, artist, album, location, art;
69 qint64 nanosec = -1;
70 int track_num = -1;
71
72 while (!reader->atEnd()) {
73 QXmlStreamReader::TokenType type = reader->readNext();
74 QString name = reader->name().toString();
75 switch (type) {
76 case QXmlStreamReader::StartElement: {
77 if (name == "location") {
78 location = reader->readElementText();
79 }
80 else if (name == "title") {
81 title = reader->readElementText();
82 }
83 else if (name == "creator") {
84 artist = reader->readElementText();
85 }
86 else if (name == "album") {
87 album = reader->readElementText();
88 }
89 else if (name == "image") {
90 art = reader->readElementText();
91 }
92 else if (name == "duration") { // in milliseconds.
93 const QString duration = reader->readElementText();
94 bool ok = false;
95 nanosec = duration.toInt(&ok) * kNsecPerMsec;
96 if (!ok) {
97 nanosec = -1;
98 }
99 }
100 else if (name == "trackNum") {
101 const QString track_num_str = reader->readElementText();
102 bool ok = false;
103 track_num = track_num_str.toInt(&ok);
104 if (!ok || track_num < 1) {
105 track_num = -1;
106 }
107 }
108 else if (name == "info") {
109 // TODO: Do something with extra info?
110 }
111 break;
112 }
113 case QXmlStreamReader::EndElement: {
114 if (name == "track") {
115 goto return_song;
116 }
117 }
118 default:
119 break;
120 }
121 }
122
123 return_song:
124 Song song = LoadSong(location, 0, dir, collection_search);
125
126 // Override metadata with what was in the playlist
127 if (song.source() != Song::Source_Collection) {
128 if (!title.isEmpty()) song.set_title(title);
129 if (!artist.isEmpty()) song.set_artist(artist);
130 if (!album.isEmpty()) song.set_album(album);
131 if (!art.isEmpty()) song.set_art_manual(QUrl(art));
132 if (nanosec > 0) song.set_length_nanosec(nanosec);
133 if (track_num > 0) song.set_track(track_num);
134 }
135
136 return song;
137
138 }
139
Save(const SongList & songs,QIODevice * device,const QDir & dir,Playlist::Path path_type) const140 void XSPFParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, Playlist::Path path_type) const {
141
142 QXmlStreamWriter writer(device);
143 writer.setAutoFormatting(true);
144 writer.setAutoFormattingIndent(2);
145 writer.writeStartDocument();
146 StreamElement playlist("playlist", &writer);
147 writer.writeAttribute("version", "1");
148 writer.writeDefaultNamespace("http://xspf.org/ns/0/");
149
150 QSettings s;
151 s.beginGroup(Playlist::kSettingsGroup);
152 bool writeMetadata = s.value(Playlist::kWriteMetadata, true).toBool();
153 s.endGroup();
154
155 StreamElement tracklist("trackList", &writer);
156 for (const Song &song : songs) {
157 QString filename_or_url = URLOrFilename(song.url(), dir, path_type).toUtf8();
158
159 StreamElement track("track", &writer);
160 writer.writeTextElement("location", filename_or_url);
161
162 if (writeMetadata) {
163 writer.writeTextElement("title", song.title());
164 if (!song.artist().isEmpty()) {
165 writer.writeTextElement("creator", song.artist());
166 }
167 if (!song.album().isEmpty()) {
168 writer.writeTextElement("album", song.album());
169 }
170 if (song.length_nanosec() != -1) {
171 writer.writeTextElement("duration", QString::number(song.length_nanosec() / kNsecPerMsec));
172 }
173 if (song.track() > 0) {
174 writer.writeTextElement("trackNum", QString::number(song.track()));
175 }
176
177 QUrl cover_url = song.art_manual().isEmpty() || song.art_manual().path().isEmpty() ? song.art_automatic() : song.art_manual();
178 // Ignore images that are in our resource bundle.
179 if (!cover_url.isEmpty() && !cover_url.path().isEmpty() && cover_url.path() != Song::kManuallyUnsetCover && cover_url.path() != Song::kEmbeddedCover) {
180 if (cover_url.scheme().isEmpty()) {
181 cover_url.setScheme("file");
182 }
183 QString cover_filename = URLOrFilename(cover_url, dir, path_type).toUtf8();
184 writer.writeTextElement("image", cover_filename);
185 }
186 }
187 }
188
189 writer.writeEndDocument();
190
191 }
192
TryMagic(const QByteArray & data) const193 bool XSPFParser::TryMagic(const QByteArray &data) const {
194 return data.contains("<playlist") && data.contains("<trackList");
195 }
196