diff --git a/plugins/classical_work_collection/.idea/classical_work_collection.iml b/plugins/classical_work_collection/.idea/classical_work_collection.iml new file mode 100644 index 00000000..31330016 --- /dev/null +++ b/plugins/classical_work_collection/.idea/classical_work_collection.iml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/plugins/classical_work_collection/__init__.py b/plugins/classical_work_collection/__init__.py new file mode 100644 index 00000000..221d35ef --- /dev/null +++ b/plugins/classical_work_collection/__init__.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 Mark Evens +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +PLUGIN_NAME = u'Classical Work Collections' +PLUGIN_AUTHOR = u'Mark Evens' +PLUGIN_DESCRIPTION = u"""Adds a context menu 'add works to collections', which operates from track or album selections +regardless of whether a file is present. It presents a dialog box showing available work collections. Select the +collection(s)and a confirmation dialog appears. Confirming will add works from all the selected tracks to the +selected collections. +If the plugin 'Classical Extras' has been used then all parent works will also be added.""" +PLUGIN_VERSION = "0.1" +PLUGIN_API_VERSIONS = ["1.3.0", "1.4.0"] +PLUGIN_LICENSE = "GPL-2.0" +PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" + +import locale +import math +from functools import partial +from picard.album import Album +from picard.track import Track +from picard.ui.itemviews import BaseAction, register_album_action, register_track_action +from picard import config, log +from PyQt4 import QtCore, QtGui +from picard.plugins.classical_work_collection.ui_select_collections import Ui_CollectionsDialog +from picard.plugins.classical_work_collection.ui_confirm import Ui_ConfirmDialog +from picard.plugins.classical_work_collection.workscollection import Collection, user_collections, load_user_collections, WorksXmlWebService + +update_list = [] +SUBMISSION_LIMIT = 200 +PROVIDE_ANALYSIS = True + +def add_works_to_list(tracks): + works = [] + for track in tracks: + metadata = track.metadata + if '~cwp_part_levels' in metadata and metadata['~cwp_part_levels'].isdigit(): # Classical Extras plugin + for ind in range(0, int(metadata['~cwp_part_levels']) + 1): + if '~cwp_workid_' + str(ind) in metadata: + work = eval(metadata['~cwp_workid_' + str(ind)]) + if isinstance(work, tuple): + works += list(work) + elif isinstance(work, list): + works += work + elif isinstance(work, basestring): + works.append(work) + else: + if 'musicbrainz_workid' in metadata: # No Classical Extras plugin + works.append(metadata['musicbrainz_workid']) + return works + + +def process_collection(error=None): + if error: + return + if update_list: + collection, work_list, diff = update_list[0] + confirm = ConfirmDialog(len(work_list), len(diff), collection.name) + if PROVIDE_ANALYSIS: + confirm.get_collection_members(confirm_dialog, confirm, collection, collection.id, collection.size, work_list) + else: + confirm_dialog(confirm, collection, None, work_list) + del update_list[0] + + +def confirm_dialog(confirm, collection, member_set, work_list): + if PROVIDE_ANALYSIS: + diff = set(work_list) - member_set + if len(diff) > 0: + confirm.ui.label_2.setText(str(len(diff)) + ' new works, from ' + str(len(set(work_list))) + ' selected, will be added.') + else: + confirm.ui.label_2.setText('All ' + str(len(set(work_list))) + ' selected works are already in the collection - no more will be added.') + else: + diff = set(work_list) + confirm.ui.label.setText('Adding ' + str(len(diff)) + ' works to the collection "' + collection.name + '"') + confirm.ui.label_2.setText('(Some may already be in the collection)') + confirmation = confirm.exec_() + if confirmation == 1: + if diff: + collection.add_works(diff, process_collection, SUBMISSION_LIMIT) + return + else: + log.debug('%s: nothing new to add', PLUGIN_NAME) + elif confirmation == 0: + pass + else: + log.error('%s: Error in dialog', PLUGIN_NAME) + process_collection() # check if there is anything left to do + +class AddWorkCollection(BaseAction): + NAME = 'Add works to collections' + + def callback(self, objs): + global SUBMISSION_LIMIT + global PROVIDE_ANALYSIS + work_list = [] + selected_albums = [a for a in objs if type(a) == Album] + for album in selected_albums: + work_list += add_works_to_list(album.tracks) + selected_tracks = [t for t in objs if type(t) == Track] + if selected_tracks: + work_list += add_works_to_list(selected_tracks) + dialog = SelectCollectionsDialog() + # Note: this loads the collection objects, which may result in a slight delay before they appear in the dialog + result = dialog.exec_() + if result == 1: # QDialog.Accepted + SUBMISSION_LIMIT = dialog.ui.max_works.value() + PROVIDE_ANALYSIS = dialog.ui.provide_analysis.isChecked() + # log.error('constants set: SUBMISSION_LIMIT = %s, PROVIDE_ANALYSIS = %s', SUBMISSION_LIMIT, PROVIDE_ANALYSIS) + if dialog.ui.collection_list.selectedItems(): + for item in dialog.ui.collection_list.selectedItems(): + id = item.data(32) + name = item.data(33) + size = item.data(34) + collection = Collection(id, name, size) # user_collections[id] + if set(work_list) & collection.pending: + return + diff = set(work_list) - collection.works + update_list.append((collection, work_list, diff)) + else: + confirm = ConfirmDialog(0, 0, 'None') + confirm.ui.label.setText('No collections selected') + confirm.ui.label_2.setText('') + confirm.exec_() + elif result == 0: + pass + else: + log.error('%s: Error in dialog', PLUGIN_NAME) + process_collection() + + + + +class SelectCollectionsDialog(QtGui.QDialog): + + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.ui = Ui_CollectionsDialog() + self.ui.setupUi(self) + self.ui.buttonBox.accepted.connect(self.accept) + self.ui.buttonBox.rejected.connect(self.reject) + self.ui.max_works.setValue(200) + self.ui.provide_analysis.setChecked(True) + load_user_collections(self.display_collections) + + def display_collections(self): + collections = self.ui.collection_list # collection_list is a QListWidget + + for id, collection in sorted(user_collections.iteritems(), + key=lambda k_v: + (locale.strxfrm(k_v[1].name.encode('utf-8')), k_v[0])): + + item = QtGui.QListWidgetItem() + item.setText(collection.name + ' (' + str(collection.size) + ')') + item.setData(32, id) # role #32 is first available user role + item.setData(33, collection.name) + item.setData(34, collection.size) + collections.addItem(item) + + +class ConfirmDialog(QtGui.QDialog): + + def __init__(self, num_works, num_diff, selected_collection, parent=None): + QtGui.QDialog.__init__(self, parent) + self.ui = Ui_ConfirmDialog() + self.ui.setupUi(self) + self.ui.buttonBox.accepted.connect(self.accept) + self.ui.buttonBox.rejected.connect(self.reject) + self.member_set = set() + + def get_collection_members(self, callback, confirm, collection, id, size, work_list): + # log.error(' in get_collection_members. work_list =') + # log.error(work_list) + works_xmlws = WorksXmlWebService() + limit = 100 + if isinstance(size, basestring): + if size.isdigit(): + size = int(size) + else: + return + chunks = int(math.ceil(float(size) / float(limit))) + for chunk in range(0, chunks): + # log.error('chunk %s of %s', chunk, chunks) + offset = chunk * limit + # log.error('offset = %s', offset) + if chunk == chunks - 1: + chunk_size = size - offset + else: + chunk_size = limit + if chunk == 0: # Lookups appear to be on a LIFO basis (?!*+$!) + end = True + else: + end = False + # log.error('call get_collection') + works_xmlws.get_collection(id, partial(self.add_collection_members, callback, confirm, collection, work_list, end, chunk_size), limit, offset) + + def add_collection_members(self, callback, confirm, collection, work_list, end, chunk_size, document, reply, error): + tagger = QtCore.QObject.tagger + if error: + tagger.window.set_statusbar_message( + N_("Error loading collections: %(error)s"), + {'error': unicode(reply.errorString())}, + echo=log.error + ) + return + node = document.metadata[0].collection + if node: + # log.error('self.member_set before = %r', self.member_set) + self.member_set = self.member_set | self.process_node(node[0], chunk_size) + # log.error('self.member_set after = %r', self.member_set) + # log.error('end = %r, len = %s', end, len(self.member_set)) + else: + return + if end: + self.ui.label.setText('Collection "' + collection.name + '" has ' + str(len(self.member_set)) + ' existing members') + callback(confirm, collection, self.member_set, work_list) + + def process_node(self, node, chunk_size): + work_set = set() + if node.attribs.get(u"entity_type") == u"work": + # name = node.name[0].text + size = min(int(node.work_list[0].count), chunk_size) + for work_item in range(0, size): + work = node.work_list[0].work[work_item] + work_set.add(work.id) + return work_set + + +work_collection = AddWorkCollection() +register_album_action(work_collection) +register_track_action(work_collection) diff --git a/plugins/classical_work_collection/confirm.ui b/plugins/classical_work_collection/confirm.ui new file mode 100644 index 00000000..a0fe333f --- /dev/null +++ b/plugins/classical_work_collection/confirm.ui @@ -0,0 +1,107 @@ + + + ConfirmDialog + + + + 0 + 0 + 440 + 130 + + + + Dialog + + + + + 20 + 20 + 391 + 71 + + + + Please confirm:- + + + + + 10 + 30 + 371 + 16 + + + + Adding xxxxxx works to the collection "Collection" + + + + + + 10 + 50 + 381 + 16 + + + + All xxxxx selected works are already in the collection - no more will be added. + + + + + + + 10 + 90 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + buttonBox + accepted() + ConfirmDialog + accept() + + + 238 + 94 + + + 157 + 274 + + + + + buttonBox + rejected() + ConfirmDialog + reject() + + + 306 + 100 + + + 286 + 274 + + + + + diff --git a/plugins/classical_work_collection/readme.md b/plugins/classical_work_collection/readme.md new file mode 100644 index 00000000..64a3b538 --- /dev/null +++ b/plugins/classical_work_collection/readme.md @@ -0,0 +1,13 @@ +This is the documentation for version 0.1 "Classical Work Collections". There may be beta versions later than this - check [my github site](https://github.com/MetaTunes/picard-plugins/releases) for newer releases. + +This plugin adds a context menu 'add works to collections', which operates from track or album selections +regardless of whether a file is present. It presents a dialog box showing available work collections. Select the +collection(s)and a confirmation dialog appears. Confirming will add works from all the selected tracks to the +selected collections. +If the plugin 'Classical Extras' has been used then all parent works will also be added. + +The first dialog box gives options: +* Maximum number of works to be added at a time: The default is 200. More than this may result in "URI too large" error (even though the MB documentation says 400 should work). If a "URI too large" error occurs, reduce the limit." +* Provide analysis of existing collection and new works before updating: Selecting this (the default) will provide information about how many of the selected works are already in the selected collection(s) and only new works will be submitted. Deselecting it will result in all selected works being submitted, but will almost certainly be faster as existing works can only be looked up at the rate of 100 per sec. + +Assuming the default on the second option above, the second dialog box (one per collection) will provide the analysis described. diff --git a/plugins/classical_work_collection/select_collections.ui b/plugins/classical_work_collection/select_collections.ui new file mode 100644 index 00000000..c2c2e0fd --- /dev/null +++ b/plugins/classical_work_collection/select_collections.ui @@ -0,0 +1,204 @@ + + + CollectionsDialog + + + + 0 + 0 + 408 + 334 + + + + Dialog + + + + + 40 + 280 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + 0 + 10 + 401 + 161 + + + + Select collections:- + + + + + 10 + 20 + 371 + 101 + + + + QAbstractItemView::MultiSelection + + + + + + 10 + 130 + 431 + 16 + + + + Highlight the collections into which to add the works from the selected tracks + + + + + + + 310 + 170 + 71 + 22 + + + + 1 + + + 400 + + + + + + 20 + 170 + 281 + 16 + + + + Maximum number of works to be added at a time + + + + + + 20 + 200 + 261 + 16 + + + + (multiple submissions will be generated automatically + + + + + + 20 + 210 + 291 + 16 + + + + if the total number of works to be added exceeds this max) + + + + + + 20 + 230 + 361 + 31 + + + + Qt::RightToLeft + + + Provide analysis of existing collection and new works before updating? + + + + + + 20 + 250 + 371 + 16 + + + + (Faster if unchecked, but less informative) + + + + + + 20 + 180 + 261 + 16 + + + + More than 200 may result in "URI too large" error + + + + + + + buttonBox + accepted() + CollectionsDialog + accept() + + + 258 + 264 + + + 157 + 274 + + + + + buttonBox + rejected() + CollectionsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/classical_work_collection/ui_confirm.py b/plugins/classical_work_collection/ui_confirm.py new file mode 100644 index 00000000..0c37616c --- /dev/null +++ b/plugins/classical_work_collection/ui_confirm.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'C:\Users\Mark\Documents\Mark's documents\Music\Picard\Classical Works Collection development\classical_work_collection\confirm.ui' +# +# Created: Wed May 16 11:07:22 2018 +# by: PyQt4 UI code generator 4.10 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) + +class Ui_ConfirmDialog(object): + def setupUi(self, ConfirmDialog): + ConfirmDialog.setObjectName(_fromUtf8("ConfirmDialog")) + ConfirmDialog.resize(440, 130) + self.groupBox = QtGui.QGroupBox(ConfirmDialog) + self.groupBox.setGeometry(QtCore.QRect(20, 20, 391, 71)) + self.groupBox.setObjectName(_fromUtf8("groupBox")) + self.label = QtGui.QLabel(self.groupBox) + self.label.setGeometry(QtCore.QRect(10, 30, 371, 16)) + self.label.setObjectName(_fromUtf8("label")) + self.label_2 = QtGui.QLabel(self.groupBox) + self.label_2.setGeometry(QtCore.QRect(10, 50, 381, 16)) + self.label_2.setObjectName(_fromUtf8("label_2")) + self.buttonBox = QtGui.QDialogButtonBox(ConfirmDialog) + self.buttonBox.setGeometry(QtCore.QRect(10, 90, 341, 32)) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok) + self.buttonBox.setObjectName(_fromUtf8("buttonBox")) + + self.retranslateUi(ConfirmDialog) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("accepted()")), ConfirmDialog.accept) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("rejected()")), ConfirmDialog.reject) + QtCore.QMetaObject.connectSlotsByName(ConfirmDialog) + + def retranslateUi(self, ConfirmDialog): + ConfirmDialog.setWindowTitle(_translate("ConfirmDialog", "Dialog", None)) + self.groupBox.setTitle(_translate("ConfirmDialog", "Please confirm:-", None)) + self.label.setText(_translate("ConfirmDialog", "Adding xxxxxx works to the collection \"Collection\"", None)) + self.label_2.setText(_translate("ConfirmDialog", "All xxxxx selected works are already in the collection - no more will be added.", None)) + diff --git a/plugins/classical_work_collection/ui_select_collections.py b/plugins/classical_work_collection/ui_select_collections.py new file mode 100644 index 00000000..55086b16 --- /dev/null +++ b/plugins/classical_work_collection/ui_select_collections.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'C:\Users\Mark\Documents\Mark's documents\Music\Picard\Classical Works Collection development\classical_work_collection\select_collections.ui' +# +# Created: Thu May 17 14:20:56 2018 +# by: PyQt4 UI code generator 4.10 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) + +class Ui_CollectionsDialog(object): + def setupUi(self, CollectionsDialog): + CollectionsDialog.setObjectName(_fromUtf8("CollectionsDialog")) + CollectionsDialog.resize(408, 334) + self.buttonBox = QtGui.QDialogButtonBox(CollectionsDialog) + self.buttonBox.setGeometry(QtCore.QRect(40, 280, 341, 32)) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok) + self.buttonBox.setObjectName(_fromUtf8("buttonBox")) + self.groupBox = QtGui.QGroupBox(CollectionsDialog) + self.groupBox.setGeometry(QtCore.QRect(0, 10, 401, 161)) + self.groupBox.setObjectName(_fromUtf8("groupBox")) + self.collection_list = QtGui.QListWidget(self.groupBox) + self.collection_list.setGeometry(QtCore.QRect(10, 20, 371, 101)) + self.collection_list.setSelectionMode(QtGui.QAbstractItemView.MultiSelection) + self.collection_list.setObjectName(_fromUtf8("collection_list")) + self.label = QtGui.QLabel(self.groupBox) + self.label.setGeometry(QtCore.QRect(10, 130, 431, 16)) + self.label.setObjectName(_fromUtf8("label")) + self.max_works = QtGui.QSpinBox(CollectionsDialog) + self.max_works.setGeometry(QtCore.QRect(310, 170, 71, 22)) + self.max_works.setMinimum(1) + self.max_works.setMaximum(400) + self.max_works.setObjectName(_fromUtf8("max_works")) + self.label_2 = QtGui.QLabel(CollectionsDialog) + self.label_2.setGeometry(QtCore.QRect(20, 170, 281, 16)) + self.label_2.setObjectName(_fromUtf8("label_2")) + self.label_3 = QtGui.QLabel(CollectionsDialog) + self.label_3.setGeometry(QtCore.QRect(20, 200, 261, 16)) + self.label_3.setObjectName(_fromUtf8("label_3")) + self.label_4 = QtGui.QLabel(CollectionsDialog) + self.label_4.setGeometry(QtCore.QRect(20, 210, 291, 16)) + self.label_4.setObjectName(_fromUtf8("label_4")) + self.provide_analysis = QtGui.QCheckBox(CollectionsDialog) + self.provide_analysis.setGeometry(QtCore.QRect(20, 230, 361, 31)) + self.provide_analysis.setLayoutDirection(QtCore.Qt.RightToLeft) + self.provide_analysis.setObjectName(_fromUtf8("provide_analysis")) + self.label_5 = QtGui.QLabel(CollectionsDialog) + self.label_5.setGeometry(QtCore.QRect(20, 250, 371, 16)) + self.label_5.setObjectName(_fromUtf8("label_5")) + self.label_7 = QtGui.QLabel(CollectionsDialog) + self.label_7.setGeometry(QtCore.QRect(20, 180, 261, 16)) + self.label_7.setObjectName(_fromUtf8("label_7")) + + self.retranslateUi(CollectionsDialog) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("accepted()")), CollectionsDialog.accept) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("rejected()")), CollectionsDialog.reject) + QtCore.QMetaObject.connectSlotsByName(CollectionsDialog) + + def retranslateUi(self, CollectionsDialog): + CollectionsDialog.setWindowTitle(_translate("CollectionsDialog", "Dialog", None)) + self.groupBox.setTitle(_translate("CollectionsDialog", "Select collections:-", None)) + self.label.setText(_translate("CollectionsDialog", "Highlight the collections into which to add the works from the selected tracks", None)) + self.label_2.setText(_translate("CollectionsDialog", "Maximum number of works to be added at a time", None)) + self.label_3.setText(_translate("CollectionsDialog", "(multiple submissions will be generated automatically", None)) + self.label_4.setText(_translate("CollectionsDialog", "if the total number of works to be added exceeds this max)", None)) + self.provide_analysis.setText(_translate("CollectionsDialog", "Provide analysis of existing collection and new works before updating? ", None)) + self.label_5.setText(_translate("CollectionsDialog", "(Faster if unchecked, but less informative)", None)) + self.label_7.setText(_translate("CollectionsDialog", "More than 200 may result in \"URI too large\" error", None)) + diff --git a/plugins/classical_work_collection/workscollection.py b/plugins/classical_work_collection/workscollection.py new file mode 100644 index 00000000..414c3e43 --- /dev/null +++ b/plugins/classical_work_collection/workscollection.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# Copyright (C) 2013 Michael Wiencek +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- + +# This module is a modified version of collection.py, designed to operate with works rather than releases +# and specifically as part of the classical_work_collection plugin +# It is part of a companion plugin for the Classical Extras plugin +# Modifications are Copyright (C) 2018 Mark Evens + +from functools import partial +from PyQt4 import QtCore +from picard import config, log +from picard.webservice import XmlWebService + +user_collections = {} + +class WorksXmlWebService(XmlWebService): + + def __init__(self, submission_limit=200, parent=None): + XmlWebService.__init__(self, parent) + self.sub_limit = submission_limit + + def collection_request(self, id, members): + while members: + ids = ";".join(members if len(members) <= self.sub_limit else members[:self.sub_limit]) + members = members[self.sub_limit:] + yield "/ws/2/collection/%s/works/%s" % (id, ids) + + def get_collection(self, id, handler, limit=100, offset=0): + host, port = config.setting['server_host'], config.setting['server_port'] + path = "/ws/2/collection" + queryargs = None + if id is not None: + path += "/%s/works" % (id) + queryargs = {} + queryargs["limit"] = limit + queryargs["offset"] = offset + return self.get(host, port, path, handler, priority=True, important=True, + mblogin=True, queryargs=queryargs) + + +class Collection(QtCore.QObject): + + def __init__(self, id, name, size): + self.id = id + self.name = name + self.pending = set() + self.size = int(size) + self.works = set() + + def __repr__(self): + return '' % (self.name, self.id) + + def add_works(self, members, callback, submission_limit): + works_xmlws = WorksXmlWebService(submission_limit) + members = members - self.pending + if members: + self.pending.update(members) + host, port = config.setting['server_host'], config.setting['server_port'] + for path in works_xmlws.collection_request(self.id, list(members)): + works_xmlws.put(host, port, path, "", partial(self._add_finished, members, callback), + queryargs=works_xmlws._get_client_queryarg()) + + def remove_works(self, members, callback): + works_xmlws = WorksXmlWebService() + members = members - self.pending + if members: + self.pending.update(members) + works_xmlws.tagger.works_xmlws.delete_from_collection(self.id, list(members), + partial(self._remove_finished, members, callback)) + + def _add_finished(self, ids, callback, document, reply, error): + tagger = QtCore.QObject.tagger + self.pending.difference_update(ids) + if not error: + count = len(ids) + self.works.update(ids) + self.size += count + mparms = { + 'count': count, + 'name': self.name + } + log.debug('Added %(count)i works to collection "%(name)s"' % mparms) + self.tagger.window.set_statusbar_message( + ungettext('Added %(count)i work to collection "%(name)s"', + 'Added %(count)i works to collection "%(name)s"', + count), + mparms, + translate=None, + echo=None + ) + if callback: + callback() + else: + log.error('Error in collection update. May be that submission is too large.') + tagger.window.set_statusbar_message( + N_("Error in collection update: May be that submission is too large.") + ) + if callback: + callback(error) + + def _remove_finished(self, ids, callback, document, reply, error): + self.pending.difference_update(ids) + if not error: + count = len(ids) + self.works.difference_update(ids) + self.size -= count + if callback: + callback() + mparms = { + 'count': count, + 'name': self.name + } + log.debug('Removed %(count)i works from collection "%(name)s"' % + mparms) + self.tagger.window.set_statusbar_message( + ungettext('Removed %(count)i work from collection "%(name)s"', + 'Removed %(count)i works from collection "%(name)s"', + count), + mparms, + translate=None, + echo=None + ) + + +def load_user_collections(callback=None): + tagger = QtCore.QObject.tagger + + def request_finished(document, reply, error): + if error: + tagger.window.set_statusbar_message( + N_("Error loading collections: %(error)s"), + {'error': unicode(reply.errorString())}, + echo=log.error + ) + return + collection_list = document.metadata[0].collection_list[0] + if "collection" in collection_list.children: + new_collections = process_node(collection_list) + for id in set(user_collections.iterkeys()) - new_collections: + del user_collections[id] + if callback: + callback() + + if tagger.xmlws.oauth_manager.is_authorized(): + tagger.xmlws.get_collection_list(partial(request_finished)) + else: + user_collections.clear() + + +def process_node(collection_list): + new_collections = set() + for node in collection_list.collection: + if node.attribs.get(u"entity_type") != u"work": + continue + new_collections.add(node.id) + collection = user_collections.get(node.id) + if collection is None: + user_collections[node.id] = Collection(node.id, node.name[0].text, node.work_list[0].count) + else: + collection.name = node.name[0].text + collection.size = int(node.work_list[0].count) + return new_collections