consider .cues when scanning the library
persist the new 'beginning' marker of songs fixed a bug where %allsongstables script would not update the attached databases
This commit is contained in:
parent
6fbe173d80
commit
da5f941333
|
@ -293,5 +293,6 @@
|
||||||
<file>schema/schema-22.sql</file>
|
<file>schema/schema-22.sql</file>
|
||||||
<file>schema/jamendo.sql</file>
|
<file>schema/jamendo.sql</file>
|
||||||
<file>schema/schema-23.sql</file>
|
<file>schema/schema-23.sql</file>
|
||||||
|
<file>schema/schema-24.sql</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
|
@ -35,7 +35,8 @@ CREATE TABLE jamendo.songs (
|
||||||
forced_compilation_off INTEGER NOT NULL DEFAULT 0,
|
forced_compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||||
effective_compilation NOT NULL DEFAULT 0,
|
effective_compilation NOT NULL DEFAULT 0,
|
||||||
skipcount 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(
|
CREATE VIRTUAL TABLE jamendo.songs_fts USING fts3(
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE %allsongstables ADD COLUMN beginning INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
UPDATE schema_version SET version=24;
|
|
@ -31,7 +31,7 @@
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
const char* Database::kDatabaseFilename = "clementine.db";
|
const char* Database::kDatabaseFilename = "clementine.db";
|
||||||
const int Database::kSchemaVersion = 23;
|
const int Database::kSchemaVersion = 24;
|
||||||
const char* Database::kMagicAllSongsTables = "%allsongstables";
|
const char* Database::kMagicAllSongsTables = "%allsongstables";
|
||||||
|
|
||||||
int Database::sNextConnectionId = 1;
|
int Database::sNextConnectionId = 1;
|
||||||
|
@ -536,10 +536,25 @@ void Database::ExecCommands(const QString &schema, QSqlDatabase &db) {
|
||||||
|
|
||||||
QStringList Database::SongsTables(QSqlDatabase& db) const {
|
QStringList Database::SongsTables(QSqlDatabase& db) const {
|
||||||
QStringList ret;
|
QStringList ret;
|
||||||
|
|
||||||
|
// look for the tables in the main db
|
||||||
foreach (const QString& table, db.tables()) {
|
foreach (const QString& table, db.tables()) {
|
||||||
if (table == "songs" || table.endsWith("_songs"))
|
if (table == "songs" || table.endsWith("_songs"))
|
||||||
ret << table;
|
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;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,7 @@ const QStringList Song::kColumns = QStringList()
|
||||||
<< "mtime" << "ctime" << "filesize" << "sampler" << "art_automatic"
|
<< "mtime" << "ctime" << "filesize" << "sampler" << "art_automatic"
|
||||||
<< "art_manual" << "filetype" << "playcount" << "lastplayed" << "rating"
|
<< "art_manual" << "filetype" << "playcount" << "lastplayed" << "rating"
|
||||||
<< "forced_compilation_on" << "forced_compilation_off"
|
<< "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::kColumnSpec = Song::kColumns.join(", ");
|
||||||
const QString Song::kBindSpec = Prepend(":", 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->comment_ = tostr(col + 11);
|
||||||
d->compilation_ = q.value(col + 12).toBool();
|
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->bitrate_ = toint(col + 14);
|
||||||
d->samplerate_ = toint(col + 15);
|
d->samplerate_ = toint(col + 15);
|
||||||
|
|
||||||
|
@ -494,9 +492,12 @@ void Song::InitFromQuery(const SqlRow& q, int col) {
|
||||||
// effective_compilation = 30
|
// effective_compilation = 30
|
||||||
|
|
||||||
d->skipcount_ = q.value(col + 31).isNull() ? 0 : q.value(col + 31).toInt();
|
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();
|
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 tostr
|
||||||
#undef toint
|
#undef toint
|
||||||
#undef tofloat
|
#undef tofloat
|
||||||
|
@ -926,7 +927,7 @@ void Song::BindToQuery(QSqlQuery *query) const {
|
||||||
query->bindValue(":comment", strval(d->comment_));
|
query->bindValue(":comment", strval(d->comment_));
|
||||||
query->bindValue(":compilation", d->compilation_ ? 1 : 0);
|
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(":length", intval(length()));
|
||||||
query->bindValue(":bitrate", intval(d->bitrate_));
|
query->bindValue(":bitrate", intval(d->bitrate_));
|
||||||
query->bindValue(":samplerate", intval(d->samplerate_));
|
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(":effective_compilation", is_compilation() ? 1 : 0);
|
||||||
|
|
||||||
query->bindValue(":skipcount", d->skipcount_);
|
query->bindValue(":skipcount", d->skipcount_);
|
||||||
|
|
||||||
query->bindValue(":score", d->score_);
|
query->bindValue(":score", d->score_);
|
||||||
|
|
||||||
|
query->bindValue(":beginning", d->beginning_);
|
||||||
|
|
||||||
#undef intval
|
#undef intval
|
||||||
#undef notnullintval
|
#undef notnullintval
|
||||||
}
|
}
|
||||||
|
@ -1042,7 +1044,8 @@ bool Song::IsMetadataEqual(const Song& other) const {
|
||||||
d->genre_ == other.d->genre_ &&
|
d->genre_ == other.d->genre_ &&
|
||||||
d->comment_ == other.d->comment_ &&
|
d->comment_ == other.d->comment_ &&
|
||||||
d->compilation_ == other.d->compilation_ &&
|
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() &&
|
length() == other.length() &&
|
||||||
d->bitrate_ == other.d->bitrate_ &&
|
d->bitrate_ == other.d->bitrate_ &&
|
||||||
d->samplerate_ == other.d->samplerate_ &&
|
d->samplerate_ == other.d->samplerate_ &&
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
#include "librarywatcher.h"
|
#include "librarywatcher.h"
|
||||||
#include "librarybackend.h"
|
#include "librarybackend.h"
|
||||||
#include "core/taskmanager.h"
|
#include "core/taskmanager.h"
|
||||||
|
#include "playlistparsers/cueparser.h"
|
||||||
|
|
||||||
#include <QFileSystemWatcher>
|
#include <QFileSystemWatcher>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
|
@ -26,12 +27,15 @@
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
#include <QSet>
|
||||||
|
|
||||||
#include <taglib/fileref.h>
|
#include <taglib/fileref.h>
|
||||||
#include <taglib/tag.h>
|
#include <taglib/tag.h>
|
||||||
|
|
||||||
|
// TODO: test removing a folder with cues
|
||||||
|
// TODO: what about .cue vs it's media file changes?
|
||||||
|
|
||||||
QStringList LibraryWatcher::sValidImages;
|
QStringList LibraryWatcher::sValidImages;
|
||||||
QStringList LibraryWatcher::sValidPlaylists;
|
|
||||||
|
|
||||||
const char* LibraryWatcher::kSettingsGroup = "LibraryWatcher";
|
const char* LibraryWatcher::kSettingsGroup = "LibraryWatcher";
|
||||||
|
|
||||||
|
@ -44,14 +48,14 @@ LibraryWatcher::LibraryWatcher(QObject* parent)
|
||||||
monitor_(true),
|
monitor_(true),
|
||||||
rescan_timer_(new QTimer(this)),
|
rescan_timer_(new QTimer(this)),
|
||||||
rescan_paused_(false),
|
rescan_paused_(false),
|
||||||
total_watches_(0)
|
total_watches_(0),
|
||||||
|
cue_parser_(new CueParser(backend_, this))
|
||||||
{
|
{
|
||||||
rescan_timer_->setInterval(1000);
|
rescan_timer_->setInterval(1000);
|
||||||
rescan_timer_->setSingleShot(true);
|
rescan_timer_->setSingleShot(true);
|
||||||
|
|
||||||
if (sValidImages.isEmpty()) {
|
if (sValidImages.isEmpty()) {
|
||||||
sValidImages << "jpg" << "png" << "gif" << "jpeg";
|
sValidImages << "jpg" << "png" << "gif" << "jpeg";
|
||||||
sValidPlaylists << "m3u" << "pls";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ReloadSettings();
|
ReloadSettings();
|
||||||
|
@ -278,6 +282,8 @@ void LibraryWatcher::ScanSubdirectory(
|
||||||
// Ask the database for a list of files in this directory
|
// Ask the database for a list of files in this directory
|
||||||
SongList songs_in_db = t->FindSongsInSubdirectory(path);
|
SongList songs_in_db = t->FindSongsInSubdirectory(path);
|
||||||
|
|
||||||
|
QSet<QString> cues_processed;
|
||||||
|
|
||||||
// Now compare the list from the database with the list of files on disk
|
// Now compare the list from the database with the list of files on disk
|
||||||
foreach (const QString& file, files_on_disk) {
|
foreach (const QString& file, files_on_disk) {
|
||||||
if (stop_requested_) return;
|
if (stop_requested_) return;
|
||||||
|
@ -304,6 +310,7 @@ void LibraryWatcher::ScanSubdirectory(
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: cues
|
||||||
if (changed) {
|
if (changed) {
|
||||||
qDebug() << file << "changed";
|
qDebug() << file << "changed";
|
||||||
|
|
||||||
|
@ -334,18 +341,42 @@ void LibraryWatcher::ScanSubdirectory(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// The song is on disk but not in the DB
|
// The song is on disk but not in the DB
|
||||||
|
SongList song_list;
|
||||||
|
|
||||||
Song song;
|
QString matching_cue = NoExtensionPart(file) + ".cue";
|
||||||
song.InitFromFile(file, t->dir());
|
|
||||||
if (!song.is_valid())
|
// don't process the same cue many times
|
||||||
|
if(cues_processed.contains(matching_cue))
|
||||||
continue;
|
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";
|
qDebug() << file << "created";
|
||||||
|
// choose an image for the song(s)
|
||||||
|
QString image = ImageForSong(file, album_art);
|
||||||
|
|
||||||
// Choose an image for the song
|
foreach (Song song, song_list) {
|
||||||
if (song.art_automatic().isEmpty())
|
song.set_directory_id(t->dir());
|
||||||
song.set_art_automatic(ImageForSong(file, album_art));
|
if (song.art_automatic().isEmpty())
|
||||||
|
song.set_art_automatic(image);
|
||||||
|
|
||||||
t->new_songs << song;
|
t->new_songs << song;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
class QFileSystemWatcher;
|
class QFileSystemWatcher;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
|
|
||||||
|
class CueParser;
|
||||||
class LibraryBackend;
|
class LibraryBackend;
|
||||||
class TaskManager;
|
class TaskManager;
|
||||||
|
|
||||||
|
@ -126,6 +127,7 @@ class LibraryWatcher : public QObject {
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static bool FindSongByPath(const SongList& list, const QString& path, Song* out);
|
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 ExtensionPart( const QString &fileName );
|
||||||
inline static QString DirectoryPart( const QString &fileName );
|
inline static QString DirectoryPart( const QString &fileName );
|
||||||
QString PickBestImage(const QStringList& images);
|
QString PickBestImage(const QStringList& images);
|
||||||
|
@ -161,14 +163,18 @@ class LibraryWatcher : public QObject {
|
||||||
|
|
||||||
int total_watches_;
|
int total_watches_;
|
||||||
|
|
||||||
|
CueParser* cue_parser_;
|
||||||
|
|
||||||
static QStringList sValidImages;
|
static QStringList sValidImages;
|
||||||
static QStringList sValidPlaylists;
|
|
||||||
|
|
||||||
#ifdef Q_OS_DARWIN
|
#ifdef Q_OS_DARWIN
|
||||||
static const int kMaxWatches = 100;
|
static const int kMaxWatches = 100;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inline QString LibraryWatcher::NoExtensionPart( const QString &fileName ) {
|
||||||
|
return fileName.contains( '.' ) ? fileName.section( '.', 0, -2 ) : "";
|
||||||
|
}
|
||||||
// Thanks Amarok
|
// Thanks Amarok
|
||||||
inline QString LibraryWatcher::ExtensionPart( const QString &fileName ) {
|
inline QString LibraryWatcher::ExtensionPart( const QString &fileName ) {
|
||||||
return fileName.contains( '.' ) ? fileName.mid( fileName.lastIndexOf('.') + 1 ).toLower() : "";
|
return fileName.contains( '.' ) ? fileName.mid( fileName.lastIndexOf('.') + 1 ).toLower() : "";
|
||||||
|
|
|
@ -93,19 +93,21 @@ QFuture<PlaylistItemPtr> PlaylistBackend::GetPlaylistItems(int playlist) {
|
||||||
QMutexLocker l(db_->Mutex());
|
QMutexLocker l(db_->Mutex());
|
||||||
QSqlDatabase db(db_->Connect());
|
QSqlDatabase db(db_->Connect());
|
||||||
|
|
||||||
QSqlQuery q("SELECT songs.ROWID, " + Song::JoinSpec("songs") + ","
|
QString query = "SELECT songs.ROWID, " + Song::JoinSpec("songs") + ","
|
||||||
" magnatune_songs.ROWID, " + Song::JoinSpec("magnatune_songs") + ","
|
" magnatune_songs.ROWID, " + Song::JoinSpec("magnatune_songs") + ","
|
||||||
" jamendo_songs.ROWID, " + Song::JoinSpec("jamendo_songs") + ","
|
" jamendo_songs.ROWID, " + Song::JoinSpec("jamendo_songs") + ","
|
||||||
" p.type, p.url, p.title, p.artist, p.album, p.length,"
|
" p.type, p.url, p.title, p.artist, p.album, p.length,"
|
||||||
" p.radio_service"
|
" p.radio_service"
|
||||||
" FROM playlist_items AS p"
|
" FROM playlist_items AS p"
|
||||||
" LEFT JOIN songs"
|
" LEFT JOIN songs"
|
||||||
" ON p.library_id = songs.ROWID"
|
" ON p.library_id = songs.ROWID"
|
||||||
" LEFT JOIN magnatune_songs"
|
" LEFT JOIN magnatune_songs"
|
||||||
" ON p.library_id = magnatune_songs.ROWID"
|
" ON p.library_id = magnatune_songs.ROWID"
|
||||||
" LEFT JOIN jamendo.songs AS jamendo_songs"
|
" LEFT JOIN jamendo.songs AS jamendo_songs"
|
||||||
" ON p.library_id = jamendo_songs.ROWID"
|
" ON p.library_id = jamendo_songs.ROWID"
|
||||||
" WHERE p.playlist = :playlist", db);
|
" WHERE p.playlist = :playlist";
|
||||||
|
QSqlQuery q(query, db);
|
||||||
|
|
||||||
q.bindValue(":playlist", playlist);
|
q.bindValue(":playlist", playlist);
|
||||||
q.exec();
|
q.exec();
|
||||||
if (db_->CheckErrors(q.lastError()))
|
if (db_->CheckErrors(q.lastError()))
|
||||||
|
|
|
@ -175,13 +175,14 @@ SongList CueParser::Load(QIODevice* device, const QDir& dir) const {
|
||||||
if (!ParseTrackLocation(entry.file, dir, ¤t)) {
|
if (!ParseTrackLocation(entry.file, dir, ¤t)) {
|
||||||
qWarning() << "failed to parse location in .cue file from " << dir_path;
|
qWarning() << "failed to parse location in .cue file from " << dir_path;
|
||||||
} else {
|
} else {
|
||||||
Song song = LoadLibrarySong(current.filename());
|
// look for the section in library
|
||||||
|
Song song = LoadLibrarySong(current.filename(), IndexToMarker(entry.index));
|
||||||
if (!song.is_valid()) {
|
if (!song.is_valid()) {
|
||||||
song.InitFromFile(current.filename(), -1);
|
song.InitFromFile(current.filename(), -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// overwrite the stuff, we may have read from the file, using
|
// overwrite the stuff, we may have read from the file or library, using
|
||||||
// the .cue's metadata
|
// the current .cue metadata
|
||||||
if(i + 1 < entries.size()) {
|
if(i + 1 < entries.size()) {
|
||||||
// incorrect indices?
|
// incorrect indices?
|
||||||
if(!UpdateSong(entry, entries.at(i + 1).index, &song)) {
|
if(!UpdateSong(entry, entries.at(i + 1).index, &song)) {
|
||||||
|
|
|
@ -79,7 +79,7 @@ QString ParserBase::MakeUrl(const QString& filename_or_url) const {
|
||||||
return QUrl::fromLocalFile(filename_or_url).toString();
|
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_)
|
if (!library_)
|
||||||
return Song();
|
return Song();
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ Song ParserBase::LoadLibrarySong(const QString& filename_or_url) const {
|
||||||
LibraryQuery query;
|
LibraryQuery query;
|
||||||
query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
|
query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
|
||||||
query.AddWhere("filename", info.canonicalFilePath());
|
query.AddWhere("filename", info.canonicalFilePath());
|
||||||
|
query.AddWhere("beginning", beginning);
|
||||||
|
|
||||||
Song song;
|
Song song;
|
||||||
if (library_->ExecQuery(&query) && query.Next()) {
|
if (library_->ExecQuery(&query) && query.Next()) {
|
||||||
|
@ -100,3 +101,7 @@ Song ParserBase::LoadLibrarySong(const QString& filename_or_url) const {
|
||||||
}
|
}
|
||||||
return song;
|
return song;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Song ParserBase::LoadLibrarySong(const QString& filename_or_url) const {
|
||||||
|
return LoadLibrarySong(filename_or_url, 0);
|
||||||
|
}
|
||||||
|
|
|
@ -57,6 +57,8 @@ protected:
|
||||||
// a song with that path. If one is found, returns it, otherwise returns an
|
// a song with that path. If one is found, returns it, otherwise returns an
|
||||||
// invalid song.
|
// invalid song.
|
||||||
Song LoadLibrarySong(const QString& filename_or_url) const;
|
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:
|
private:
|
||||||
LibraryBackendInterface* library_;
|
LibraryBackendInterface* library_;
|
||||||
|
|
Loading…
Reference in New Issue