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:
Paweł Bara 2011-01-16 23:46:58 +00:00
parent b9b504be30
commit a883630ab3
18 changed files with 250 additions and 16 deletions

View File

@ -7,3 +7,4 @@ function(install_script_files scriptname)
endfunction(install_script_files)
add_subdirectory(digitallyimported-radio)
add_subdirectory(remove-duplicates)

View File

@ -0,0 +1,5 @@
install_script_files(remove-duplicates
icon.png
remove_duplicates.py
script.ini
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View 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()

View 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

View File

@ -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)

View File

@ -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();
}

View File

@ -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);

View File

@ -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();

View File

@ -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*);

View File

@ -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_);

View File

@ -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()); }

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -26,6 +26,7 @@ class ScriptInterface : QObject {
CLASS(RadioService),
CLASS(ScriptInterface),
CLASS(SettingsDialog),
CLASS(SongInsertVetoListener),
CLASS(SongLoader),
CLASS(TaskManager),
CLASS(UIInterface),

View 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;
};

View File

@ -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) {