mirror of
https://github.com/clementine-player/Clementine
synced 2024-12-16 03:09:57 +01:00
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
This commit is contained in:
parent
b9b504be30
commit
a883630ab3
scripts
src
@ -7,3 +7,4 @@ function(install_script_files scriptname)
|
||||
endfunction(install_script_files)
|
||||
|
||||
add_subdirectory(digitallyimported-radio)
|
||||
add_subdirectory(remove-duplicates)
|
||||
|
5
scripts/remove-duplicates/CMakeLists.txt
Normal file
5
scripts/remove-duplicates/CMakeLists.txt
Normal file
@ -0,0 +1,5 @@
|
||||
install_script_files(remove-duplicates
|
||||
icon.png
|
||||
remove_duplicates.py
|
||||
script.ini
|
||||
)
|
BIN
scripts/remove-duplicates/icon.png
Normal file
BIN
scripts/remove-duplicates/icon.png
Normal file
Binary file not shown.
After (image error) Size: 6.2 KiB |
70
scripts/remove-duplicates/remove_duplicates.py
Normal file
70
scripts/remove-duplicates/remove_duplicates.py
Normal file
@ -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()
|
9
scripts/remove-duplicates/script.ini
Normal file
9
scripts/remove-duplicates/script.ini
Normal file
@ -0,0 +1,9 @@
|
||||
[Script]
|
||||
name=Duplicate remover
|
||||
description=This script will prevent duplicates being added to your playlists.
|
||||
author=Pawel Bara <keirangtp ( at ) gmail.com>
|
||||
url=http://www.clementine-player.org
|
||||
icon=icon.png
|
||||
|
||||
language=python
|
||||
script_file=remove_duplicates.py
|
@ -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)
|
||||
|
||||
|
@ -1154,3 +1154,9 @@ QFuture<bool> Song::BackgroundSave() const {
|
||||
QFuture<bool> 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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -41,14 +41,15 @@
|
||||
#include "smartplaylists/generatorinserter.h"
|
||||
#include "smartplaylists/generatormimedata.h"
|
||||
|
||||
#include <QtDebug>
|
||||
#include <QApplication>
|
||||
#include <QMimeData>
|
||||
#include <QBuffer>
|
||||
#include <QFileInfo>
|
||||
#include <QDirIterator>
|
||||
#include <QUndoStack>
|
||||
#include <QFileInfo>
|
||||
#include <QMimeData>
|
||||
#include <QMutableListIterator>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QUndoStack>
|
||||
#include <QtDebug>
|
||||
|
||||
#include <boost/bind.hpp>
|
||||
#include <algorithm>
|
||||
@ -122,13 +123,14 @@ Playlist::~Playlist() {
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
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<LibraryPlaylistItem>(this, song_data->songs, row, play_now, enqueue_now);
|
||||
InsertSongItems<LibraryPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
|
||||
else if (song_data->backend && song_data->backend->songs_table() == MagnatuneService::kSongsTable)
|
||||
InsertSongItems<MagnatunePlaylistItem>(this, song_data->songs, row, play_now, enqueue_now);
|
||||
InsertSongItems<MagnatunePlaylistItem>(song_data->songs, row, play_now, enqueue_now);
|
||||
else if (song_data->backend && song_data->backend->songs_table() == JamendoService::kSongsTable)
|
||||
InsertSongItems<JamendoPlaylistItem>(this, song_data->songs, row, play_now, enqueue_now);
|
||||
InsertSongItems<JamendoPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
|
||||
else
|
||||
InsertSongItems<SongPlaylistItem>(this, song_data->songs, row, play_now, enqueue_now);
|
||||
InsertSongItems<SongPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
|
||||
} else if (const RadioMimeData* radio_data = qobject_cast<const RadioMimeData*>(data)) {
|
||||
// Dragged from the Radio pane
|
||||
InsertRadioStations(radio_data->model, radio_data->indexes,
|
||||
@ -783,10 +785,44 @@ void Playlist::MoveItemsWithoutUndo(int start, const QList<int>& 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<Song> vetoed;
|
||||
foreach(SongInsertVetoListener* listener, veto_listeners_) {
|
||||
foreach(const Song& song, listener->AboutToInsertSongs(GetAllSongs(), songs)) {
|
||||
vetoed.append(song);
|
||||
}
|
||||
}
|
||||
|
||||
if(!vetoed.isEmpty()) {
|
||||
QMutableListIterator<PlaylistItemPtr> 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<LibraryPlaylistItem>(this, songs, pos, play_now, enqueue);
|
||||
InsertSongItems<LibraryPlaylistItem>(songs, pos, play_now, enqueue);
|
||||
}
|
||||
|
||||
void Playlist::InsertSongs(const SongList& songs, int pos, bool play_now, bool enqueue) {
|
||||
InsertSongItems<SongPlaylistItem>(this, songs, pos, play_now, enqueue);
|
||||
InsertSongItems<SongPlaylistItem>(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<int>& indicesIn) {
|
||||
// Sort the indices descending because removing elements 'backwards'
|
||||
// is easier - indices don't 'move' in the process.
|
||||
QList<int> 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<SongInsertVetoListener*>(sender()));
|
||||
}
|
||||
|
||||
void Playlist::Shuffle() {
|
||||
layoutAboutToBeChanged();
|
||||
|
||||
|
@ -48,6 +48,19 @@ typedef QMap<int, Qt::Alignment> 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<QUrl>& 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<int>& indices);
|
||||
|
||||
void StopAfter(int row);
|
||||
void ReloadItems(const QList<int>& 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<typename T>
|
||||
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<SongInsertVetoListener*> veto_listeners_;
|
||||
};
|
||||
|
||||
QDataStream& operator <<(QDataStream&, const Playlist*);
|
||||
|
@ -70,6 +70,16 @@ void PlaylistManager::Init(LibraryBackend* library_backend,
|
||||
emit PlaylistManagerInitialized();
|
||||
}
|
||||
|
||||
const QList<Playlist*> PlaylistManager::GetAllPlaylists() const {
|
||||
QList<Playlist*> 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_);
|
||||
|
@ -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<Playlist*> 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()); }
|
||||
|
@ -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
|
||||
|
@ -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<QUrl>& urls, int pos = -1, bool play_now = false, bool enqueue = false);
|
||||
void RemoveItemsWithoutUndo (const QList<int>& indicesIn);
|
||||
|
||||
void StopAfter(int row);
|
||||
void ReloadItems(const QList<int>& 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);
|
||||
|
||||
|
@ -12,6 +12,8 @@ public:
|
||||
Playlist* current() const;
|
||||
Playlist* active() const;
|
||||
|
||||
const QList<Playlist*> GetAllPlaylists() const;
|
||||
|
||||
const QItemSelection& selection(int id) const;
|
||||
const QItemSelection& current_selection() const;
|
||||
const QItemSelection& active_selection() const;
|
||||
|
@ -26,6 +26,7 @@ class ScriptInterface : QObject {
|
||||
CLASS(RadioService),
|
||||
CLASS(ScriptInterface),
|
||||
CLASS(SettingsDialog),
|
||||
CLASS(SongInsertVetoListener),
|
||||
CLASS(SongLoader),
|
||||
CLASS(TaskManager),
|
||||
CLASS(UIInterface),
|
||||
|
8
src/scripting/python/songinsertvetolistener.sip
Normal file
8
src/scripting/python/songinsertvetolistener.sip
Normal file
@ -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;
|
||||
};
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user