1 #include "library/rhythmbox/rhythmboxfeature.h"
2 
3 #include <QMessageBox>
4 #include <QStringList>
5 #include <QUrl>
6 #include <QtDebug>
7 
8 #include "library/baseexternalplaylistmodel.h"
9 #include "library/baseexternaltrackmodel.h"
10 #include "library/library.h"
11 #include "library/queryutil.h"
12 #include "library/trackcollection.h"
13 #include "library/trackcollectionmanager.h"
14 #include "library/treeitem.h"
15 #include "moc_rhythmboxfeature.cpp"
16 
RhythmboxFeature(Library * pLibrary,UserSettingsPointer pConfig)17 RhythmboxFeature::RhythmboxFeature(Library* pLibrary, UserSettingsPointer pConfig)
18         : BaseExternalLibraryFeature(pLibrary, pConfig),
19           m_cancelImport(false),
20           m_icon(":/images/library/ic_library_rhythmbox.svg") {
21     QString tableName = "rhythmbox_library";
22     QString idColumn = "id";
23     QStringList columns;
24     columns << "id"
25             << "artist"
26             << "title"
27             << "album"
28             << "year"
29             << "genre"
30             << "tracknumber"
31             << "location"
32             << "comment"
33             << "rating"
34             << "duration"
35             << "bitrate"
36             << "bpm";
37     m_trackSource = QSharedPointer<BaseTrackCache>(
38             new BaseTrackCache(m_pTrackCollection,
39                     tableName, idColumn, columns, false));
40     QStringList searchColumns;
41     searchColumns << "artist"
42                   << "album"
43                   << "location"
44                   << "comment"
45                   << "title"
46                   << "genre";
47     m_trackSource->setSearchColumns(searchColumns);
48 
49     m_pRhythmboxTrackModel = new BaseExternalTrackModel(
50         this, pLibrary->trackCollections(),
51         "mixxx.db.model.rhythmbox",
52         "rhythmbox_library",
53         m_trackSource);
54     m_pRhythmboxPlaylistModel = new BaseExternalPlaylistModel(
55         this, pLibrary->trackCollections(),
56         "mixxx.db.model.rhythmbox_playlist",
57         "rhythmbox_playlists",
58         "rhythmbox_playlist_tracks",
59         m_trackSource);
60 
61     m_isActivated =  false;
62     m_title = tr("Rhythmbox");
63 
64     m_database = QSqlDatabase::cloneDatabase(pLibrary->trackCollections()->internalCollection()->database(),
65                                              "RHYTHMBOX_SCANNER");
66 
67     //Open the database connection in this thread.
68     if (!m_database.open()) {
69         qDebug() << "Failed to open database for Rhythmbox scanner."
70                  << m_database.lastError();
71     }
72     connect(&m_track_watcher,
73             &QFutureWatcher<TreeItem*>::finished,
74             this,
75             &RhythmboxFeature::onTrackCollectionLoaded,
76             Qt::QueuedConnection);
77 }
78 
~RhythmboxFeature()79 RhythmboxFeature::~RhythmboxFeature() {
80     m_database.close();
81     // stop import thread, if still running
82     m_cancelImport = true;
83     m_track_future.waitForFinished();
84     delete m_pRhythmboxTrackModel;
85     delete m_pRhythmboxPlaylistModel;
86 }
87 
getPlaylistModelForPlaylist(const QString & playlist)88 BaseSqlTableModel* RhythmboxFeature::getPlaylistModelForPlaylist(const QString& playlist) {
89     BaseExternalPlaylistModel* pModel = new BaseExternalPlaylistModel(
90                                             this, m_pLibrary->trackCollections(),
91                                             "mixxx.db.model.rhythmbox_playlist",
92                                             "rhythmbox_playlists",
93                                             "rhythmbox_playlist_tracks",
94                                             m_trackSource);
95     pModel->setPlaylist(playlist);
96     return pModel;
97 }
98 
isSupported()99 bool RhythmboxFeature::isSupported() {
100     return (QFile::exists(QDir::homePath() + "/.gnome2/rhythmbox/rhythmdb.xml") ||
101             QFile::exists(QDir::homePath() + "/.local/share/rhythmbox/rhythmdb.xml"));
102 }
103 
title()104 QVariant RhythmboxFeature::title() {
105     return m_title;
106 }
107 
getIcon()108 QIcon RhythmboxFeature::getIcon() {
109     return m_icon;
110 }
111 
getChildModel()112 TreeItemModel* RhythmboxFeature::getChildModel() {
113     return &m_childModel;
114 }
115 
activate()116 void RhythmboxFeature::activate() {
117     qDebug() << "RhythmboxFeature::activate()";
118 
119     if (!m_isActivated) {
120         m_isActivated =  true;
121         m_track_future = QtConcurrent::run(this, &RhythmboxFeature::importMusicCollection);
122         m_track_watcher.setFuture(m_track_future);
123         m_title = "(loading) Rhythmbox";
124         //calls a slot in the sidebar model such that 'Rhythmbox (isLoading)' is displayed.
125         emit featureIsLoading(this, true);
126     }
127 
128     emit showTrackModel(m_pRhythmboxTrackModel);
129     emit enableCoverArtDisplay(false);
130 }
131 
activateChild(const QModelIndex & index)132 void RhythmboxFeature::activateChild(const QModelIndex& index) {
133     //qDebug() << "RhythmboxFeature::activateChild()" << index;
134     QString playlist = index.data().toString();
135     qDebug() << "Activating " << playlist;
136     m_pRhythmboxPlaylistModel->setPlaylist(playlist);
137     emit showTrackModel(m_pRhythmboxPlaylistModel);
138     emit enableCoverArtDisplay(false);
139 }
140 
importMusicCollection()141 TreeItem* RhythmboxFeature::importMusicCollection() {
142     qDebug() << "importMusicCollection Thread Id: " << QThread::currentThread();
143      // Try and open the Rhythmbox DB. An API call which tells us where
144      // the file is would be nice.
145     QFile db(QDir::homePath() + "/.gnome2/rhythmbox/rhythmdb.xml");
146     if (!db.exists()) {
147         db.setFileName(QDir::homePath() + "/.local/share/rhythmbox/rhythmdb.xml");
148         if (!db.exists()) {
149             return nullptr;
150         }
151     }
152 
153     if (!Sandbox::askForAccess(QFileInfo(db).absoluteFilePath()) ||
154             !db.open(QIODevice::ReadOnly)) {
155         return nullptr;
156     }
157 
158     //Delete all table entries of Traktor feature
159     ScopedTransaction transaction(m_database);
160     clearTable("rhythmbox_playlist_tracks");
161     clearTable("rhythmbox_library");
162     clearTable("rhythmbox_playlists");
163     transaction.commit();
164 
165     transaction.transaction();
166     QSqlQuery query(m_database);
167     query.prepare("INSERT INTO rhythmbox_library (artist, title, album, year, "
168                   "genre, comment, tracknumber, bpm, bitrate,"
169                   "duration, location, rating ) "
170                   "VALUES (:artist, :title, :album, :year, :genre, :comment, "
171                   ":tracknumber, :bpm, :bitrate, :duration, :location, :rating )");
172 
173 
174     QXmlStreamReader xml(&db);
175     while (!xml.atEnd() && !m_cancelImport) {
176         xml.readNext();
177         if (xml.isStartElement() && xml.name() == "entry") {
178             QXmlStreamAttributes attr = xml.attributes();
179             //Check if we really parse a track and not album art information
180             if (attr.value("type").toString() == "song") {
181                 importTrack(xml, query);
182             }
183         }
184     }
185     transaction.commit();
186 
187     if (xml.hasError()) {
188         // do error handling
189         qDebug() << "Cannot process Rhythmbox music collection";
190         qDebug() << "XML ERROR: " << xml.errorString();
191         return nullptr;
192     }
193 
194     db.close();
195     if (m_cancelImport) {
196         return nullptr;
197     }
198     return importPlaylists();
199 }
200 
importPlaylists()201 TreeItem* RhythmboxFeature::importPlaylists() {
202     QFile db(QDir::homePath() + "/.gnome2/rhythmbox/playlists.xml");
203     if (!db.exists()) {
204         db.setFileName(QDir::homePath() + "/.local/share/rhythmbox/playlists.xml");
205         if (!db.exists()) {
206             return nullptr;
207         }
208     }
209     //Open file
210     if (!db.open(QIODevice::ReadOnly)) {
211         return nullptr;
212     }
213 
214     QSqlQuery query_insert_to_playlists(m_database);
215     query_insert_to_playlists.prepare("INSERT INTO rhythmbox_playlists (id, name) "
216                                       "VALUES (:id, :name)");
217 
218     QSqlQuery query_insert_to_playlist_tracks(m_database);
219     query_insert_to_playlist_tracks.prepare(
220             "INSERT INTO rhythmbox_playlist_tracks (playlist_id, track_id, position) "
221             "VALUES (:playlist_id, :track_id, :position)");
222     //The tree structure holding the playlists
223     std::unique_ptr<TreeItem> rootItem = TreeItem::newRoot(this);
224 
225     QXmlStreamReader xml(&db);
226     while (!xml.atEnd() && !m_cancelImport) {
227         xml.readNext();
228         if (xml.isStartElement() && xml.name() == "playlist") {
229             QXmlStreamAttributes attr = xml.attributes();
230 
231             //Only parse non build-in playlists
232             if (attr.value("type").toString() == "static") {
233                 QString playlist_name = attr.value("name").toString();
234 
235                 //Construct the childmodel
236                 rootItem->appendChild(playlist_name);
237 
238                 //Execute SQL statement
239                 query_insert_to_playlists.bindValue(":name", playlist_name);
240 
241                 if (!query_insert_to_playlists.exec()) {
242                     LOG_FAILED_QUERY(query_insert_to_playlists)
243                             << "Couldn't insert playlist:" << playlist_name;
244                     continue;
245                 }
246 
247                 // get playlist_id
248                 int playlist_id = query_insert_to_playlists.lastInsertId().toInt();
249 
250                 //Process playlist entries
251                 importPlaylist(xml, query_insert_to_playlist_tracks, playlist_id);
252             }
253         }
254     }
255 
256     if (xml.hasError()) {
257         // do error handling
258         qDebug() << "Cannot process Rhythmbox music collection";
259         qDebug() << "XML ERROR: " << xml.errorString();
260         return nullptr;
261     }
262     db.close();
263 
264     return rootItem.release();
265 }
266 
importTrack(QXmlStreamReader & xml,QSqlQuery & query)267 void RhythmboxFeature::importTrack(QXmlStreamReader &xml, QSqlQuery &query) {
268     QString title;
269     QString artist;
270     QString album;
271     QString year;
272     QString genre;
273     QUrl locationUrl;
274 
275     int bpm = 0;
276     int bitrate = 0;
277 
278     //duration of a track
279     int playtime = 0;
280     int rating = 0;
281     QString comment;
282     QString tracknumber;
283 
284     while (!xml.atEnd()) {
285         xml.readNext();
286         if (xml.isStartElement()) {
287             if (xml.name() == "title") {
288                 title = xml.readElementText();
289                 continue;
290             }
291             if (xml.name() == "artist") {
292                 artist = xml.readElementText();
293                 continue;
294             }
295             if (xml.name() == "genre") {
296                 genre = xml.readElementText();
297                 continue;
298             }
299             if (xml.name() == "album") {
300                 album = xml.readElementText();
301                 continue;
302             }
303             if (xml.name() == "track-number") {
304                 tracknumber = xml.readElementText();
305                 continue;
306             }
307             if (xml.name() == "duration") {
308                 playtime = xml.readElementText().toInt();;
309                 continue;
310             }
311             if (xml.name() == "bitrate") {
312                 bitrate = xml.readElementText().toInt();
313                 continue;
314             }
315             if (xml.name() == "beats-per-minute") {
316                 bpm = xml.readElementText().toInt();
317                 continue;
318             }
319             if (xml.name() == "comment") {
320                 comment = xml.readElementText();
321                 continue;
322             }
323             if (xml.name() == "location") {
324                 locationUrl = QUrl(xml.readElementText());
325                 continue;
326             }
327         }
328         //exit the loop if we reach the closing <entry> tag
329         if (xml.isEndElement() && xml.name() == "entry") {
330             break;
331         }
332     }
333 
334     const auto trackFile = TrackFile::fromUrl(locationUrl);
335     QString location = trackFile.location();
336     if (location.isEmpty()) {
337         // here in case of smb:// location
338         // TODO(XXX) QUrl does not support SMB:// locations does Mixxx?
339         // use ~/.gvfs location instead
340         return;
341     }
342 
343     query.bindValue(":artist", artist);
344     query.bindValue(":title", title);
345     query.bindValue(":album", album);
346     query.bindValue(":genre", genre);
347     query.bindValue(":year", year);
348     query.bindValue(":duration", playtime);
349     query.bindValue(":location", location);
350     query.bindValue(":rating", rating);
351     query.bindValue(":comment", comment);
352     query.bindValue(":tracknumber", tracknumber);
353     query.bindValue(":bpm", bpm);
354     query.bindValue(":bitrate", bitrate);
355 
356     bool success = query.exec();
357 
358     if (!success) {
359         qDebug() << "SQL Error in rhythmboxfeature.cpp: line" << __LINE__
360                  << " " << query.lastError();
361         return;
362     }
363 }
364 
365 // reads all playlist entries and executes a SQL statement
importPlaylist(QXmlStreamReader & xml,QSqlQuery & query_insert_to_playlist_tracks,int playlist_id)366 void RhythmboxFeature::importPlaylist(QXmlStreamReader &xml,
367                                       QSqlQuery &query_insert_to_playlist_tracks,
368                                       int playlist_id) {
369     int playlist_position = 1;
370     while (!xml.atEnd()) {
371         //read next XML element
372         xml.readNext();
373         if (xml.isStartElement() && xml.name() == "location") {
374             const auto trackFile = TrackFile::fromUrl(xml.readElementText());
375 
376             //get the ID of the file in the rhythmbox_library table
377             int track_id = -1;
378             QSqlQuery finder_query(m_database);
379             finder_query.prepare("select id from rhythmbox_library where location=:path");
380             finder_query.bindValue(":path", trackFile.location());
381             bool success = finder_query.exec();
382 
383             if (success) {
384                 const int idColumn = finder_query.record().indexOf("id");
385                 while (finder_query.next()) {
386                     track_id = finder_query.value(idColumn).toInt();
387                 }
388              } else {
389                 qDebug() << "SQL Error in RhythmboxFeature.cpp: line"
390                          << __LINE__ << " " << finder_query.lastError();
391             }
392 
393             query_insert_to_playlist_tracks.bindValue(":playlist_id", playlist_id);
394             query_insert_to_playlist_tracks.bindValue(":track_id", track_id);
395             query_insert_to_playlist_tracks.bindValue(":position", playlist_position++);
396             success = query_insert_to_playlist_tracks.exec();
397 
398             if (!success) {
399                 qDebug() << "SQL Error in RhythmboxFeature.cpp: line" << __LINE__ << " "
400                          << query_insert_to_playlist_tracks.lastError()
401                          << "trackid" << track_id
402                          << "playlis ID " << playlist_id
403                          << "-----------------";
404             }
405         }
406         // Exit the the loop if we reach the closing <playlist> tag
407         if (xml.isEndElement() && xml.name() == "playlist") {
408             break;
409         }
410     }
411 }
412 
clearTable(const QString & table_name)413 void RhythmboxFeature::clearTable(const QString& table_name) {
414     qDebug() << "clearTable Thread Id: " << QThread::currentThread();
415     QSqlQuery query(m_database);
416     query.prepare("delete from "+table_name);
417     bool success = query.exec();
418 
419     if (!success) {
420         qDebug() << "Could not delete remove old entries from table "
421                  << table_name << " : " << query.lastError();
422     } else {
423         qDebug() << "Rhythmbox table entries of '" << table_name
424                  << "' have been cleared.";
425     }
426 }
427 
onTrackCollectionLoaded()428 void RhythmboxFeature::onTrackCollectionLoaded() {
429     std::unique_ptr<TreeItem> root(m_track_future.result());
430     if (root) {
431         m_childModel.setRootItem(std::move(root));
432 
433         // Tell the rhythmbox track source that it should re-build its index.
434         m_trackSource->buildIndex();
435 
436         //m_pRhythmboxTrackModel->select();
437     } else {
438          qDebug() << "Rhythmbox Playlists loaded: false";
439     }
440 
441     // calls a slot in the sidebarmodel such that 'isLoading' is removed from
442     // the feature title.
443     m_title = tr("Rhythmbox");
444     emit featureLoadingFinished(this);
445     activate();
446 }
447