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