Store the playlist in the database. This is still quite slow.

This commit is contained in:
David Sansome 2010-04-14 21:03:00 +00:00
parent dc782cfa5b
commit af07b5becd
25 changed files with 518 additions and 214 deletions

View File

@ -78,5 +78,6 @@
<file>schema-6.sql</file>
<file>list-add.png</file>
<file>document-save.png</file>
<file>schema-7.sql</file>
</qresource>
</RCC>

26
data/schema-7.sql Normal file
View File

@ -0,0 +1,26 @@
CREATE TABLE playlists (
name TEXT NOT NULL
);
CREATE TABLE playlist_items (
playlist INTEGER NOT NULL,
type TEXT NOT NULL, /* Library, Stream, File, or Radio */
/* Library */
library_id INTEGER,
/* Stream, File or Radio */
url TEXT,
/* Stream or Radio */
title TEXT,
artist TEXT,
album TEXT,
length INTEGER,
/* Radio */
radio_service TEXT
);
UPDATE schema_version SET version=7;

View File

@ -76,6 +76,8 @@ set(CLEMENTINE-SOURCES
stickyslider.cpp
commandlineoptions.cpp
settingsprovider.cpp
libraryplaylistitem.cpp
scopedtransaction.cpp
)
# Header files that have Q_OBJECT in

View File

@ -505,7 +505,7 @@ void Library::InitQuery(GroupBy type, LibraryQuery* q) {
q->SetColumnSpec("DISTINCT albumartist");
break;
case GroupBy_None:
q->SetColumnSpec("ROWID, " + QString(Song::kColumnSpec));
q->SetColumnSpec("ROWID, " + Song::kColumnSpec);
break;
}
}

View File

@ -16,6 +16,7 @@
#include "librarybackend.h"
#include "libraryquery.h"
#include "scopedtransaction.h"
#include <QFile>
#include <QDir>
@ -32,7 +33,7 @@
const char* LibraryBackend::kDatabaseName = "clementine.db";
const int LibraryBackend::kSchemaVersion = 6;
const int LibraryBackend::kSchemaVersion = 7;
int (*LibraryBackend::_sqlite3_create_function) (
sqlite3*, const char*, int, int, void*,
@ -359,7 +360,7 @@ void LibraryBackend::RemoveDirectory(const Directory& dir) {
// Remove songs first
DeleteSongs(FindSongsInDirectory(dir.id));
db.transaction();
ScopedTransaction transaction(&db);
// Delete the subdirs that were in this directory
QSqlQuery q("DELETE FROM subdirectories WHERE directory = :id", db);
@ -375,13 +376,13 @@ void LibraryBackend::RemoveDirectory(const Directory& dir) {
emit DirectoryDeleted(dir);
db.commit();
transaction.Commit();
}
SongList LibraryBackend::FindSongsInDirectory(int id) {
QSqlDatabase db(Connect());
QSqlQuery q("SELECT ROWID, " + QString(Song::kColumnSpec) +
QSqlQuery q("SELECT ROWID, " + Song::kColumnSpec +
" FROM songs WHERE directory = :directory", db);
q.bindValue(":directory", id);
q.exec();
@ -407,7 +408,7 @@ void LibraryBackend::AddOrUpdateSubdirs(const SubdirectoryList& subdirs) {
QSqlQuery delete_query("DELETE FROM subdirectories"
" WHERE directory = :id AND path = :path", db);
db.transaction();
ScopedTransaction transaction(&db);
foreach (const Subdirectory& subdir, subdirs) {
if (subdir.mtime == 0) {
// Delete the subdirectory
@ -437,7 +438,7 @@ void LibraryBackend::AddOrUpdateSubdirs(const SubdirectoryList& subdirs) {
}
}
}
db.commit();
transaction.Commit();
}
void LibraryBackend::AddOrUpdateSongs(const SongList& songs) {
@ -446,13 +447,13 @@ void LibraryBackend::AddOrUpdateSongs(const SongList& songs) {
QSqlQuery check_dir(
"SELECT ROWID FROM directories WHERE ROWID = :id", db);
QSqlQuery add_song(
"INSERT INTO songs (" + QString(Song::kColumnSpec) + ")"
" VALUES (" + QString(Song::kBindSpec) + ")", db);
"INSERT INTO songs (" + Song::kColumnSpec + ")"
" VALUES (" + Song::kBindSpec + ")", db);
QSqlQuery update_song(
"UPDATE songs SET " + QString(Song::kUpdateSpec) +
"UPDATE songs SET " + Song::kUpdateSpec +
" WHERE ROWID = :id", db);
db.transaction();
ScopedTransaction transaction(&db);
SongList added_songs;
SongList deleted_songs;
@ -495,7 +496,7 @@ void LibraryBackend::AddOrUpdateSongs(const SongList& songs) {
}
}
db.commit();
transaction.Commit();
if (!deleted_songs.isEmpty())
emit SongsDeleted(deleted_songs);
@ -511,14 +512,14 @@ void LibraryBackend::UpdateMTimesOnly(const SongList& songs) {
QSqlQuery q("UPDATE songs SET mtime = :mtime WHERE ROWID = :id", db);
db.transaction();
ScopedTransaction transaction(&db);
foreach (const Song& song, songs) {
q.bindValue(":mtime", song.mtime());
q.bindValue(":id", song.id());
q.exec();
CheckErrors(q.lastError());
}
db.commit();
transaction.Commit();
}
void LibraryBackend::DeleteSongs(const SongList &songs) {
@ -526,13 +527,13 @@ void LibraryBackend::DeleteSongs(const SongList &songs) {
QSqlQuery q("DELETE FROM songs WHERE ROWID = :id", db);
db.transaction();
ScopedTransaction transaction(&db);
foreach (const Song& song, songs) {
q.bindValue(":id", song.id());
q.exec();
CheckErrors(q.lastError());
}
db.commit();
transaction.Commit();
emit SongsDeleted(songs);
@ -564,7 +565,7 @@ LibraryBackend::AlbumList LibraryBackend::GetAlbumsByArtist(const QString& artis
SongList LibraryBackend::GetSongs(const QString& artist, const QString& album, const QueryOptions& opt) {
LibraryQuery query(opt);
query.SetColumnSpec("ROWID, " + QString(Song::kColumnSpec));
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddCompilationRequirement(false);
query.AddWhere("artist", artist);
query.AddWhere("album", album);
@ -583,7 +584,7 @@ SongList LibraryBackend::GetSongs(const QString& artist, const QString& album, c
Song LibraryBackend::GetSongById(int id) {
QSqlDatabase db(Connect());
QSqlQuery q("SELECT ROWID, " + QString(Song::kColumnSpec) + " FROM songs"
QSqlQuery q("SELECT ROWID, " + Song::kColumnSpec + " FROM songs"
" WHERE ROWID = :id", db);
q.bindValue(":id", id);
q.exec();
@ -612,7 +613,7 @@ LibraryBackend::AlbumList LibraryBackend::GetCompilationAlbums(const QueryOption
SongList LibraryBackend::GetCompilationSongs(const QString& album, const QueryOptions& opt) {
LibraryQuery query(opt);
query.SetColumnSpec("ROWID, " + QString(Song::kColumnSpec));
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddCompilationRequirement(true);
query.AddWhere("album", album);
@ -670,13 +671,13 @@ void LibraryBackend::UpdateCompilations() {
" SET sampler = :sampler,"
" effective_compilation = ((compilation OR :sampler OR forced_compilation_on) AND NOT forced_compilation_off) + 0"
" WHERE album = :album", db);
QSqlQuery find_songs("SELECT ROWID, " + QString(Song::kColumnSpec) + " FROM songs"
QSqlQuery find_songs("SELECT ROWID, " + Song::kColumnSpec + " FROM songs"
" WHERE album = :album AND sampler = :sampler", db);
SongList deleted_songs;
SongList added_songs;
db.transaction();
ScopedTransaction transaction(&db);
QMap<QString, CompilationInfo>::const_iterator it = compilation_info.constBegin();
for ( ; it != compilation_info.constEnd() ; ++it) {
@ -695,7 +696,7 @@ void LibraryBackend::UpdateCompilations() {
}
}
db.commit();
transaction.Commit();
if (!deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
@ -821,7 +822,7 @@ void LibraryBackend::ForceCompilation(const QString& artist, const QString& albu
// Get the songs before they're updated
LibraryQuery query;
query.SetColumnSpec("ROWID, " + QString(Song::kColumnSpec));
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddWhere("album", album);
if (!artist.isNull())
query.AddWhere("artist", artist);
@ -872,3 +873,71 @@ void LibraryBackend::ForceCompilation(const QString& artist, const QString& albu
bool LibraryBackend::ExecQuery(LibraryQuery *q) {
return !CheckErrors(q->Exec(Connect()));
}
LibraryBackendInterface::PlaylistList LibraryBackend::GetAllPlaylists() {
qWarning() << "Not implemented:" << __PRETTY_FUNCTION__;
return PlaylistList();
}
PlaylistItemList LibraryBackend::GetPlaylistItems(int playlist) {
QSqlDatabase db(Connect());
PlaylistItemList ret;
QSqlQuery q("SELECT songs.ROWID, " + Song::kJoinSpec + ","
" 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"
" WHERE p.playlist = :playlist", db);
q.bindValue(":playlist", playlist);
q.exec();
if (CheckErrors(q.lastError()))
return ret;
while (q.next()) {
// The song table gets joined first, plus one for the song ROWID
const int row = Song::kColumns.count() + 1;
PlaylistItem* item = PlaylistItem::NewFromType(q.value(row + 0).toString());
if (!item)
continue;
item->InitFromQuery(q);
ret << item;
}
return ret;
}
void LibraryBackend::SavePlaylist(int playlist, const PlaylistItemList& items) {
QSqlDatabase db(Connect());
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 (:playlist, :type, :library_id, :url, :title,"
" :artist, :album, :length, :radio_service)", db);
clear.bindValue(":playlist", playlist);
ScopedTransaction transaction(&db);
// Clear the existing items in the playlist
clear.exec();
if (CheckErrors(clear.lastError()))
return;
// Save the new ones
foreach (const PlaylistItem* item, items) {
insert.bindValue(":playlist", playlist);
item->BindToQuery(&insert);
insert.exec();
CheckErrors(insert.lastError());
}
transaction.Commit();
}

View File

@ -26,6 +26,7 @@
#include "directory.h"
#include "song.h"
#include "libraryquery.h"
#include "playlistitem.h"
#include <sqlite3.h>
@ -52,6 +53,12 @@ class LibraryBackendInterface : public QObject {
};
typedef QList<Album> AlbumList;
struct Playlist {
int id;
QString name;
};
typedef QList<Playlist> PlaylistList;
virtual void Stop() {};
// Get a list of directories in the library. Emits DirectoriesDiscovered.
@ -60,6 +67,7 @@ class LibraryBackendInterface : public QObject {
// Counts the songs in the library. Emits TotalSongCountUpdated
virtual void UpdateTotalSongCountAsync() = 0;
// Functions for getting songs
virtual SongList FindSongsInDirectory(int id) = 0;
virtual SubdirectoryList SubdirsInDirectory(int id) = 0;
@ -78,12 +86,19 @@ class LibraryBackendInterface : public QObject {
virtual Song GetSongById(int id) = 0;
virtual bool ExecQuery(LibraryQuery* q) = 0;
// Add or remove directories to the library
virtual void AddDirectory(const QString& path) = 0;
virtual void RemoveDirectory(const Directory& dir) = 0;
// Update compilation flags on songs
virtual void UpdateCompilationsAsync() = 0;
virtual bool ExecQuery(LibraryQuery* q) = 0;
// Functions for getting playlists
virtual PlaylistList GetAllPlaylists() = 0;
virtual PlaylistItemList GetPlaylistItems(int playlist) = 0;
virtual void SavePlaylist(int playlist, const PlaylistItemList& items) = 0;
public slots:
virtual void LoadDirectories() = 0;
@ -151,6 +166,10 @@ class LibraryBackend : public LibraryBackendInterface {
bool ExecQuery(LibraryQuery* q);
PlaylistList GetAllPlaylists();
PlaylistItemList GetPlaylistItems(int playlist);
void SavePlaylist(int playlist, const PlaylistItemList& items);
public slots:
void LoadDirectories();
void UpdateTotalSongCount();

View File

@ -0,0 +1,52 @@
/* This file is part of Clementine.
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "libraryplaylistitem.h"
#include <QtDebug>
#include <QSettings>
LibraryPlaylistItem::LibraryPlaylistItem(const QString& type)
: PlaylistItem(type)
{
}
LibraryPlaylistItem::LibraryPlaylistItem(const Song& song)
: PlaylistItem("Library"),
song_(song)
{
}
QUrl LibraryPlaylistItem::Url() const {
return QUrl::fromLocalFile(song_.filename());
}
void LibraryPlaylistItem::Reload() {
song_.InitFromFile(song_.filename(), song_.directory_id());
}
void LibraryPlaylistItem::InitFromQuery(const QSqlQuery &query) {
// Rows from the songs table come first
song_.InitFromQuery(query);
}
QVariant LibraryPlaylistItem::DatabaseValue(DatabaseColumn column) const {
switch (column) {
case Column_LibraryId: return song_.id();
default: return PlaylistItem::DatabaseValue(column);
}
}

43
src/libraryplaylistitem.h Normal file
View File

@ -0,0 +1,43 @@
/* This file is part of Clementine.
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LIBRARYPLAYLISTITEM_H
#define LIBRARYPLAYLISTITEM_H
#include "playlistitem.h"
#include "song.h"
class LibraryPlaylistItem : public PlaylistItem {
public:
LibraryPlaylistItem(const QString& type);
LibraryPlaylistItem(const Song& song);
void InitFromQuery(const QSqlQuery &query);
void BindToQuery(QSqlQuery *query) const;
void Reload();
Song Metadata() const { return song_; }
QUrl Url() const;
protected:
QVariant DatabaseValue(DatabaseColumn column) const;
private:
Song song_;
};
#endif // LIBRARYPLAYLISTITEM_H

View File

@ -120,8 +120,6 @@ MainWindow::MainWindow(QNetworkAccessManager* network, QWidget *parent)
library_sort_model_->setDynamicSortFilter(true);
library_sort_model_->sort(0);
playlist_->Restore();
playlist_->IgnoreSorting(true);
ui_.playlist->setModel(playlist_);
ui_.playlist->setItemDelegates(library_);
@ -226,6 +224,8 @@ MainWindow::MainWindow(QNetworkAccessManager* network, QWidget *parent)
connect(library_, SIGNAL(ScanFinished()), SLOT(LibraryScanFinished()));
connect(library_, SIGNAL(BackendReady(boost::shared_ptr<LibraryBackendInterface>)),
cover_manager_, SLOT(SetBackend(boost::shared_ptr<LibraryBackendInterface>)));
connect(library_, SIGNAL(BackendReady(boost::shared_ptr<LibraryBackendInterface>)),
playlist_, SLOT(SetBackend(boost::shared_ptr<LibraryBackendInterface>)));
// Age filters
QActionGroup* filter_age_group = new QActionGroup(this);
@ -546,7 +546,7 @@ void MainWindow::AddLibraryItemToPlaylist(const QModelIndex& index) {
idx = library_sort_model_->mapToSource(idx);
QModelIndex first_song =
playlist_->InsertSongs(library_->GetChildSongs(idx));
playlist_->InsertLibraryItems(library_->GetChildSongs(idx));
if (first_song.isValid() && player_->GetState() != Engine::Playing)
player_->PlayAt(first_song.row(), Engine::First);

View File

@ -331,27 +331,16 @@ inline void AddMetadata(const QString& key, int metadata, QVariantMap* map) {
QVariantMap Player::GetMetadata(const PlaylistItem& item) const {
QVariantMap ret;
if (item.type() == PlaylistItem::Type_Song) {
const Song& song = item.Metadata();
if (song.is_valid()) {
AddMetadata("location", item.Url().toString(), &ret);
AddMetadata("title", song.PrettyTitle(), &ret);
AddMetadata("artist", song.artist(), &ret);
AddMetadata("album", song.album(), &ret);
AddMetadata("time", song.length(), &ret);
AddMetadata("tracknumber", song.track(), &ret);
}
return ret;
} else {
AddMetadata("location", item.Url().toString(), &ret);
const Song& song = item.Metadata();
AddMetadata("title", song.PrettyTitle(), &ret);
AddMetadata("artist", song.artist(), &ret);
AddMetadata("album", song.album(), &ret);
AddMetadata("time", song.length(), &ret);
AddMetadata("tracknumber", song.track(), &ret);
return ret;
}
const Song& song = item.Metadata();
AddMetadata("location", item.Url().toString(), &ret);
AddMetadata("title", song.PrettyTitle(), &ret);
AddMetadata("artist", song.artist(), &ret);
AddMetadata("album", song.album(), &ret);
AddMetadata("time", song.length(), &ret);
AddMetadata("tracknumber", song.track(), &ret);
return ret;
}
QVariantMap Player::GetMetadata() const {

View File

@ -21,6 +21,8 @@
#include "radioplaylistitem.h"
#include "radiomodel.h"
#include "savedradio.h"
#include "librarybackend.h"
#include "libraryplaylistitem.h"
#include <QtDebug>
#include <QMimeData>
@ -334,7 +336,7 @@ bool Playlist::dropMimeData(const QMimeData* data, Qt::DropAction action, int ro
if (const SongMimeData* song_data = qobject_cast<const SongMimeData*>(data)) {
// Dragged from the library
InsertSongs(song_data->songs, row);
InsertLibraryItems(song_data->songs, row);
} else if (const RadioMimeData* radio_data = qobject_cast<const RadioMimeData*>(data)) {
// Dragged from the Radio pane
InsertRadioStations(radio_data->items, row);
@ -457,6 +459,14 @@ QModelIndex Playlist::InsertItems(const QList<PlaylistItem*>& items, int after)
return index(start, 0);
}
QModelIndex Playlist::InsertLibraryItems(const SongList& songs, int after) {
QList<PlaylistItem*> items;
foreach (const Song& song, songs) {
items << new LibraryPlaylistItem(song);
}
return InsertItems(items, after);
}
QModelIndex Playlist::InsertSongs(const SongList& songs, int after) {
QList<PlaylistItem*> items;
foreach (const Song& song, songs) {
@ -625,37 +635,34 @@ void Playlist::SetCurrentIsPaused(bool paused) {
index(current_item_.row(), ColumnCount));
}
void Playlist::SetBackend(boost::shared_ptr<LibraryBackendInterface> backend) {
backend_ = backend;
Restore();
}
void Playlist::Save() const {
settings_->beginWriteArray("items", items_.count());
for (int i=0 ; i<items_.count() ; ++i) {
settings_->setArrayIndex(i);
settings_->setValue("type", items_.at(i)->type_string());
items_.at(i)->Save(settings_.get());
}
settings_->endArray();
if (!backend_)
return;
backend_->SavePlaylist(1, items_);
settings_->setValue("last_index", last_played_index());
}
void Playlist::Restore() {
if (!backend_)
return;
qDeleteAll(items_);
items_.clear();
virtual_items_.clear();
int count = settings_->beginReadArray("items");
for (int i=0 ; i<count ; ++i) {
settings_->setArrayIndex(i);
QString type(settings_->value("type").toString());
items_ = backend_->GetPlaylistItems(1);
PlaylistItem* item = PlaylistItem::NewFromType(type);
if (!item)
continue;
item->Restore(*settings_.get());
items_ << item;
virtual_items_ << virtual_items_.count();
}
settings_->endArray();
for (int i=0 ; i<items_.count() ; ++i) {
virtual_items_ << i;
};
reset();

View File

@ -20,6 +20,8 @@
#include <QAbstractItemModel>
#include <QList>
#include <boost/shared_ptr.hpp>
#include "playlistitem.h"
#include "song.h"
#include "radioitem.h"
@ -27,6 +29,7 @@
#include "settingsprovider.h"
class RadioService;
class LibraryBackendInterface;
class Playlist : public QAbstractListModel {
Q_OBJECT
@ -102,7 +105,8 @@ class Playlist : public QAbstractListModel {
void set_scrobbled(bool v) { has_scrobbled_ = v; }
// Changing the playlist
QModelIndex InsertItems(const QList<PlaylistItem*>& items, int after = -1);
QModelIndex InsertItems(const PlaylistItemList& items, int after = -1);
QModelIndex InsertLibraryItems(const SongList& items, int after = -1);
QModelIndex InsertSongs(const SongList& items, int after = -1);
QModelIndex InsertRadioStations(const QList<RadioItem*>& items, int after = -1);
QModelIndex InsertStreamUrls(const QList<QUrl>& urls, int after = -1);
@ -126,6 +130,8 @@ class Playlist : public QAbstractListModel {
public slots:
void SetBackend(boost::shared_ptr<LibraryBackendInterface>);
void set_current_index(int index);
void Paused();
void Playing();
@ -155,7 +161,9 @@ class Playlist : public QAbstractListModel {
private:
boost::scoped_ptr<SettingsProvider> settings_;
QList<PlaylistItem*> items_;
boost::shared_ptr<LibraryBackendInterface> backend_;
PlaylistItemList items_;
QList<int> virtual_items_; // Contains the indices into items_ in the order
// that they will be played.

View File

@ -17,25 +17,31 @@
#include "playlistitem.h"
#include "songplaylistitem.h"
#include "radioplaylistitem.h"
#include "libraryplaylistitem.h"
#include <QtDebug>
QString PlaylistItem::type_string() const {
switch (type()) {
case Type_Song: return "Song";
case Type_Radio: return "Radio";
default:
qWarning() << "Invalid PlaylistItem type:" << type();
return QString::null;
}
}
PlaylistItem* PlaylistItem::NewFromType(const QString& type) {
if (type == "Song")
return new SongPlaylistItem;
if (type == "Library")
return new LibraryPlaylistItem(type);
if (type == "Stream" || type == "File")
return new SongPlaylistItem(type);
if (type == "Radio")
return new RadioPlaylistItem;
return new RadioPlaylistItem(type);
qWarning() << "Invalid PlaylistItem type:" << type;
return NULL;
}
void PlaylistItem::BindToQuery(QSqlQuery* query) const {
query->bindValue(":type", type());
query->bindValue(":library_id", DatabaseValue(Column_LibraryId));
query->bindValue(":url", DatabaseValue(Column_Url));
query->bindValue(":title", DatabaseValue(Column_Title));
query->bindValue(":artist", DatabaseValue(Column_Artist));
query->bindValue(":album", DatabaseValue(Column_Album));
query->bindValue(":length", DatabaseValue(Column_Length));
query->bindValue(":radio_service", DatabaseValue(Column_RadioService));
}

View File

@ -21,20 +21,16 @@
#include <QUrl>
class Song;
class SettingsProvider;
class QSqlQuery;
class PlaylistItem {
public:
PlaylistItem() {}
PlaylistItem(const QString& type) : type_(type) {}
virtual ~PlaylistItem() {}
static PlaylistItem* NewFromType(const QString& type);
enum Type {
Type_Song,
Type_Radio,
};
enum Option {
Default = 0x00,
@ -45,13 +41,12 @@ class PlaylistItem {
};
Q_DECLARE_FLAGS(Options, Option);
virtual Type type() const = 0;
QString type_string() const;
virtual QString type() const { return type_; }
virtual Options options() const { return Default; }
virtual void Save(SettingsProvider* settings) const = 0;
virtual void Restore(const SettingsProvider& settings) = 0;
virtual void InitFromQuery(const QSqlQuery& query) = 0;
void BindToQuery(QSqlQuery* query) const;
virtual void Reload() {}
virtual Song Metadata() const = 0;
@ -69,7 +64,24 @@ class PlaylistItem {
virtual void SetTemporaryMetadata(const Song& metadata) {Q_UNUSED(metadata)}
virtual void ClearTemporaryMetadata() {}
protected:
enum DatabaseColumn {
Column_LibraryId,
Column_Url,
Column_Title,
Column_Artist,
Column_Album,
Column_Length,
Column_RadioService,
};
virtual QVariant DatabaseValue(DatabaseColumn) const {
return QVariant(QVariant::String); }
QString type_;
};
typedef QList<PlaylistItem*> PlaylistItemList;
Q_DECLARE_OPERATORS_FOR_FLAGS(PlaylistItem::Options);

View File

@ -21,15 +21,18 @@
#include <QSettings>
#include <QApplication>
#include <QtDebug>
RadioPlaylistItem::RadioPlaylistItem()
: service_(NULL)
RadioPlaylistItem::RadioPlaylistItem(const QString& type)
: PlaylistItem(type),
service_(NULL)
{
}
RadioPlaylistItem::RadioPlaylistItem(RadioService* service, const QUrl& url,
const QString& title, const QString& artist)
: service_(service),
: PlaylistItem("Radio"),
service_(service),
url_(url),
title_(title),
artist_(artist)
@ -37,22 +40,30 @@ RadioPlaylistItem::RadioPlaylistItem(RadioService* service, const QUrl& url,
InitMetadata();
}
void RadioPlaylistItem::Save(SettingsProvider* settings) const {
settings->setValue("service", service_->name());
settings->setValue("url", url_.toString());
settings->setValue("title", title_);
settings->setValue("artist", artist_);
}
void RadioPlaylistItem::InitFromQuery(const QSqlQuery &query) {
// The song table gets joined first, plus one for the song ROWID
const int row = Song::kColumns.count() + 1;
void RadioPlaylistItem::Restore(const SettingsProvider& settings) {
service_ = RadioModel::ServiceByName(settings.value("service").toString());
url_ = settings.value("url").toString();
title_ = settings.value("title").toString();
artist_ = settings.value("artist").toString();
url_ = query.value(row + 1).toString();
title_ = query.value(row + 2).toString();
artist_ = query.value(row + 3).toString();
QString service(query.value(row + 6).toString());
service_ = RadioModel::ServiceByName(service);
InitMetadata();
}
QVariant RadioPlaylistItem::DatabaseValue(DatabaseColumn column) const {
switch (column) {
case Column_Url: return url_.toString();
case Column_Title: return title_;
case Column_Artist: return artist_;
case Column_RadioService: return service_->name();
default: return PlaylistItem::DatabaseValue(column);
}
}
void RadioPlaylistItem::InitMetadata() {
if (!service_)
metadata_.set_title(QApplication::translate("RadioPlaylistItem", "Radio service couldn't be loaded :-("));

View File

@ -26,15 +26,14 @@ class RadioService;
class RadioPlaylistItem : public PlaylistItem {
public:
RadioPlaylistItem();
RadioPlaylistItem(const QString& type);
RadioPlaylistItem(RadioService* service, const QUrl& url,
const QString& title, const QString& artist);
Type type() const { return Type_Radio; }
Options options() const;
void Save(SettingsProvider* settings) const;
void Restore(const SettingsProvider& settings);
void InitFromQuery(const QSqlQuery &query);
void BindToQuery(QSqlQuery *query) const;
Song Metadata() const;
@ -46,6 +45,9 @@ class RadioPlaylistItem : public PlaylistItem {
void SetTemporaryMetadata(const Song& metadata);
void ClearTemporaryMetadata();
protected:
QVariant DatabaseValue(DatabaseColumn) const;
private:
void InitMetadata();

43
src/scopedtransaction.cpp Normal file
View File

@ -0,0 +1,43 @@
/* This file is part of Clementine.
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "scopedtransaction.h"
#include <QSqlDatabase>
#include <QtDebug>
ScopedTransaction::ScopedTransaction(QSqlDatabase* db)
: db_(db),
pending_(true)
{
}
ScopedTransaction::~ScopedTransaction() {
if (pending_) {
qDebug() << __PRETTY_FUNCTION__ << "Rolling back transaction";
db_->rollback();
}
}
void ScopedTransaction::Commit() {
if (!pending_) {
qWarning() << "Tried to commit a ScopedTransaction twice";
return;
}
db_->commit();
pending_ = false;
}

39
src/scopedtransaction.h Normal file
View File

@ -0,0 +1,39 @@
/* This file is part of Clementine.
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SCOPEDTRANSACTION_H
#define SCOPEDTRANSACTION_H
#include <boost/noncopyable.hpp>
class QSqlDatabase;
// Opens a transaction on a database.
// Rolls back the transaction if the object goes out of scope before Commit()
// is called.
class ScopedTransaction : boost::noncopyable {
public:
ScopedTransaction(QSqlDatabase* db);
~ScopedTransaction();
void Commit();
private:
QSqlDatabase* db_;
bool pending_;
};
#endif // SCOPEDTRANSACTION_H

View File

@ -53,35 +53,35 @@ using boost::scoped_ptr;
#include "engines/enginebase.h"
#include "albumcoverloader.h"
const char* Song::kColumnSpec =
"title, album, artist, albumartist, composer, "
"track, disc, bpm, year, genre, comment, compilation, "
"length, bitrate, samplerate, directory, filename, "
"mtime, ctime, filesize, sampler, art_automatic, art_manual, "
"filetype, playcount, lastplayed, rating, forced_compilation_on, "
"forced_compilation_off, effective_compilation";
static QStringList Prepend(const QString& text, const QStringList& list) {
QStringList ret(list);
for (int i=0 ; i<ret.count() ; ++i)
ret[i].prepend(text);
return ret;
}
static QStringList Updateify(const QStringList& list) {
QStringList ret(list);
for (int i=0 ; i<ret.count() ; ++i)
ret[i].prepend(ret[i] + " = :");
return ret;
}
const QStringList Song::kColumns = QStringList()
<< "title" << "album" << "artist" << "albumartist" << "composer" << "track"
<< "disc" << "bpm" << "year" << "genre" << "comment" << "compilation"
<< "length" << "bitrate" << "samplerate" << "directory" << "filename"
<< "mtime" << "ctime" << "filesize" << "sampler" << "art_automatic"
<< "art_manual" << "filetype" << "playcount" << "lastplayed" << "rating"
<< "forced_compilation_on" << "forced_compilation_off"
<< "effective_compilation";
const QString Song::kColumnSpec = Song::kColumns.join(", ");
const QString Song::kJoinSpec = Prepend("songs.", Song::kColumns).join(", ");
const QString Song::kBindSpec = Prepend(":", Song::kColumns).join(", ");
const QString Song::kUpdateSpec = Updateify(Song::kColumns).join(", ");
const char* Song::kBindSpec =
":title, :album, :artist, :albumartist, :composer, "
":track, :disc, :bpm, :year, :genre, :comment, :compilation, "
":length, :bitrate, :samplerate, :directory_id, :filename, "
":mtime, :ctime, :filesize, :sampler, :art_automatic, :art_manual, "
":filetype, :playcount, :lastplayed, :rating, :forced_compilation_on, "
":forced_compilation_off, :effective_compilation";
const char* Song::kUpdateSpec =
"title = :title, album = :album, artist = :artist, "
"albumartist = :albumartist, composer = :composer, track = :track, "
"disc = :disc, bpm = :bpm, year = :year, genre = :genre, "
"comment = :comment, compilation = :compilation, length = :length, "
"bitrate = :bitrate, samplerate = :samplerate, "
"directory = :directory_id, filename = :filename, mtime = :mtime, "
"ctime = :ctime, filesize = :filesize, sampler = :sampler, "
"art_automatic = :art_automatic, art_manual = :art_manual, "
"filetype = :filetype, playcount = :playcount, lastplayed = :lastplayed, "
"rating = :rating, forced_compilation_on = :forced_compilation_on, "
"forced_compilation_off = :forced_compilation_off, "
"effective_compilation = :effective_compilation";
static TagLib::String QStringToTaglibString(const QString& s);

View File

@ -56,9 +56,11 @@ class Song {
Song(const Song& other);
Song(FileRefFactory* factory);
static const char* kColumnSpec;
static const char* kBindSpec;
static const char* kUpdateSpec;
static const QStringList kColumns;
static const QString kColumnSpec;
static const QString kJoinSpec;
static const QString kBindSpec;
static const QString kUpdateSpec;
// Don't change these values - they're stored in the database
enum FileType {

View File

@ -15,76 +15,58 @@
*/
#include "songplaylistitem.h"
#include "settingsprovider.h"
#include <QtDebug>
#include <QFile>
#include <QSettings>
SongPlaylistItem::SongPlaylistItem()
SongPlaylistItem::SongPlaylistItem(const QString& type)
: PlaylistItem(type)
{
}
SongPlaylistItem::SongPlaylistItem(const Song& song)
: song_(song)
: PlaylistItem(song.filetype() == Song::Type_Stream ? "Stream" : "File"),
song_(song)
{
}
void SongPlaylistItem::Save(SettingsProvider* settings) const {
settings->setValue("filename", song_.filename());
settings->setValue("art_automatic", song_.art_automatic());
settings->setValue("art_manual", song_.art_manual());
void SongPlaylistItem::InitFromQuery(const QSqlQuery &query) {
// The song table gets joined first, plus one for the song ROWID
const int row = Song::kColumns.count() + 1;
if (song_.filetype() == Song::Type_Stream) {
SaveStream(settings);
QString filename(query.value(row + 1).toString());
if (type() == "Stream") {
QString title(query.value(row + 2).toString());
QString artist(query.value(row + 3).toString());
QString album(query.value(row + 4).toString());
int length(query.value(row + 5).toInt());
if (title.isEmpty()) title = "Unknown";
if (artist.isEmpty()) artist = "Unknown";
if (album.isEmpty()) album = "Unknown";
if (length == 0) length = -1;
song_.set_filename(filename);
song_.set_filetype(Song::Type_Stream);
song_.Init(title, artist, album, length);
} else {
SaveFile(settings);
song_.InitFromFile(filename, -1);
}
}
void SongPlaylistItem::SaveFile(SettingsProvider* settings) const {
settings->setValue("stream", false);
settings->setValue("library_directory", song_.directory_id());
}
void SongPlaylistItem::SaveStream(SettingsProvider* settings) const {
settings->setValue("stream", true);
settings->setValue("title", song_.title());
settings->setValue("artist", song_.artist());
settings->setValue("album", song_.album());
settings->setValue("length", song_.length());
}
void SongPlaylistItem::Restore(const SettingsProvider& settings) {
song_.set_art_automatic(settings.value("art_automatic").toString());
song_.set_art_manual(settings.value("art_manual").toString());
const bool stream = settings.value("stream", false).toBool();
if (stream) {
RestoreStream(settings);
} else {
RestoreFile(settings);
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);
}
}
void SongPlaylistItem::RestoreFile(const SettingsProvider& settings) {
QString filename(settings.value("filename").toString());
int directory_id(settings.value("library_directory", -1).toInt());
song_.InitFromFile(filename, directory_id);
}
void SongPlaylistItem::RestoreStream(const SettingsProvider& settings) {
QString filename(settings.value("filename").toString());
song_.set_filename(filename);
song_.set_filetype(Song::Type_Stream);
song_.Init(settings.value("title", "Unknown").toString(),
settings.value("artist", "Unknown").toString(),
settings.value("album", "Unknown").toString(),
settings.value("length", -1).toInt());
}
QUrl SongPlaylistItem::Url() const {
if (QFile::exists(song_.filename())) {
return QUrl::fromLocalFile(song_.filename());

View File

@ -22,25 +22,20 @@
class SongPlaylistItem : public PlaylistItem {
public:
SongPlaylistItem();
SongPlaylistItem(const QString& type);
SongPlaylistItem(const Song& song);
Type type() const { return Type_Song; }
void Save(SettingsProvider* settings) const;
void Restore(const SettingsProvider& settings);
void InitFromQuery(const QSqlQuery &query);
void Reload();
Song Metadata() const { return song_; }
QUrl Url() const;
private:
void SaveFile(SettingsProvider* settings) const;
void SaveStream(SettingsProvider* settings) const;
protected:
QVariant DatabaseValue(DatabaseColumn) const;
void RestoreFile(const SettingsProvider& settings);
void RestoreStream(const SettingsProvider& settings);
private:
Song song_;
};

View File

@ -19,7 +19,7 @@
using ::testing::_;
using ::testing::Return;
MockPlaylistItem::MockPlaylistItem() {
EXPECT_CALL(*this, Save(_))
.WillRepeatedly(Return());
MockPlaylistItem::MockPlaylistItem()
: PlaylistItem("DummyType")
{
}

View File

@ -27,14 +27,10 @@ class MockPlaylistItem : public PlaylistItem {
public:
MockPlaylistItem();
MOCK_CONST_METHOD0(type,
Type());
MOCK_CONST_METHOD0(options,
Options());
MOCK_CONST_METHOD1(Save,
void(SettingsProvider* settings));
MOCK_METHOD1(Restore,
void(const SettingsProvider& settings));
MOCK_METHOD1(InitFromQuery,
void(const QSqlQuery& settings));
MOCK_METHOD0(Reload,
void());
MOCK_CONST_METHOD0(Metadata,
@ -49,6 +45,8 @@ class MockPlaylistItem : public PlaylistItem {
void(const Song& metadata));
MOCK_METHOD0(ClearTemporaryMetadata,
void());
MOCK_METHOD1(DatabaseValue,
QVariant(DatabaseColumn));
};
#endif // MOCK_PLAYLISTITEM_H

View File

@ -47,8 +47,6 @@ class PlaylistTest : public ::testing::Test {
metadata.Init(title, artist, album, length);
MockPlaylistItem* ret = new MockPlaylistItem;
EXPECT_CALL(*ret, type())
.WillRepeatedly(Return(PlaylistItem::Type_Song));
EXPECT_CALL(*ret, Metadata())
.WillRepeatedly(Return(metadata));