diff --git a/data/data.qrc b/data/data.qrc index f485e9b10..63882f474 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -295,6 +295,7 @@ schema/schema-23.sql schema/schema-24.sql schema/schema-25.sql + schema/schema-26.sql pythonstartup.py diff --git a/data/schema/schema-26.sql b/data/schema/schema-26.sql new file mode 100644 index 000000000..0de6687b1 --- /dev/null +++ b/data/schema/schema-26.sql @@ -0,0 +1,5 @@ +ALTER TABLE playlist_items ADD COLUMN beginning INTEGER; + +ALTER TABLE playlist_items ADD COLUMN cue_path TEXT; + +UPDATE schema_version SET version=26; diff --git a/src/core/database.cpp b/src/core/database.cpp index da61fb614..b9c3dc471 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -31,7 +31,7 @@ #include const char* Database::kDatabaseFilename = "clementine.db"; -const int Database::kSchemaVersion = 25; +const int Database::kSchemaVersion = 26; const char* Database::kMagicAllSongsTables = "%allsongstables"; int Database::sNextConnectionId = 1; diff --git a/src/playlist/playlistbackend.cpp b/src/playlist/playlistbackend.cpp index 92942a2cc..af5cc82e2 100644 --- a/src/playlist/playlistbackend.cpp +++ b/src/playlist/playlistbackend.cpp @@ -21,21 +21,33 @@ #include "core/song.h" #include "library/librarybackend.h" #include "library/sqlrow.h" +#include "playlist/songplaylistitem.h" +#include "playlistparsers/cueparser.h" #include "smartplaylists/generator.h" +#include +#include +#include #include #include #include +#include + using smart_playlists::GeneratorPtr; using boost::shared_ptr; PlaylistBackend::PlaylistBackend(QObject* parent) - : QObject(parent) + : QObject(parent), + library_(NULL) { } +void PlaylistBackend::SetLibrary(LibraryBackend* library) { + library_ = library; +} + PlaylistBackend::PlaylistList PlaylistBackend::GetAllPlaylists() { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); @@ -97,7 +109,7 @@ QFuture PlaylistBackend::GetPlaylistItems(int playlist) { " magnatune_songs.ROWID, " + Song::JoinSpec("magnatune_songs") + "," " jamendo_songs.ROWID, " + Song::JoinSpec("jamendo_songs") + "," " p.type, p.url, p.title, p.artist, p.album, p.length," - " p.radio_service" + " p.radio_service, p.beginning, p.cue_path" " FROM playlist_items AS p" " LEFT JOIN songs" " ON p.library_id = songs.ROWID" @@ -119,18 +131,74 @@ QFuture PlaylistBackend::GetPlaylistItems(int playlist) { rows << SqlRow(q); } - return QtConcurrent::mapped(rows, &PlaylistBackend::NewSongFromQuery); + // it's probable that we'll have a few songs associated with the + // same CUE so we're caching results of parsing CUEs + boost::shared_ptr state_ptr(new NewSongFromQueryState()); + return QtConcurrent::mapped(rows, boost::bind(&PlaylistBackend::NewSongFromQuery, this, _1, state_ptr)); } -PlaylistItemPtr PlaylistBackend::NewSongFromQuery(const SqlRow& row) { +PlaylistItemPtr PlaylistBackend::NewSongFromQuery(const SqlRow& row, boost::shared_ptr state) { // The song tables get joined first, plus one each for the song ROWIDs const int playlist_row = (Song::kColumns.count() + 1) * 3; - PlaylistItemPtr item( - PlaylistItem::NewFromType(row.value(playlist_row).toString())); + PlaylistItemPtr item(PlaylistItem::NewFromType(row.value(playlist_row).toString())); if (item) { item->InitFromQuery(row); + return RestoreCueData(item, state); + } else { + return item; } +} + +// If song had a CUE and the CUE still exists, the metadata from it will +// be applied here. +PlaylistItemPtr PlaylistBackend::RestoreCueData(PlaylistItemPtr item, boost::shared_ptr state) { + // we need library to run a CueParser; also, this method applies only to + // file-type PlaylistItems + if(!library_ || item->type() != "File") { + return item; + } + CueParser cue_parser(library_); + + Song song = item->Metadata(); + // we're only interested in .cue songs here + if(!song.has_cue()) { + return item; + } + + QString cue_path = song.cue_path(); + // if .cue was deleted - reload the song + if(!QFile::exists(cue_path)) { + item->Reload(); + return item; + } + + SongList song_list; + { + QMutexLocker locker(&state->mutex_); + + if(!state->cached_cues_.contains(cue_path)) { + QFile cue(cue_path); + cue.open(QIODevice::ReadOnly); + + song_list = cue_parser.Load(&cue, cue_path, QDir(cue_path.section('/', 0, -2))); + state->cached_cues_[cue_path] = song_list; + } else { + song_list = state->cached_cues_[cue_path]; + } + } + + foreach(const Song& from_list, song_list) { + if(from_list.filename() == song.filename() && + from_list.beginning() == song.beginning()) { + // we found a matching section; replace the input + // item with a new one containing CUE metadata + return PlaylistItemPtr(new SongPlaylistItem(from_list)); + } + } + + // there's no such section in the related .cue -> reload the song + item->Reload(); return item; } @@ -151,8 +219,8 @@ void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList& items, QSqlQuery clear("DELETE FROM playlist_items WHERE playlist = :playlist", db); QSqlQuery insert("INSERT INTO playlist_items" " (playlist, type, library_id, url, title, artist, album," - " length, radio_service)" - " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", db); + " length, radio_service, beginning, cue_path)" + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", db); QSqlQuery update("UPDATE playlists SET " " last_played=:last_played," " dynamic_playlist_type=:dynamic_type," diff --git a/src/playlist/playlistbackend.h b/src/playlist/playlistbackend.h index 7285d4565..39098792d 100644 --- a/src/playlist/playlistbackend.h +++ b/src/playlist/playlistbackend.h @@ -19,7 +19,9 @@ #define PLAYLISTBACKEND_H #include +#include #include +#include #include #include "playlistitem.h" @@ -28,6 +30,7 @@ #include class Database; +class LibraryBackend; class PlaylistBackend : public QObject { Q_OBJECT @@ -58,12 +61,22 @@ class PlaylistBackend : public QObject { void RemovePlaylist(int id); void RenamePlaylist(int id, const QString& new_name); + void SetLibrary(LibraryBackend* library); + public slots: void SavePlaylist(int playlist, const PlaylistItemList& items, int last_played, smart_playlists::GeneratorPtr dynamic); private: - static PlaylistItemPtr NewSongFromQuery(const SqlRow& row); + struct NewSongFromQueryState { + QHash cached_cues_; + QMutex mutex_; + }; + + PlaylistItemPtr NewSongFromQuery(const SqlRow& row, boost::shared_ptr state); + PlaylistItemPtr RestoreCueData(PlaylistItemPtr item, boost::shared_ptr state); + + LibraryBackend* library_; boost::shared_ptr db_; }; diff --git a/src/playlist/playlistitem.cpp b/src/playlist/playlistitem.cpp index 9b0ea6df6..3d783ccd5 100644 --- a/src/playlist/playlistitem.cpp +++ b/src/playlist/playlistitem.cpp @@ -71,6 +71,8 @@ void PlaylistItem::BindToQuery(QSqlQuery* query) const { query->bindValue(6, DatabaseValue(Column_Album)); query->bindValue(7, DatabaseValue(Column_Length)); query->bindValue(8, DatabaseValue(Column_RadioService)); + query->bindValue(9, DatabaseValue(Column_Beginning)); + query->bindValue(10, DatabaseValue(Column_CuePath)); } void PlaylistItem::SetTemporaryMetadata(const Song& metadata) { diff --git a/src/playlist/playlistitem.h b/src/playlist/playlistitem.h index 868b7c5fa..995569cf4 100644 --- a/src/playlist/playlistitem.h +++ b/src/playlist/playlistitem.h @@ -128,6 +128,8 @@ class PlaylistItem : public boost::enable_shared_from_this { Column_Album, Column_Length, Column_RadioService, + Column_Beginning, + Column_CuePath, }; virtual QVariant DatabaseValue(DatabaseColumn) const { diff --git a/src/playlist/songplaylistitem.cpp b/src/playlist/songplaylistitem.cpp index 5cda1afb5..1d39e9e6d 100644 --- a/src/playlist/songplaylistitem.cpp +++ b/src/playlist/songplaylistitem.cpp @@ -55,18 +55,27 @@ bool SongPlaylistItem::InitFromQuery(const SqlRow& query) { if (!song_.is_valid()) return false; + + int beginning(query.value(row + 7).toInt()); + QString cue_path(query.value(row + 8).toString()); + + song_.set_beginning(beginning); + song_.set_cue_path(cue_path); } + return true; } QVariant SongPlaylistItem::DatabaseValue(DatabaseColumn column) const { switch (column) { - case Column_Url: return song_.filename(); - case Column_Title: return song_.title(); - case Column_Artist: return song_.artist(); - case Column_Album: return song_.album(); - case Column_Length: return song_.length(); - default: return PlaylistItem::DatabaseValue(column); + case Column_Url: return song_.filename(); + case Column_Title: return song_.title(); + case Column_Artist: return song_.artist(); + case Column_Album: return song_.album(); + case Column_Length: return song_.length(); + case Column_Beginning: return song_.beginning(); + case Column_CuePath: return song_.cue_path(); + default: return PlaylistItem::DatabaseValue(column); } } diff --git a/src/playlist/songplaylistitem.h b/src/playlist/songplaylistitem.h index 9f5b6b489..f5a5595a7 100644 --- a/src/playlist/songplaylistitem.h +++ b/src/playlist/songplaylistitem.h @@ -26,6 +26,9 @@ class SongPlaylistItem : public PlaylistItem { SongPlaylistItem(const QString& type); SongPlaylistItem(const Song& song); + // Restores a stream- or file-related playlist item using query row. + // If it's a file related playlist item, this will restore it's CUE + // attributes (if any) but won't parse the CUE! bool InitFromQuery(const SqlRow& query); void Reload(); diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 443c26eb6..2a9ddbc1a 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -197,6 +197,8 @@ MainWindow::MainWindow( library_ = new Library(database_, task_manager_, this); devices_ = new DeviceManager(database_, task_manager_, this); + playlist_backend_->SetLibrary(library_->backend()); + // Initialise the UI ui_->setupUi(this); ui_->multi_loading_indicator->SetTaskManager(task_manager_);