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