diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt index 74bb9b592..7d7be4072 100644 --- a/scripts/CMakeLists.txt +++ b/scripts/CMakeLists.txt @@ -7,3 +7,4 @@ function(install_script_files scriptname) endfunction(install_script_files) add_subdirectory(digitallyimported-radio) +add_subdirectory(remove-duplicates) diff --git a/scripts/remove-duplicates/CMakeLists.txt b/scripts/remove-duplicates/CMakeLists.txt new file mode 100644 index 000000000..768622c6f --- /dev/null +++ b/scripts/remove-duplicates/CMakeLists.txt @@ -0,0 +1,5 @@ +install_script_files(remove-duplicates + icon.png + remove_duplicates.py + script.ini +) diff --git a/scripts/remove-duplicates/icon.png b/scripts/remove-duplicates/icon.png new file mode 100644 index 000000000..8645dd9e4 Binary files /dev/null and b/scripts/remove-duplicates/icon.png differ diff --git a/scripts/remove-duplicates/remove_duplicates.py b/scripts/remove-duplicates/remove_duplicates.py new file mode 100644 index 000000000..9379bbb89 --- /dev/null +++ b/scripts/remove-duplicates/remove_duplicates.py @@ -0,0 +1,70 @@ +import clementine +from clementine import SongInsertVetoListener + + +class RemoveDuplicatesListener(SongInsertVetoListener): + + def __init__(self): + SongInsertVetoListener.__init__(self) + + def init_listener(self): + for playlist in clementine.playlists.GetAllPlaylists(): + playlist.AddSongInsertVetoListener(self) + + clementine.playlists.PlaylistAdded.connect(self.playlist_added) + + def remove_duplicates(self): + for playlist in clementine.playlists.GetAllPlaylists(): + self.remove_duplicates_from(playlist) + + def playlist_added(self, playlist_id): + playlist = clementine.playlists.playlist(playlist_id) + + playlist.AddSongInsertVetoListener(self) + self.remove_duplicates_from(playlist) + + def AboutToInsertSongs(self, old_songs, new_songs): + vetoed = [] + used_urls = set() + + songs = old_songs + new_songs + for song in songs: + url = self.url_for_song(song) + + # don't veto songs without URL (possibly radios) + if len(url) > 0: + if url in used_urls: + vetoed.append(song) + used_urls.add(url) + + return vetoed + + def remove_duplicates_from(self, playlist): + indices = [] + used_urls = set() + + songs = playlist.GetAllSongs() + for i in range(0, len(songs)): + song = songs[i] + url = self.url_for_song(song) + + # ignore songs without URL (possibly radios) + if len(url) > 0: + if url in used_urls: + indices.append(i) + used_urls.add(url) + + if len(indices) > 0: + playlist.RemoveItemsWithoutUndo(indices) + + def url_for_song(self, song): + if not song.filename() == "": + return song.filename() + ":" + str(song.beginning()) + else: + return "" + + +script = RemoveDuplicatesListener() + +script.init_listener() +script.remove_duplicates() \ No newline at end of file diff --git a/scripts/remove-duplicates/script.ini b/scripts/remove-duplicates/script.ini new file mode 100644 index 000000000..5841f8f08 --- /dev/null +++ b/scripts/remove-duplicates/script.ini @@ -0,0 +1,9 @@ +[Script] +name=Duplicate remover +description=This script will prevent duplicates being added to your playlists. +author=Pawel Bara +url=http://www.clementine-player.org +icon=icon.png + +language=python +script_file=remove_duplicates.py diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index be3f25b70..3df725522 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -758,6 +758,7 @@ if(HAVE_SCRIPTING_PYTHON) ${CMAKE_CURRENT_BINARY_DIR}/sipclementineQList0100Song.cpp ${CMAKE_CURRENT_BINARY_DIR}/sipclementineQList0100Subdirectory.cpp ${CMAKE_CURRENT_BINARY_DIR}/sipclementineQList0100TaskManagerTask.cpp + ${CMAKE_CURRENT_BINARY_DIR}/sipclementineQList0101Playlist.cpp ) endif(HAVE_SCRIPTING_PYTHON) diff --git a/src/core/song.cpp b/src/core/song.cpp index bdd307a03..cf19c9cbc 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -1154,3 +1154,9 @@ QFuture Song::BackgroundSave() const { QFuture future = QtConcurrent::run(&Song::Save, Song(*this)); return future; } + +bool Song::operator==(const Song& other) const { + // TODO: this isn't working for radios + return filename() == other.filename() && + beginning() == other.beginning(); +} diff --git a/src/core/song.h b/src/core/song.h index b4bc0b206..b55a23d45 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -254,6 +254,8 @@ class Song { // Comparison functions bool IsMetadataEqual(const Song& other) const; + bool operator==(const Song& other) const; + private: void GuessFileType(TagLib::FileRef* fileref); static bool Save(const Song& song); diff --git a/src/playlist/playlist.cpp b/src/playlist/playlist.cpp index e34af8674..7696dd5f4 100644 --- a/src/playlist/playlist.cpp +++ b/src/playlist/playlist.cpp @@ -41,14 +41,15 @@ #include "smartplaylists/generatorinserter.h" #include "smartplaylists/generatormimedata.h" -#include #include -#include #include -#include #include -#include +#include +#include +#include #include +#include +#include #include #include @@ -122,13 +123,14 @@ Playlist::~Playlist() { } template -static void InsertSongItems(Playlist* playlist, const SongList& songs, - int pos, bool play_now, bool enqueue) { +void Playlist::InsertSongItems(const SongList& songs, int pos, bool play_now, bool enqueue) { PlaylistItemList items; + foreach (const Song& song, songs) { items << PlaylistItemPtr(new T(song)); } - playlist->InsertItems(items, pos, play_now, enqueue); + + InsertItems(items, pos, play_now, enqueue); } QVariant Playlist::headerData(int section, Qt::Orientation, int role) const { @@ -609,13 +611,13 @@ bool Playlist::dropMimeData(const QMimeData* data, Qt::DropAction action, int ro // We want to check if these songs are from the actual local file backend, // if they are we treat them differently. if (song_data->backend && song_data->backend->songs_table() == Library::kSongsTable) - InsertSongItems(this, song_data->songs, row, play_now, enqueue_now); + InsertSongItems(song_data->songs, row, play_now, enqueue_now); else if (song_data->backend && song_data->backend->songs_table() == MagnatuneService::kSongsTable) - InsertSongItems(this, song_data->songs, row, play_now, enqueue_now); + InsertSongItems(song_data->songs, row, play_now, enqueue_now); else if (song_data->backend && song_data->backend->songs_table() == JamendoService::kSongsTable) - InsertSongItems(this, song_data->songs, row, play_now, enqueue_now); + InsertSongItems(song_data->songs, row, play_now, enqueue_now); else - InsertSongItems(this, song_data->songs, row, play_now, enqueue_now); + InsertSongItems(song_data->songs, row, play_now, enqueue_now); } else if (const RadioMimeData* radio_data = qobject_cast(data)) { // Dragged from the Radio pane InsertRadioStations(radio_data->model, radio_data->indexes, @@ -783,10 +785,44 @@ void Playlist::MoveItemsWithoutUndo(int start, const QList& dest_rows) { Save(); } -void Playlist::InsertItems(const PlaylistItemList& items, int pos, bool play_now, bool enqueue) { - if (items.isEmpty()) +void Playlist::InsertItems(const PlaylistItemList& itemsIn, int pos, bool play_now, bool enqueue) { + if (itemsIn.isEmpty()) return; + PlaylistItemList items = itemsIn; + + // exercise vetoes + SongList songs; + + foreach(PlaylistItemPtr item, items) { + songs << item.get()->Metadata(); + } + + QList vetoed; + foreach(SongInsertVetoListener* listener, veto_listeners_) { + foreach(const Song& song, listener->AboutToInsertSongs(GetAllSongs(), songs)) { + vetoed.append(song); + } + } + + if(!vetoed.isEmpty()) { + QMutableListIterator it(items); + while (it.hasNext()) { + PlaylistItemPtr item = it.next(); + const Song& current = item.get()->Metadata(); + + if(vetoed.contains(current)) { + vetoed.removeOne(current); + it.remove(); + } + } + + // check for empty items once again after veto + if(items.isEmpty()) { + return; + } + } + const int start = pos == -1 ? items_.count() : pos; undo_stack_->push(new PlaylistUndoCommands::InsertItems(this, items, pos, enqueue)); @@ -836,11 +872,11 @@ void Playlist::InsertItemsWithoutUndo(const PlaylistItemList& items, } void Playlist::InsertLibraryItems(const SongList& songs, int pos, bool play_now, bool enqueue) { - InsertSongItems(this, songs, pos, play_now, enqueue); + InsertSongItems(songs, pos, play_now, enqueue); } void Playlist::InsertSongs(const SongList& songs, int pos, bool play_now, bool enqueue) { - InsertSongItems(this, songs, pos, play_now, enqueue); + InsertSongItems(songs, pos, play_now, enqueue); } void Playlist::InsertSongsOrLibraryItems(const SongList& songs, int pos, bool play_now, bool enqueue) { @@ -1109,6 +1145,31 @@ void Playlist::ItemsLoaded() { } } +static bool DescendingIntLessThan(int a, int b) { + return a > b; +} + +void Playlist::RemoveItemsWithoutUndo(const QList& indicesIn) { + // Sort the indices descending because removing elements 'backwards' + // is easier - indices don't 'move' in the process. + QList indices = indicesIn; + qSort(indices.begin(), indices.end(), DescendingIntLessThan); + + for(int j = 0; j < indices.count(); j++) { + int beginning = indices[j], end = indices[j]; + + // Splits the indices into sequences. For example this: [1, 2, 4], + // will get split into [1, 2] and [4]. + while(j != indices.count() - 1 && indices[j] == indices[j + 1] + 1) { + beginning--; + j++; + } + + // Remove the current sequence. + removeRows(beginning, end - beginning + 1); + } +} + bool Playlist::removeRows(int row, int count, const QModelIndex& parent) { if (row < 0 || row >= items_.size() || row + count > items_.size()) { return false; @@ -1306,6 +1367,21 @@ void Playlist::RateSong(const QModelIndex& index, double rating) { } } +void Playlist::AddSongInsertVetoListener(SongInsertVetoListener* listener) { + veto_listeners_.append(listener); + connect(listener, SIGNAL(destroyed()), this, SLOT(SongInsertVetoListenerDestroyed())); +} + +void Playlist::RemoveSongInsertVetoListener(SongInsertVetoListener* listener) { + disconnect(listener, SIGNAL(destroyed()), this, SLOT(SongInsertVetoListenerDestroyed())); + veto_listeners_.removeAll(listener); +} + +void Playlist::SongInsertVetoListenerDestroyed() { + // qobject_cast returns NULL here for Python SIP listeners. + veto_listeners_.removeAll(static_cast(sender())); +} + void Playlist::Shuffle() { layoutAboutToBeChanged(); diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h index f3dab6118..16b447251 100644 --- a/src/playlist/playlist.h +++ b/src/playlist/playlist.h @@ -48,6 +48,19 @@ typedef QMap ColumnAlignmentMap; Q_DECLARE_METATYPE(Qt::Alignment); Q_DECLARE_METATYPE(ColumnAlignmentMap); +// Objects that may prevent a song being added to the playlist. When there +// is something about to be inserted into it, Playlist notifies all of it's +// listeners about the fact and every one of them picks 'invalid' songs. +class SongInsertVetoListener : public QObject { + Q_OBJECT + +public: + // Listener returns a list of 'invalid' songs. 'old_songs' are songs that are + // currently in the playlist and 'new_songs' are the songs about to be added if + // nobody exercises a veto. + virtual SongList AboutToInsertSongs(const SongList& old_songs, const SongList& new_songs) = 0; +}; + class Playlist : public QAbstractListModel { Q_OBJECT @@ -171,6 +184,8 @@ class Playlist : public QAbstractListModel { void InsertSongsOrLibraryItems(const SongList& items, int pos = -1, bool play_now = false, bool enqueue = false); void InsertSmartPlaylist (smart_playlists::GeneratorPtr gen, int pos = -1, bool play_now = false, bool enqueue = false); void InsertUrls (const QList& urls, int pos = -1, bool play_now = false, bool enqueue = false); + // Removes items with given indices from the playlist. This operation is not undoable. + void RemoveItemsWithoutUndo (const QList& indices); void StopAfter(int row); void ReloadItems(const QList& rows); @@ -178,6 +193,12 @@ class Playlist : public QAbstractListModel { // Changes rating of a song to the given value asynchronously void RateSong(const QModelIndex& index, double rating); + // Registers an object which will get notifications when new songs + // are about to be inserted into this playlist. + void AddSongInsertVetoListener(SongInsertVetoListener* listener); + // Unregisters a SongInsertVetoListener object. + void RemoveSongInsertVetoListener(SongInsertVetoListener* listener); + // QAbstractListModel int rowCount(const QModelIndex& = QModelIndex()) const { return items_.count(); } int columnCount(const QModelIndex& = QModelIndex()) const { return ColumnCount; } @@ -236,6 +257,9 @@ class Playlist : public QAbstractListModel { const QModelIndexList& items, int pos, bool play_now, bool enqueue); + template + void InsertSongItems(const SongList& songs, int pos, bool play_now, bool enqueue); + // Modify the playlist without changing the undo stack. These are used by // our friends in PlaylistUndoCommands void InsertItemsWithoutUndo(const PlaylistItemList& items, int pos, @@ -254,6 +278,7 @@ class Playlist : public QAbstractListModel { void SongSaveComplete(); void ItemReloadComplete(); void ItemsLoaded(); + void SongInsertVetoListenerDestroyed(); private: bool is_loading_; @@ -296,6 +321,8 @@ class Playlist : public QAbstractListModel { smart_playlists::GeneratorPtr dynamic_playlist_; ColumnAlignmentMap column_alignments_; + + QList veto_listeners_; }; QDataStream& operator <<(QDataStream&, const Playlist*); diff --git a/src/playlist/playlistmanager.cpp b/src/playlist/playlistmanager.cpp index 6f3c26af7..c3aa16761 100644 --- a/src/playlist/playlistmanager.cpp +++ b/src/playlist/playlistmanager.cpp @@ -70,6 +70,16 @@ void PlaylistManager::Init(LibraryBackend* library_backend, emit PlaylistManagerInitialized(); } +const QList PlaylistManager::GetAllPlaylists() const { + QList result; + + foreach(const Data& data, playlists_.values()) { + result.append(data.p); + } + + return result; +} + Playlist* PlaylistManager::AddPlaylist(int id, const QString& name) { Playlist* ret = new Playlist(playlist_backend_, task_manager_, library_backend_, id); ret->set_sequence(sequence_); diff --git a/src/playlist/playlistmanager.h b/src/playlist/playlistmanager.h index efd6403f7..6a3961675 100644 --- a/src/playlist/playlistmanager.h +++ b/src/playlist/playlistmanager.h @@ -49,6 +49,9 @@ public: Playlist* current() const { return playlist(current_id()); } Playlist* active() const { return playlist(active_id()); } + // Returns the collection of playlists managed by this PlaylistManager. + const QList GetAllPlaylists() const; + const QItemSelection& selection(int id) const { return playlists_[id].selection; } const QItemSelection& current_selection() const { return selection(current_id()); } const QItemSelection& active_selection() const { return selection(active_id()); } diff --git a/src/scripting/python/clementine.sip b/src/scripting/python/clementine.sip index 8c923209a..0a1779e74 100644 --- a/src/scripting/python/clementine.sip +++ b/src/scripting/python/clementine.sip @@ -25,6 +25,7 @@ %Include scriptinterface.sip %Include settingsdialog.sip %Include song.sip +%Include songinsertvetolistener.sip %Include songloader.sip %Include taskmanager.sip %Include uiinterface.sip diff --git a/src/scripting/python/playlist.sip b/src/scripting/python/playlist.sip index b9cd30d6e..d6ea78dfb 100644 --- a/src/scripting/python/playlist.sip +++ b/src/scripting/python/playlist.sip @@ -2,6 +2,7 @@ class Playlist : QAbstractListModel { %TypeHeaderCode #include "playlist/playlist.h" +#include "scripting/python/pythonengine.h" %End public: @@ -107,12 +108,21 @@ public: void InsertSongsOrLibraryItems(const SongList& items, int pos = -1, bool play_now = false, bool enqueue = false); // void InsertSmartPlaylist (smart_playlists::GeneratorPtr gen, int pos = -1, bool play_now = false, bool enqueue = false); void InsertUrls (const QList& urls, int pos = -1, bool play_now = false, bool enqueue = false); + void RemoveItemsWithoutUndo (const QList& indicesIn); + void StopAfter(int row); void ReloadItems(const QList& rows); // Changes rating of a song to the given value asynchronously void RateSong(const QModelIndex& index, double rating); + void AddSongInsertVetoListener(SongInsertVetoListener* listener /Transfer/); +%MethodCode + sipCpp->AddSongInsertVetoListener(a0); + PythonEngine::instance()->RegisterNativeObject(a0); +%End + void RemoveSongInsertVetoListener(SongInsertVetoListener* listener); + public slots: void set_current_row(int index); diff --git a/src/scripting/python/playlistmanager.sip b/src/scripting/python/playlistmanager.sip index d6cfe01ef..c7eb16499 100644 --- a/src/scripting/python/playlistmanager.sip +++ b/src/scripting/python/playlistmanager.sip @@ -12,6 +12,8 @@ public: Playlist* current() const; Playlist* active() const; + const QList GetAllPlaylists() const; + const QItemSelection& selection(int id) const; const QItemSelection& current_selection() const; const QItemSelection& active_selection() const; diff --git a/src/scripting/python/scriptinterface.sip b/src/scripting/python/scriptinterface.sip index f05504935..bab9379d0 100644 --- a/src/scripting/python/scriptinterface.sip +++ b/src/scripting/python/scriptinterface.sip @@ -26,6 +26,7 @@ class ScriptInterface : QObject { CLASS(RadioService), CLASS(ScriptInterface), CLASS(SettingsDialog), + CLASS(SongInsertVetoListener), CLASS(SongLoader), CLASS(TaskManager), CLASS(UIInterface), diff --git a/src/scripting/python/songinsertvetolistener.sip b/src/scripting/python/songinsertvetolistener.sip new file mode 100644 index 000000000..2903543de --- /dev/null +++ b/src/scripting/python/songinsertvetolistener.sip @@ -0,0 +1,8 @@ +class SongInsertVetoListener : QObject { +%TypeHeaderCode + #include "playlist/playlist.h" +%End + +public: + virtual SongList AboutToInsertSongs(const SongList& old_songs, const SongList& new_songs) = 0; +}; diff --git a/src/scripting/script.cpp b/src/scripting/script.cpp index e9f24c6d4..30f2f488f 100644 --- a/src/scripting/script.cpp +++ b/src/scripting/script.cpp @@ -32,7 +32,9 @@ Script::~Script() { } void Script::AddNativeObject(QObject* object) { - native_objects_ << object; + if(!native_objects_.contains(object)) { + native_objects_ << object; + } } void Script::RemoveNativeObject(QObject* object) {