diff --git a/data/data.qrc b/data/data.qrc index 32add3588..84cb9fbbc 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -293,5 +293,6 @@ schema/schema-22.sql schema/jamendo.sql schema/schema-23.sql + schema/schema-24.sql diff --git a/data/schema/jamendo.sql b/data/schema/jamendo.sql index af34e7537..db23e703f 100644 --- a/data/schema/jamendo.sql +++ b/data/schema/jamendo.sql @@ -35,7 +35,8 @@ CREATE TABLE jamendo.songs ( forced_compilation_off INTEGER NOT NULL DEFAULT 0, effective_compilation NOT NULL DEFAULT 0, skipcount NOT NULL DEFAULT 0, - score NOT NULL DEFAULT 0 + score NOT NULL DEFAULT 0, + beginning NOT NULL DEFAULT 0 ); CREATE VIRTUAL TABLE jamendo.songs_fts USING fts3( diff --git a/data/schema/schema-24.sql b/data/schema/schema-24.sql new file mode 100644 index 000000000..d010867f5 --- /dev/null +++ b/data/schema/schema-24.sql @@ -0,0 +1,3 @@ +ALTER TABLE %allsongstables ADD COLUMN beginning INTEGER NOT NULL DEFAULT 0; + +UPDATE schema_version SET version=24; diff --git a/src/core/database.cpp b/src/core/database.cpp index 5128b182e..058d0fb14 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 = 23; +const int Database::kSchemaVersion = 24; const char* Database::kMagicAllSongsTables = "%allsongstables"; int Database::sNextConnectionId = 1; @@ -536,10 +536,25 @@ void Database::ExecCommands(const QString &schema, QSqlDatabase &db) { QStringList Database::SongsTables(QSqlDatabase& db) const { QStringList ret; + + // look for the tables in the main db foreach (const QString& table, db.tables()) { if (table == "songs" || table.endsWith("_songs")) ret << table; } + + // look for the tables in attached dbs + foreach (const QString& key, attached_databases_.keys()) { + QSqlQuery q(QString("SELECT NAME FROM %1.sqlite_master" + " WHERE type='table' AND name='songs' OR name LIKE '%songs'").arg(key), db); + if (q.exec()) { + while(q.next()) { + QString tab_name = key + "." + q.value(0).toString(); + ret << tab_name; + } + } + } + return ret; } diff --git a/src/core/song.cpp b/src/core/song.cpp index c1fc47270..91e7921bb 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -93,7 +93,7 @@ const QStringList Song::kColumns = QStringList() << "mtime" << "ctime" << "filesize" << "sampler" << "art_automatic" << "art_manual" << "filetype" << "playcount" << "lastplayed" << "rating" << "forced_compilation_on" << "forced_compilation_off" - << "effective_compilation" << "skipcount" << "score"; + << "effective_compilation" << "skipcount" << "score" << "beginning"; const QString Song::kColumnSpec = Song::kColumns.join(", "); const QString Song::kBindSpec = Prepend(":", Song::kColumns).join(", "); @@ -466,8 +466,6 @@ void Song::InitFromQuery(const SqlRow& q, int col) { d->comment_ = tostr(col + 11); d->compilation_ = q.value(col + 12).toBool(); - // TODO: this should be replaced by beginning and end - set_length(toint(col + 13)); d->bitrate_ = toint(col + 14); d->samplerate_ = toint(col + 15); @@ -494,9 +492,12 @@ void Song::InitFromQuery(const SqlRow& q, int col) { // effective_compilation = 30 d->skipcount_ = q.value(col + 31).isNull() ? 0 : q.value(col + 31).toInt(); - d->score_ = q.value(col + 32).isNull() ? 0 : q.value(col + 32).toInt(); + // TODO: 'end' instead of 'length' + d->beginning_ = q.value(col + 33).isNull() ? 0 : q.value(col + 33).toInt(); + set_length(toint(col + 13)); + #undef tostr #undef toint #undef tofloat @@ -926,7 +927,7 @@ void Song::BindToQuery(QSqlQuery *query) const { query->bindValue(":comment", strval(d->comment_)); query->bindValue(":compilation", d->compilation_ ? 1 : 0); - // TODO: replace this with beginning and end + // TODO: replace this with 'end' query->bindValue(":length", intval(length())); query->bindValue(":bitrate", intval(d->bitrate_)); query->bindValue(":samplerate", intval(d->samplerate_)); @@ -952,9 +953,10 @@ void Song::BindToQuery(QSqlQuery *query) const { query->bindValue(":effective_compilation", is_compilation() ? 1 : 0); query->bindValue(":skipcount", d->skipcount_); - query->bindValue(":score", d->score_); + query->bindValue(":beginning", d->beginning_); + #undef intval #undef notnullintval } @@ -1042,7 +1044,8 @@ bool Song::IsMetadataEqual(const Song& other) const { d->genre_ == other.d->genre_ && d->comment_ == other.d->comment_ && d->compilation_ == other.d->compilation_ && - // this should be replaced by beginning and end + d->beginning_ == other.d->beginning_ && + // this should be replaced by 'end' length() == other.length() && d->bitrate_ == other.d->bitrate_ && d->samplerate_ == other.d->samplerate_ && diff --git a/src/library/librarywatcher.cpp b/src/library/librarywatcher.cpp index 4afc197ef..2c34fff44 100644 --- a/src/library/librarywatcher.cpp +++ b/src/library/librarywatcher.cpp @@ -18,6 +18,7 @@ #include "librarywatcher.h" #include "librarybackend.h" #include "core/taskmanager.h" +#include "playlistparsers/cueparser.h" #include #include @@ -26,12 +27,15 @@ #include #include #include +#include #include #include +// TODO: test removing a folder with cues +// TODO: what about .cue vs it's media file changes? + QStringList LibraryWatcher::sValidImages; -QStringList LibraryWatcher::sValidPlaylists; const char* LibraryWatcher::kSettingsGroup = "LibraryWatcher"; @@ -44,14 +48,14 @@ LibraryWatcher::LibraryWatcher(QObject* parent) monitor_(true), rescan_timer_(new QTimer(this)), rescan_paused_(false), - total_watches_(0) + total_watches_(0), + cue_parser_(new CueParser(backend_, this)) { rescan_timer_->setInterval(1000); rescan_timer_->setSingleShot(true); if (sValidImages.isEmpty()) { sValidImages << "jpg" << "png" << "gif" << "jpeg"; - sValidPlaylists << "m3u" << "pls"; } ReloadSettings(); @@ -278,6 +282,8 @@ void LibraryWatcher::ScanSubdirectory( // Ask the database for a list of files in this directory SongList songs_in_db = t->FindSongsInSubdirectory(path); + QSet cues_processed; + // Now compare the list from the database with the list of files on disk foreach (const QString& file, files_on_disk) { if (stop_requested_) return; @@ -304,6 +310,7 @@ void LibraryWatcher::ScanSubdirectory( changed = true; } + // TODO: cues if (changed) { qDebug() << file << "changed"; @@ -334,18 +341,42 @@ void LibraryWatcher::ScanSubdirectory( } } else { // The song is on disk but not in the DB + SongList song_list; - Song song; - song.InitFromFile(file, t->dir()); - if (!song.is_valid()) + QString matching_cue = NoExtensionPart(file) + ".cue"; + + // don't process the same cue many times + if(cues_processed.contains(matching_cue)) continue; + + // it's a cue - create virtual tracks + if(QFile::exists(matching_cue)) { + QFile cue(matching_cue); + cue.open(QIODevice::ReadOnly); + + song_list = cue_parser_->Load(&cue, path); + cues_processed << matching_cue; + + // it's a normal media file + } else { + Song song; + song.InitFromFile(file, -1); + if (!song.is_valid()) + continue; + song_list << song; + } + qDebug() << file << "created"; + // choose an image for the song(s) + QString image = ImageForSong(file, album_art); - // Choose an image for the song - if (song.art_automatic().isEmpty()) - song.set_art_automatic(ImageForSong(file, album_art)); + foreach (Song song, song_list) { + song.set_directory_id(t->dir()); + if (song.art_automatic().isEmpty()) + song.set_art_automatic(image); - t->new_songs << song; + t->new_songs << song; + } } } diff --git a/src/library/librarywatcher.h b/src/library/librarywatcher.h index 6ccaabb64..212ea1beb 100644 --- a/src/library/librarywatcher.h +++ b/src/library/librarywatcher.h @@ -28,6 +28,7 @@ class QFileSystemWatcher; class QTimer; +class CueParser; class LibraryBackend; class TaskManager; @@ -126,6 +127,7 @@ class LibraryWatcher : public QObject { private: static bool FindSongByPath(const SongList& list, const QString& path, Song* out); + inline static QString NoExtensionPart( const QString &fileName ); inline static QString ExtensionPart( const QString &fileName ); inline static QString DirectoryPart( const QString &fileName ); QString PickBestImage(const QStringList& images); @@ -161,14 +163,18 @@ class LibraryWatcher : public QObject { int total_watches_; + CueParser* cue_parser_; + static QStringList sValidImages; - static QStringList sValidPlaylists; #ifdef Q_OS_DARWIN static const int kMaxWatches = 100; #endif }; +inline QString LibraryWatcher::NoExtensionPart( const QString &fileName ) { + return fileName.contains( '.' ) ? fileName.section( '.', 0, -2 ) : ""; +} // Thanks Amarok inline QString LibraryWatcher::ExtensionPart( const QString &fileName ) { return fileName.contains( '.' ) ? fileName.mid( fileName.lastIndexOf('.') + 1 ).toLower() : ""; diff --git a/src/playlist/playlistbackend.cpp b/src/playlist/playlistbackend.cpp index 2bd2a6985..92942a2cc 100644 --- a/src/playlist/playlistbackend.cpp +++ b/src/playlist/playlistbackend.cpp @@ -93,19 +93,21 @@ QFuture PlaylistBackend::GetPlaylistItems(int playlist) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - QSqlQuery q("SELECT songs.ROWID, " + Song::JoinSpec("songs") + "," - " 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" - " FROM playlist_items AS p" - " LEFT JOIN songs" - " ON p.library_id = songs.ROWID" - " LEFT JOIN magnatune_songs" - " ON p.library_id = magnatune_songs.ROWID" - " LEFT JOIN jamendo.songs AS jamendo_songs" - " ON p.library_id = jamendo_songs.ROWID" - " WHERE p.playlist = :playlist", db); + QString query = "SELECT songs.ROWID, " + Song::JoinSpec("songs") + "," + " 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" + " FROM playlist_items AS p" + " LEFT JOIN songs" + " ON p.library_id = songs.ROWID" + " LEFT JOIN magnatune_songs" + " ON p.library_id = magnatune_songs.ROWID" + " LEFT JOIN jamendo.songs AS jamendo_songs" + " ON p.library_id = jamendo_songs.ROWID" + " WHERE p.playlist = :playlist"; + QSqlQuery q(query, db); + q.bindValue(":playlist", playlist); q.exec(); if (db_->CheckErrors(q.lastError())) diff --git a/src/playlistparsers/cueparser.cpp b/src/playlistparsers/cueparser.cpp index fc3bb295a..0561c120b 100644 --- a/src/playlistparsers/cueparser.cpp +++ b/src/playlistparsers/cueparser.cpp @@ -175,13 +175,14 @@ SongList CueParser::Load(QIODevice* device, const QDir& dir) const { if (!ParseTrackLocation(entry.file, dir, ¤t)) { qWarning() << "failed to parse location in .cue file from " << dir_path; } else { - Song song = LoadLibrarySong(current.filename()); + // look for the section in library + Song song = LoadLibrarySong(current.filename(), IndexToMarker(entry.index)); if (!song.is_valid()) { song.InitFromFile(current.filename(), -1); } - // overwrite the stuff, we may have read from the file, using - // the .cue's metadata + // overwrite the stuff, we may have read from the file or library, using + // the current .cue metadata if(i + 1 < entries.size()) { // incorrect indices? if(!UpdateSong(entry, entries.at(i + 1).index, &song)) { diff --git a/src/playlistparsers/parserbase.cpp b/src/playlistparsers/parserbase.cpp index 6812e2c5b..cfe947b89 100644 --- a/src/playlistparsers/parserbase.cpp +++ b/src/playlistparsers/parserbase.cpp @@ -79,7 +79,7 @@ QString ParserBase::MakeUrl(const QString& filename_or_url) const { return QUrl::fromLocalFile(filename_or_url).toString(); } -Song ParserBase::LoadLibrarySong(const QString& filename_or_url) const { +Song ParserBase::LoadLibrarySong(const QString& filename_or_url, int beginning) const { if (!library_) return Song(); @@ -93,6 +93,7 @@ Song ParserBase::LoadLibrarySong(const QString& filename_or_url) const { LibraryQuery query; query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec); query.AddWhere("filename", info.canonicalFilePath()); + query.AddWhere("beginning", beginning); Song song; if (library_->ExecQuery(&query) && query.Next()) { @@ -100,3 +101,7 @@ Song ParserBase::LoadLibrarySong(const QString& filename_or_url) const { } return song; } + +Song ParserBase::LoadLibrarySong(const QString& filename_or_url) const { + return LoadLibrarySong(filename_or_url, 0); +} diff --git a/src/playlistparsers/parserbase.h b/src/playlistparsers/parserbase.h index e04a3a06b..c112c84fb 100644 --- a/src/playlistparsers/parserbase.h +++ b/src/playlistparsers/parserbase.h @@ -57,6 +57,8 @@ protected: // a song with that path. If one is found, returns it, otherwise returns an // invalid song. Song LoadLibrarySong(const QString& filename_or_url) const; + // Just like the method above, but looks for a SECTION of a song. + Song LoadLibrarySong(const QString& filename_or_url, int beginning) const; private: LibraryBackendInterface* library_;