From a883630ab3454b2ce1ef80ff4f1ee6993b7b96cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bara?= Date: Sun, 16 Jan 2011 23:46:58 +0000 Subject: [PATCH] duplicate remover script (fixes #21) fix a scripting crash when one native object was registered more than once veto mechanism for inserting songs into playlist --- scripts/CMakeLists.txt | 1 + scripts/remove-duplicates/CMakeLists.txt | 5 + scripts/remove-duplicates/icon.png | Bin 0 -> 6309 bytes .../remove-duplicates/remove_duplicates.py | 70 ++++++++++++ scripts/remove-duplicates/script.ini | 9 ++ src/CMakeLists.txt | 1 + src/core/song.cpp | 6 + src/core/song.h | 2 + src/playlist/playlist.cpp | 106 +++++++++++++++--- src/playlist/playlist.h | 27 +++++ src/playlist/playlistmanager.cpp | 10 ++ src/playlist/playlistmanager.h | 3 + src/scripting/python/clementine.sip | 1 + src/scripting/python/playlist.sip | 10 ++ src/scripting/python/playlistmanager.sip | 2 + src/scripting/python/scriptinterface.sip | 1 + .../python/songinsertvetolistener.sip | 8 ++ src/scripting/script.cpp | 4 +- 18 files changed, 250 insertions(+), 16 deletions(-) create mode 100644 scripts/remove-duplicates/CMakeLists.txt create mode 100644 scripts/remove-duplicates/icon.png create mode 100644 scripts/remove-duplicates/remove_duplicates.py create mode 100644 scripts/remove-duplicates/script.ini create mode 100644 src/scripting/python/songinsertvetolistener.sip 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 0000000000000000000000000000000000000000..8645dd9e456d262a7be610fba72a3445dec69c42 GIT binary patch literal 6309 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 bX9oWZ+8q(Qy_*Rs00000NkvXXu0mjfQ_b(7 literal 0 HcmV?d00001 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) {