library: watcher now updates CUE associated songs + ignores the not-CUE-associated songs in a CUE sheet

CueParser: properly update length for the last song from every FILE in multifile CUE sheet
new 'playlist_path' argument in the 'Load' part of PlaylistParser's API
This commit is contained in:
Paweł Bara 2011-01-11 23:09:59 +00:00
parent bc573d55f1
commit 4f7e804638
28 changed files with 145 additions and 81 deletions

View File

@ -155,7 +155,7 @@ SongLoader::Result SongLoader::LoadLocal(const QString& filename, bool block,
QFile cue(matching_cue);
cue.open(QIODevice::ReadOnly);
song_list = cue_parser_->Load(&cue, QDir(filename.section('/', 0, -2)));
song_list = cue_parser_->Load(&cue, matching_cue, QDir(filename.section('/', 0, -2)));
// it's a normal media file
} else {
@ -183,7 +183,7 @@ void SongLoader::LoadPlaylistAndEmit(ParserBase* parser, const QString& filename
void SongLoader::LoadPlaylist(ParserBase* parser, const QString& filename) {
QFile file(filename);
file.open(QIODevice::ReadOnly);
songs_ = parser->Load(&file, QFileInfo(filename).path());
songs_ = parser->Load(&file, filename, QFileInfo(filename).path());
}
static bool CompareSongs(const Song& left, const Song& right) {

View File

@ -561,6 +561,19 @@ SongList LibraryBackend::GetSongsById(const QStringList& ids, QSqlDatabase& db)
return ret;
}
Song LibraryBackend::GetSongByFilename(const QString& filename, int beginning) {
LibraryQuery query;
query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
query.AddWhere("filename", filename);
query.AddWhere("beginning", beginning);
Song song;
if (ExecQuery(&query) && query.Next()) {
song.InitFromQuery(query);
}
return song;
}
bool LibraryBackend::HasCompilations(const QueryOptions& opt) {
LibraryQuery query(opt);
query.SetColumnSpec("%songs_table.ROWID");

View File

@ -82,6 +82,11 @@ class LibraryBackendInterface : public QObject {
virtual Song GetSongById(int id) = 0;
// Returns a section of a song with the given filename and beginning. If the section
// is not present in library, returns invalid song.
// Using default beginning value is suitable when searching for single-section songs.
virtual Song GetSongByFilename(const QString& filename, int beginning = 0) = 0;
virtual void AddDirectory(const QString& path) = 0;
virtual void RemoveDirectory(const Directory& dir) = 0;
@ -135,6 +140,8 @@ class LibraryBackend : public LibraryBackendInterface {
SongList GetSongsByForeignId(const QStringList& ids, const QString& table,
const QString& column);
Song GetSongByFilename(const QString& filename, int beginning = 0);
void AddDirectory(const QString& path);
void RemoveDirectory(const Directory& dir);

View File

@ -21,6 +21,7 @@
#include "playlistparsers/cueparser.h"
#include <QFileSystemWatcher>
#include <QDateTime>
#include <QDirIterator>
#include <QtDebug>
#include <QThread>
@ -32,9 +33,6 @@
#include <taglib/fileref.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;
const char* LibraryWatcher::kSettingsGroup = "LibraryWatcher";
@ -288,6 +286,13 @@ void LibraryWatcher::ScanSubdirectory(
foreach (const QString& file, files_on_disk) {
if (stop_requested_) return;
QString matching_cue = NoExtensionPart(file) + ".cue";
QDateTime cue_last_modified = QFileInfo(matching_cue).lastModified();
uint cue_mtime = cue_last_modified.isValid()
? cue_last_modified.toTime_t()
: 0;
Song matching_song;
if (FindSongByPath(songs_in_db, file, &matching_song)) {
// The song is in the database and still on disk.
@ -301,7 +306,8 @@ void LibraryWatcher::ScanSubdirectory(
continue;
}
bool changed = matching_song.mtime() != file_info.lastModified().toTime_t();
// watch out for cue songs which have their mtime equal to qMax(media_file_mtime, cue_sheet_mtime)
bool changed = matching_song.mtime() != qMax(file_info.lastModified().toTime_t(), cue_mtime);
// Also want to look to see whether the album art has changed
QString image = ImageForSong(file, album_art);
@ -310,59 +316,69 @@ void LibraryWatcher::ScanSubdirectory(
changed = true;
}
// TODO: cues
// the song's changed - reread the metadata from file
// TODO: problem if cue gets deleted or added before an update
if (changed) {
qDebug() << file << "changed";
// It's changed - reread the metadata from the file
Song song_on_disk;
song_on_disk.InitFromFile(file, t->dir());
if (!song_on_disk.is_valid())
continue;
song_on_disk.set_id(matching_song.id());
song_on_disk.set_art_automatic(image);
// cue associated?
if(cue_mtime) {
QFile cue(matching_cue);
cue.open(QIODevice::ReadOnly);
// Preserve user-settable fields
song_on_disk.set_playcount(matching_song.playcount());
song_on_disk.set_skipcount(matching_song.skipcount());
song_on_disk.set_lastplayed(matching_song.lastplayed());
song_on_disk.set_rating(matching_song.rating());
song_on_disk.set_score(matching_song.score());
song_on_disk.set_art_manual(matching_song.art_manual());
if (!matching_song.IsMetadataEqual(song_on_disk)) {
qDebug() << file << "metadata changed";
// Update the song in the DB
t->new_songs << song_on_disk;
foreach(Song cue_song, cue_parser_->Load(&cue, matching_cue, path)) {
// update every song that's in the cue and library
Song matching_section = backend_->GetSongByFilename(cue_song.filename(), cue_song.beginning());
if(matching_section.is_valid()) {
cue_song.set_directory_id(t->dir());
PreserveUserSetData(file, image, matching_section, &cue_song, t);
}
}
} else {
// Only the mtimes changed
t->touched_songs << song_on_disk;
Song song_on_disk;
song_on_disk.InitFromFile(file, t->dir());
if (!song_on_disk.is_valid())
continue;
PreserveUserSetData(file, image, matching_song, &song_on_disk, t);
}
}
} else {
// The song is on disk but not in the DB
SongList song_list;
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)) {
if(cue_mtime) {
QFile cue(matching_cue);
cue.open(QIODevice::ReadOnly);
song_list = cue_parser_->Load(&cue, path);
// ignore FILEs pointing to other media files
foreach(const Song& cue_song, cue_parser_->Load(&cue, matching_cue, path)) {
if(cue_song.filename() == file) {
song_list << cue_song;
}
}
if(song_list.isEmpty()) {
continue;
}
cues_processed << matching_cue;
// it's a normal media file
} else {
Song song;
song.InitFromFile(file, -1);
if (!song.is_valid())
if (!song.is_valid()) {
continue;
}
song_list << song;
}
@ -410,6 +426,29 @@ void LibraryWatcher::ScanSubdirectory(
}
}
void LibraryWatcher::PreserveUserSetData(const QString& file, const QString& image,
const Song& matching_song, Song* out, ScanTransaction* t) {
out->set_id(matching_song.id());
out->set_art_automatic(image);
out->set_playcount(matching_song.playcount());
out->set_skipcount(matching_song.skipcount());
out->set_lastplayed(matching_song.lastplayed());
out->set_rating(matching_song.rating());
out->set_score(matching_song.score());
out->set_art_manual(matching_song.art_manual());
if (!matching_song.IsMetadataEqual(*out)) {
qDebug() << file << "metadata changed";
// Update the song in the DB
t->new_songs << *out;
} else {
// Only the mtime's changed
t->touched_songs << *out;
}
}
void LibraryWatcher::AddWatch(QFileSystemWatcher* w, const QString& path) {
if (!QFile::exists(path))
return;

View File

@ -133,6 +133,8 @@ class LibraryWatcher : public QObject {
QString PickBestImage(const QStringList& images);
QString ImageForSong(const QString& path, QMap<QString, QStringList>& album_art);
void AddWatch(QFileSystemWatcher* w, const QString& path);
void PreserveUserSetData(const QString& file, const QString& image,
const Song& matching_song, Song* out, ScanTransaction* t);
private:
// One of these gets stored for each Directory we're watching

View File

@ -29,7 +29,7 @@ bool AsxIniParser::TryMagic(const QByteArray &data) const {
return data.toLower().contains("[reference]");
}
SongList AsxIniParser::Load(QIODevice *device, const QDir &dir) const {
SongList AsxIniParser::Load(QIODevice *device, const QString& playlist_path, const QDir &dir) const {
SongList ret;
while (!device->atEnd()) {

View File

@ -31,7 +31,7 @@ public:
bool TryMagic(const QByteArray &data) const;
SongList Load(QIODevice *device, const QDir &dir = QDir()) const;
SongList Load(QIODevice *device, const QString& playlist_path = "", const QDir &dir = QDir()) const;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir()) const;
};

View File

@ -31,7 +31,7 @@ ASXParser::ASXParser(LibraryBackendInterface* library, QObject* parent)
{
}
SongList ASXParser::Load(QIODevice *device, const QDir&) const {
SongList ASXParser::Load(QIODevice *device, const QString& playlist_path, const QDir&) const {
// We have to load everything first so we can munge the "XML".
QByteArray data = device->readAll();

View File

@ -31,7 +31,7 @@ class ASXParser : public XMLParser {
bool TryMagic(const QByteArray &data) const;
SongList Load(QIODevice *device, const QDir &dir = QDir()) const;
SongList Load(QIODevice *device, const QString& playlist_path = "", const QDir &dir = QDir()) const;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir()) const;
private:

View File

@ -18,6 +18,8 @@
#include "cueparser.h"
#include <QBuffer>
#include <QDateTime>
#include <QFileInfo>
#include <QStringBuilder>
#include <QRegExp>
#include <QTextCodec>
@ -40,7 +42,7 @@ CueParser::CueParser(LibraryBackendInterface* library, QObject* parent)
{
}
SongList CueParser::Load(QIODevice* device, const QDir& dir) const {
SongList CueParser::Load(QIODevice* device, const QString& playlist_path, const QDir& dir) const {
SongList ret;
QTextStream text_stream(device);
@ -198,6 +200,8 @@ SongList CueParser::Load(QIODevice* device, const QDir& dir) const {
}
}
QDateTime cue_mtime = QFileInfo(playlist_path).lastModified();
// finalize parsing songs
for(int i = 0; i < entries.length(); i++) {
CueEntry entry = entries.at(i);
@ -212,6 +216,11 @@ SongList CueParser::Load(QIODevice* device, const QDir& dir) const {
song.InitFromFile(current.filename(), -1);
}
// cue song has mtime equal to qMax(media_file_mtime, cue_sheet_mtime)
if(cue_mtime.isValid()) {
song.set_mtime(qMax(cue_mtime.toTime_t(), song.mtime()));
}
// overwrite the stuff, we may have read from the file or library, using
// the current .cue metadata
@ -220,7 +229,9 @@ SongList CueParser::Load(QIODevice* device, const QDir& dir) const {
song.set_track(i + 1);
}
if(i + 1 < entries.size()) {
// the last TRACK for every FILE gets it's 'end' marker from the media file's
// length
if(i + 1 < entries.size() && entries.at(i).file == entries.at(i + 1).file) {
// incorrect indices?
if(!UpdateSong(entry, entries.at(i + 1).index, &song)) {
continue;

View File

@ -48,7 +48,7 @@ class CueParser : public ParserBase {
bool TryMagic(const QByteArray& data) const;
SongList Load(QIODevice* device, const QDir& dir = QDir()) const;
SongList Load(QIODevice* device, const QString& playlist_path = "", const QDir& dir = QDir()) const;
void Save(const SongList& songs, QIODevice* device, const QDir& dir = QDir()) const;
private:

View File

@ -25,7 +25,7 @@ M3UParser::M3UParser(LibraryBackendInterface* library, QObject* parent)
{
}
SongList M3UParser::Load(QIODevice* device, const QDir& dir) const {
SongList M3UParser::Load(QIODevice* device, const QString& playlist_path, const QDir& dir) const {
SongList ret;
M3UType type = STANDARD;

View File

@ -36,7 +36,7 @@ class M3UParser : public ParserBase {
bool TryMagic(const QByteArray &data) const;
SongList Load(QIODevice* device, const QDir& dir = QDir()) const;
SongList Load(QIODevice* device, const QString& playlist_path = "", const QDir& dir = QDir()) const;
void Save(const SongList &songs, QIODevice* device, const QDir& dir = QDir()) const;
private:

View File

@ -90,18 +90,5 @@ Song ParserBase::LoadLibrarySong(const QString& filename_or_url, int beginning)
else
info.setFile(filename_or_url);
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()) {
song.InitFromQuery(query);
}
return song;
}
Song ParserBase::LoadLibrarySong(const QString& filename_or_url) const {
return LoadLibrarySong(filename_or_url, 0);
return library_->GetSongByFilename(info.canonicalFilePath(), beginning);
}

View File

@ -37,7 +37,7 @@ public:
virtual bool TryMagic(const QByteArray& data) const = 0;
virtual SongList Load(QIODevice* device, const QDir& dir = QDir()) const = 0;
virtual SongList Load(QIODevice* device, const QString& playlist_path = "", const QDir& dir = QDir()) const = 0;
virtual void Save(const SongList& songs, QIODevice* device, const QDir& dir = QDir()) const = 0;
protected:
@ -54,11 +54,9 @@ protected:
QString MakeUrl(const QString& filename_or_url) const;
// Converts the URL or path to a canonical path and searches the library for
// 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;
// a section of a song with that path and the given beginning. If one is found,
// returns it, otherwise returns an invalid song.
Song LoadLibrarySong(const QString& filename_or_url, int beginning = 0) const;
private:
LibraryBackendInterface* library_;

View File

@ -99,7 +99,7 @@ ParserBase* PlaylistParser::MaybeGetParserForMagic(const QByteArray& data,
return NULL;
}
SongList PlaylistParser::Load(const QString &filename, ParserBase* p) const {
SongList PlaylistParser::Load(const QString &filename, const QString& playlist_path, ParserBase* p) const {
QFileInfo info(filename);
// Find a parser that supports this file extension
@ -113,7 +113,7 @@ SongList PlaylistParser::Load(const QString &filename, ParserBase* p) const {
QFile file(filename);
file.open(QIODevice::ReadOnly);
return parser->Load(&file, info.absolutePath());
return parser->Load(&file, playlist_path, info.absolutePath());
}
void PlaylistParser::Save(const SongList &songs, const QString &filename) const {

View File

@ -43,7 +43,7 @@ public:
const QString& mime_type = QString()) const;
ParserBase* ParserForExtension(const QString& suffix) const;
SongList Load(const QString& filename, ParserBase* parser = 0) const;
SongList Load(const QString& filename, const QString& playlist_path = "", ParserBase* parser = 0) const;
void Save(const SongList& songs, const QString& filename) const;
private:

View File

@ -25,7 +25,7 @@ PLSParser::PLSParser(LibraryBackendInterface* library, QObject* parent)
{
}
SongList PLSParser::Load(QIODevice *device, const QDir &dir) const {
SongList PLSParser::Load(QIODevice *device, const QString& playlist_path, const QDir &dir) const {
QMap<int, Song> songs;
QRegExp n_re("\\d+$");

View File

@ -31,7 +31,7 @@ public:
bool TryMagic(const QByteArray &data) const;
SongList Load(QIODevice* device, const QDir& dir = QDir()) const;
SongList Load(QIODevice* device, const QString& playlist_path = "", const QDir& dir = QDir()) const;
void Save(const SongList& songs, QIODevice* device, const QDir& dir = QDir()) const;
};

View File

@ -29,7 +29,7 @@ XSPFParser::XSPFParser(LibraryBackendInterface* library, QObject* parent)
{
}
SongList XSPFParser::Load(QIODevice *device, const QDir&) const {
SongList XSPFParser::Load(QIODevice *device, const QString& playlist_path, const QDir&) const {
SongList ret;
QXmlStreamReader reader(device);

View File

@ -36,7 +36,7 @@ class XSPFParser : public XMLParser {
bool TryMagic(const QByteArray &data) const;
SongList Load(QIODevice *device, const QDir &dir = QDir()) const;
SongList Load(QIODevice *device, const QString& playlist_path = "", const QDir &dir = QDir()) const;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir()) const;
private:

View File

@ -56,6 +56,8 @@ public:
SongList GetSongsByForeignId(const QStringList& ids, const QString& table,
const QString& column);
Song GetSongByFilename(const QString& filename, int beginning);
void AddDirectory(const QString& path);
void RemoveDirectory(const Directory& dir);

View File

@ -35,7 +35,7 @@ TEST_F(AsxIniParserTest, ParsesBasicTrackList) {
QFile file(":/testdata/test.asxini");
file.open(QIODevice::ReadOnly);
SongList songs = parser_.Load(&file, QDir());
SongList songs = parser_.Load(&file, "", QDir());
ASSERT_EQ(2, songs.length());
EXPECT_EQ("http://195.245.168.21/antena3?MSWMExt=.asf", songs[0].filename());
EXPECT_EQ("http://195.245.168.21:80/antena3?MSWMExt=.asf", songs[1].filename());

View File

@ -41,7 +41,7 @@ TEST_F(CueParserTest, ParsesASong) {
QFile file(":testdata/onesong.cue");
file.open(QIODevice::ReadOnly);
SongList song_list = parser_.Load(&file, QDir(""));
SongList song_list = parser_.Load(&file, "", QDir(""));
// one song
ASSERT_EQ(1, song_list.size());
@ -60,7 +60,7 @@ TEST_F(CueParserTest, ParsesTwoSongs) {
QFile file(":testdata/twosongs.cue");
file.open(QIODevice::ReadOnly);
SongList song_list = parser_.Load(&file, QDir(""));
SongList song_list = parser_.Load(&file, "", QDir(""));
// two songs
ASSERT_EQ(2, song_list.size());
@ -89,7 +89,7 @@ TEST_F(CueParserTest, SkipsBrokenSongs) {
QFile file(":testdata/brokensong.cue");
file.open(QIODevice::ReadOnly);
SongList song_list = parser_.Load(&file, QDir(""));
SongList song_list = parser_.Load(&file, "", QDir(""));
// two songs (the broken one is not in the list)
ASSERT_EQ(2, song_list.size());
@ -120,7 +120,7 @@ TEST_F(CueParserTest, UsesAllMetadataInformation) {
QFile file(":testdata/fullmetadata.cue");
file.open(QIODevice::ReadOnly);
SongList song_list = parser_.Load(&file, QDir(""));
SongList song_list = parser_.Load(&file, "", QDir(""));
// two songs
ASSERT_EQ(2, song_list.size());
@ -153,7 +153,7 @@ TEST_F(CueParserTest, AcceptsMultipleFileBasedCues) {
QFile file(":testdata/manyfiles.cue");
file.open(QIODevice::ReadOnly);
SongList song_list = parser_.Load(&file, QDir(""));
SongList song_list = parser_.Load(&file, "", QDir(""));
// five songs
ASSERT_EQ(5, song_list.size());
@ -212,7 +212,7 @@ TEST_F(CueParserTest, SkipsBrokenSongsInMultipleFileBasedCues) {
QFile file(":testdata/manyfilesbroken.cue");
file.open(QIODevice::ReadOnly);
SongList song_list = parser_.Load(&file, QDir(""));
SongList song_list = parser_.Load(&file, "", QDir(""));
// four songs
ASSERT_EQ(4, song_list.size());
@ -262,7 +262,7 @@ TEST_F(CueParserTest, SkipsDataFiles) {
QFile file(":testdata/withdatafiles.cue");
file.open(QIODevice::ReadOnly);
SongList song_list = parser_.Load(&file, QDir(""));
SongList song_list = parser_.Load(&file, "", QDir(""));
// two songs
ASSERT_EQ(2, song_list.size());

View File

@ -112,7 +112,7 @@ TEST_F(M3UParserTest, ParsesNonExtendedM3U) {
QBuffer buffer(&data);
buffer.open(QIODevice::ReadOnly);
M3UParser parser(NULL);
SongList songs = parser.Load(&buffer, QDir("somedir"));
SongList songs = parser.Load(&buffer, "", QDir("somedir"));
ASSERT_EQ(2, songs.size());
EXPECT_PRED_FORMAT2(::testing::IsSubstring,
"http://foo.com/bar/somefile.mp3", songs[0].filename().toStdString());
@ -127,7 +127,7 @@ TEST_F(M3UParserTest, ParsesActualM3U) {
QFile file(":testdata/test.m3u");
file.open(QIODevice::ReadOnly);
M3UParser parser(NULL);
SongList songs = parser.Load(&file, QDir("somedir"));
SongList songs = parser.Load(&file, "", QDir("somedir"));
ASSERT_EQ(239, songs.size());
EXPECT_EQ("gravity", songs[0].title());
EXPECT_EQ(203, songs[0].length());

View File

@ -51,6 +51,8 @@ class MockLibraryBackend : public LibraryBackendInterface {
MOCK_METHOD1(GetSongById, Song(int));
MOCK_METHOD2(GetSongByFilename, Song(const QString&, int));
MOCK_METHOD1(AddDirectory, void(const QString&));
MOCK_METHOD1(RemoveDirectory, void(const Directory&));

View File

@ -46,7 +46,7 @@ protected:
TEST_F(PLSParserTest, ParseOneTrack) {
shared_ptr<QFile> file(Open("pls_one.pls"));
SongList songs = parser_.Load(file.get(), QDir("/relative/to/"));
SongList songs = parser_.Load(file.get(), "", QDir("/relative/to/"));
ASSERT_EQ(1, songs.length());
EXPECT_EQ("/relative/to/filename with spaces.mp3", songs[0].filename());
EXPECT_EQ("Title", songs[0].title());
@ -103,7 +103,7 @@ TEST_F(PLSParserTest, SaveAndLoad) {
parser_.Save(songs, &temp);
temp.seek(0);
songs = parser_.Load(&temp, QDir("/meep"));
songs = parser_.Load(&temp, "", QDir("/meep"));
ASSERT_EQ(2, songs.count());
EXPECT_EQ(one.filename(), songs[0].filename());

View File

@ -53,6 +53,9 @@ protected:
library_.reset(new MockLibraryBackend);
loader_.reset(new SongLoader(library_.get()));
loader_->set_timeout(20000);
// the thing we return is not really important
EXPECT_CALL(*library_.get(), GetSongByFilename(_, _)).WillRepeatedly(Return(Song()));
}
void LoadLocalDirectory(const QString& dir);