Beispiel #1
0
    def __init__(self, picard_args, unparsed_args, localedir, autoupdate):

        # Use the new fusion style from PyQt5 for a modern and consistent look
        # across all OSes.
        if not IS_MACOS:
            self.setStyle('Fusion')

        # Set the WM_CLASS to 'MusicBrainz-Picard' so desktop environments
        # can use it to look up the app
        super().__init__(['MusicBrainz-Picard'] + unparsed_args)
        self.__class__.__instance = self
        config._setup(self, picard_args.config_file)

        super().setStyleSheet(
            'QGroupBox::title { /* PICARD-1206, Qt bug workaround */ }')

        self._cmdline_files = picard_args.FILE
        self.autoupdate_enabled = autoupdate
        self._no_restore = picard_args.no_restore
        self._no_plugins = picard_args.no_plugins

        self.set_log_level(config.setting['log_verbosity'])

        if picard_args.debug or "PICARD_DEBUG" in os.environ:
            self.set_log_level(logging.DEBUG)

        # FIXME: Figure out what's wrong with QThreadPool.globalInstance().
        # It's a valid reference, but its start() method doesn't work.
        self.thread_pool = QtCore.QThreadPool(self)

        # Provide a separate thread pool for operations that should not be
        # delayed by longer background processing tasks, e.g. because the user
        # expects instant feedback instead of waiting for a long list of
        # operations to finish.
        self.priority_thread_pool = QtCore.QThreadPool(self)

        # Use a separate thread pool for file saving, with a thread count of 1,
        # to avoid race conditions in File._save_and_rename.
        self.save_thread_pool = QtCore.QThreadPool(self)
        self.save_thread_pool.setMaxThreadCount(1)

        if not IS_WIN:
            # Set up signal handling
            # It's not possible to call all available functions from signal
            # handlers, therefore we need to set up a QSocketNotifier to listen
            # on a socket. Sending data through a socket can be done in a
            # signal handler, so we use the socket to notify the application of
            # the signal.
            # This code is adopted from
            # https://qt-project.org/doc/qt-4.8/unix-signals.html

            # To not make the socket module a requirement for the Windows
            # installer, import it here and not globally
            import socket
            self.signalfd = socket.socketpair(socket.AF_UNIX,
                                              socket.SOCK_STREAM, 0)

            self.signalnotifier = QtCore.QSocketNotifier(
                self.signalfd[1].fileno(), QtCore.QSocketNotifier.Read, self)
            self.signalnotifier.activated.connect(self.sighandler)

            signal.signal(signal.SIGHUP, self.signal)
            signal.signal(signal.SIGINT, self.signal)
            signal.signal(signal.SIGTERM, self.signal)

        if IS_MACOS:
            # On macOS it is not common that the global menu shows icons
            self.setAttribute(QtCore.Qt.AA_DontShowIconsInMenus)

        # Setup logging
        log.debug("Starting Picard from %r", os.path.abspath(__file__))
        log.debug("Platform: %s %s %s", platform.platform(),
                  platform.python_implementation(), platform.python_version())
        log.debug("Versions: %s", versions.as_string())
        log.debug("Configuration file path: %r", config.config.fileName())

        log.debug("User directory: %r", os.path.abspath(USER_DIR))

        # for compatibility with pre-1.3 plugins
        QtCore.QObject.tagger = self
        QtCore.QObject.config = config
        QtCore.QObject.log = log

        check_io_encoding()

        # Must be before config upgrade because upgrade dialogs need to be
        # translated
        setup_gettext(localedir, config.setting["ui_language"], log.debug)

        upgrade_config(config.config)

        self.webservice = WebService()
        self.mb_api = MBAPIHelper(self.webservice)
        self.acoustid_api = AcoustIdAPIHelper(self.webservice)

        load_user_collections()

        # Initialize fingerprinting
        self._acoustid = acoustid.AcoustIDClient()
        self._acoustid.init()

        # Load plugins
        self.pluginmanager = PluginManager()
        if not self._no_plugins:
            if IS_FROZEN:
                self.pluginmanager.load_plugins_from_directory(
                    os.path.join(os.path.dirname(sys.argv[0]), "plugins"))
            else:
                mydir = os.path.dirname(os.path.abspath(__file__))
                self.pluginmanager.load_plugins_from_directory(
                    os.path.join(mydir, "plugins"))

            if not os.path.exists(USER_PLUGIN_DIR):
                os.makedirs(USER_PLUGIN_DIR)
            self.pluginmanager.load_plugins_from_directory(USER_PLUGIN_DIR)

        self.acoustidmanager = AcoustIDManager()
        self.browser_integration = BrowserIntegration()

        self.files = {}
        self.clusters = ClusterList()
        self.albums = {}
        self.release_groups = {}
        self.mbid_redirects = {}
        self.unclustered_files = UnclusteredFiles()
        self.nats = None
        self.window = MainWindow()
        self.exit_cleanup = []
        self.stopping = False

        # Load release version information
        if self.autoupdate_enabled:
            self.updatecheckmanager = UpdateCheckManager(parent=self.window)
Beispiel #2
0
 def setUp(self):
     self.config = {'server_host': "mb.org", "server_port": 443}
     config.setting = self.config.copy()
     self.ws = MagicMock(auto_spec=WebService)
     self.api = MBAPIHelper(self.ws)
Beispiel #3
0
    def __init__(self, picard_args, unparsed_args, localedir, autoupdate):
        # Set the WM_CLASS to 'MusicBrainz-Picard' so desktop environments
        # can use it to look up the app
        QtWidgets.QApplication.__init__(self,
                                        ['MusicBrainz-Picard'] + unparsed_args)
        self.__class__.__instance = self
        config._setup(self, picard_args.config_file)

        # Allow High DPI Support
        self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
        self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)

        self._cmdline_files = picard_args.FILE
        self._autoupdate = autoupdate
        self._debug = False

        # FIXME: Figure out what's wrong with QThreadPool.globalInstance().
        # It's a valid reference, but its start() method doesn't work.
        self.thread_pool = QtCore.QThreadPool(self)

        # Use a separate thread pool for file saving, with a thread count of 1,
        # to avoid race conditions in File._save_and_rename.
        self.save_thread_pool = QtCore.QThreadPool(self)
        self.save_thread_pool.setMaxThreadCount(1)

        if not sys.platform == "win32":
            # Set up signal handling
            # It's not possible to call all available functions from signal
            # handlers, therefore we need to set up a QSocketNotifier to listen
            # on a socket. Sending data through a socket can be done in a
            # signal handler, so we use the socket to notify the application of
            # the signal.
            # This code is adopted from
            # https://qt-project.org/doc/qt-4.8/unix-signals.html

            # To not make the socket module a requirement for the Windows
            # installer, import it here and not globally
            import socket
            self.signalfd = socket.socketpair(socket.AF_UNIX,
                                              socket.SOCK_STREAM, 0)

            self.signalnotifier = QtCore.QSocketNotifier(
                self.signalfd[1].fileno(), QtCore.QSocketNotifier.Read, self)
            self.signalnotifier.activated.connect(self.sighandler)

            signal.signal(signal.SIGHUP, self.signal)
            signal.signal(signal.SIGINT, self.signal)
            signal.signal(signal.SIGTERM, self.signal)

        # Setup logging
        self.debug(picard_args.debug or "PICARD_DEBUG" in os.environ)
        log.debug("Starting Picard from %r", os.path.abspath(__file__))
        log.debug("Platform: %s %s %s", platform.platform(),
                  platform.python_implementation(), platform.python_version())
        log.debug("Versions: %s", versions.as_string())
        log.debug("Configuration file path: %r", config.config.fileName())

        # TODO remove this before the final release
        if sys.platform == "win32":
            olduserdir = "~\\Local Settings\\Application Data\\MusicBrainz Picard"
        else:
            olduserdir = "~/.picard"
        olduserdir = os.path.expanduser(olduserdir)
        if os.path.isdir(olduserdir):
            log.info("Moving %r to %r", olduserdir, USER_DIR)
            try:
                shutil.move(olduserdir, USER_DIR)
            except:
                pass
        log.debug("User directory: %r", os.path.abspath(USER_DIR))

        # for compatibility with pre-1.3 plugins
        QtCore.QObject.tagger = self
        QtCore.QObject.config = config
        QtCore.QObject.log = log

        check_io_encoding()

        # Must be before config upgrade because upgrade dialogs need to be
        # translated
        setup_gettext(localedir, config.setting["ui_language"], log.debug)

        upgrade_config()

        self.webservice = WebService()
        self.mb_api = MBAPIHelper(self.webservice)
        self.acoustid_api = AcoustIdAPIHelper(self.webservice)

        load_user_collections()

        # Initialize fingerprinting
        self._acoustid = acoustid.AcoustIDClient()
        self._acoustid.init()

        # Load plugins
        self.pluginmanager = PluginManager()
        if hasattr(sys, "frozen"):
            self.pluginmanager.load_plugindir(
                os.path.join(os.path.dirname(sys.argv[0]), "plugins"))
        else:
            mydir = os.path.dirname(os.path.abspath(__file__))
            self.pluginmanager.load_plugindir(os.path.join(mydir, "plugins"))
            self.pluginmanager.load_plugindir(
                os.path.join(mydir, os.pardir, "contrib", "plugins"))

        if not os.path.exists(USER_PLUGIN_DIR):
            os.makedirs(USER_PLUGIN_DIR)
        self.pluginmanager.load_plugindir(USER_PLUGIN_DIR)
        self.pluginmanager.query_available_plugins()

        self.acoustidmanager = AcoustIDManager()
        self.browser_integration = BrowserIntegration()

        self.files = {}
        self.clusters = ClusterList()
        self.albums = {}
        self.release_groups = {}
        self.mbid_redirects = {}
        self.unmatched_files = UnmatchedFiles()
        self.nats = None
        self.window = MainWindow()
        self.exit_cleanup = []
        self.stopping = False
Beispiel #4
0
 def setUp(self):
     self.config = {'server_host': "mb.org", "server_port": 443}
     config.setting = self.config.copy()
     self.ws = MagicMock(auto_spec=WebService)
     self.api = MBAPIHelper(self.ws)
Beispiel #5
0
class MBAPITest(PicardTestCase):

    def setUp(self):
        self.config = {'server_host': "mb.org", "server_port": 443}
        config.setting = self.config.copy()
        self.ws = MagicMock(auto_spec=WebService)
        self.api = MBAPIHelper(self.ws)

    def _test_ws_function_args(self, ws_function):
        self.assertGreater(ws_function.call_count, 0)
        self.assertEqual(ws_function.call_args[0][0], self.config['server_host'])
        self.assertEqual(ws_function.call_args[0][1], self.config['server_port'])
        self.assertIn("/ws/2/", ws_function.call_args[0][2])

    def assertInPath(self, ws_function, path):
        self.assertIn(path, ws_function.call_args[0][2])

    def assertNotInPath(self, ws_function, path):
        self.assertNotIn(path, ws_function.call_args[0][2])

    def assertInQuery(self, ws_function, argname, value=None):
        query_args = ws_function.call_args[1]['queryargs']
        self.assertIn(argname, query_args)
        self.assertEqual(value, query_args[argname])

    def _test_inc_args(self, ws_function, arg_list):
        self.assertInQuery(self.ws.get, 'inc', "+".join(arg_list))

    def test_get_release(self):
        inc_args_list = ['test']
        self.api.get_release_by_id("1", None, inc=inc_args_list)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "/release/1")
        self._test_inc_args(self.ws.get, inc_args_list)

    def test_get_track(self):
        inc_args_list = ['test']
        self.api.get_track_by_id("1", None, inc=inc_args_list)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "/recording/1")
        self._test_inc_args(self.ws.get, inc_args_list)

    def test_get_collection(self):
        inc_args_list = ["releases", "artist-credits", "media"]
        self.api.get_collection("1", None)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "collection")
        self.assertInPath(self.ws.get, "1/releases")
        self._test_inc_args(self.ws.get, inc_args_list)

    def test_get_collection_list(self):
        self.api.get_collection_list(None)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "collection")
        self.assertNotInPath(self.ws.get, "releases")

    def test_put_collection(self):
        self.api.put_to_collection("1", ["1", "2", "3"], None)
        self._test_ws_function_args(self.ws.put)
        self.assertInPath(self.ws.put, "collection/1/releases/1;2;3")

    def test_delete_collection(self):
        self.api.delete_from_collection("1", ["1", "2", "3", "4"] * 200, None)
        collection_string = ";".join(["1", "2", "3", "4"] * 100)
        self._test_ws_function_args(self.ws.delete)
        self.assertInPath(self.ws.delete, "collection/1/releases/" + collection_string)
        self.assertNotInPath(self.ws.delete, collection_string + ";" + collection_string)
        self.assertEqual(self.ws.delete.call_count, 2)
Beispiel #6
0
class MBAPITest(unittest.TestCase):
    def setUp(self):
        self.config = {'server_host': "mb.org", "server_port": 443}
        config.setting = self.config.copy()
        self.ws = MagicMock(auto_spec=WebService)
        self.api = MBAPIHelper(self.ws)

    def _test_ws_function_args(self, ws_function):
        self.assertGreater(ws_function.call_count, 0)
        self.assertEqual(ws_function.call_args[0][0],
                         self.config['server_host'])
        self.assertEqual(ws_function.call_args[0][1],
                         self.config['server_port'])
        self.assertIn("/ws/2/", ws_function.call_args[0][2])

    def assertInPath(self, ws_function, path):
        self.assertIn(path, ws_function.call_args[0][2])

    def assertNotInPath(self, ws_function, path):
        self.assertNotIn(path, ws_function.call_args[0][2])

    def assertInQuery(self, ws_function, argname, value=None):
        query_args = ws_function.call_args[1]['queryargs']
        self.assertIn(argname, query_args)
        self.assertEqual(value, query_args[argname])

    def _test_inc_args(self, ws_function, arg_list):
        self.assertInQuery(self.ws.get, 'inc', "+".join(arg_list))

    def test_get_release(self):
        inc_args_list = ['test']
        self.api.get_release_by_id("1", None, inc=inc_args_list)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "/release/1")
        self._test_inc_args(self.ws.get, inc_args_list)

    def test_get_track(self):
        inc_args_list = ['test']
        self.api.get_track_by_id("1", None, inc=inc_args_list)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "/recording/1")
        self._test_inc_args(self.ws.get, inc_args_list)

    def test_get_collection(self):
        inc_args_list = ["releases", "artist-credits", "media"]
        self.api.get_collection("1", None)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "collection")
        self.assertInPath(self.ws.get, "1/releases")
        self._test_inc_args(self.ws.get, inc_args_list)

    def test_get_collection_list(self):
        self.api.get_collection_list(None)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "collection")
        self.assertNotInPath(self.ws.get, "releases")

    def test_put_collection(self):
        self.api.put_to_collection("1", ["1", "2", "3"], None)
        self._test_ws_function_args(self.ws.put)
        self.assertInPath(self.ws.put, "collection/1/releases/1;2;3")

    def test_delete_collection(self):
        self.api.delete_from_collection("1", ["1", "2", "3", "4"] * 200, None)
        collection_string = ";".join(["1", "2", "3", "4"] * 100)
        self._test_ws_function_args(self.ws.delete)
        self.assertInPath(self.ws.delete,
                          "collection/1/releases/" + collection_string)
        self.assertNotInPath(self.ws.delete,
                             collection_string + ";" + collection_string)
        self.assertEqual(self.ws.delete.call_count, 2)
Beispiel #7
0
 def setUp(self):
     super().setUp()
     self.config = {'server_host': "mb.org", "server_port": 443}
     self.set_config_values(self.config)
     self.ws = MagicMock(auto_spec=WebService)
     self.api = MBAPIHelper(self.ws)
Beispiel #8
0
    def __init__(self, picard_args, unparsed_args, localedir, autoupdate):

        super().__init__(sys.argv)
        self.__class__.__instance = self
        setup_config(self, picard_args.config_file)
        config = get_config()
        theme.setup(self)

        self._cmdline_files = picard_args.FILE
        self.autoupdate_enabled = autoupdate
        self._no_restore = picard_args.no_restore
        self._no_plugins = picard_args.no_plugins

        self.set_log_level(config.setting['log_verbosity'])

        if picard_args.debug or "PICARD_DEBUG" in os.environ:
            self.set_log_level(logging.DEBUG)

        # Default thread pool
        self.thread_pool = ThreadPoolExecutor()

        # Provide a separate thread pool for operations that should not be
        # delayed by longer background processing tasks, e.g. because the user
        # expects instant feedback instead of waiting for a long list of
        # operations to finish.
        self.priority_thread_pool = ThreadPoolExecutor(max_workers=1)

        # Use a separate thread pool for file saving, with a thread count of 1,
        # to avoid race conditions in File._save_and_rename.
        self.save_thread_pool = ThreadPoolExecutor(max_workers=1)

        if not IS_WIN:
            # Set up signal handling
            # It's not possible to call all available functions from signal
            # handlers, therefore we need to set up a QSocketNotifier to listen
            # on a socket. Sending data through a socket can be done in a
            # signal handler, so we use the socket to notify the application of
            # the signal.
            # This code is adopted from
            # https://qt-project.org/doc/qt-4.8/unix-signals.html

            # To not make the socket module a requirement for the Windows
            # installer, import it here and not globally
            import socket
            self.signalfd = socket.socketpair(socket.AF_UNIX,
                                              socket.SOCK_STREAM, 0)

            self.signalnotifier = QtCore.QSocketNotifier(
                self.signalfd[1].fileno(), QtCore.QSocketNotifier.Type.Read,
                self)
            self.signalnotifier.activated.connect(self.sighandler)

            signal.signal(signal.SIGHUP, self.signal)
            signal.signal(signal.SIGINT, self.signal)
            signal.signal(signal.SIGTERM, self.signal)

        # Setup logging
        log.debug("Starting Picard from %r", os.path.abspath(__file__))
        log.debug("Platform: %s %s %s", platform.platform(),
                  platform.python_implementation(), platform.python_version())
        log.debug("Versions: %s", versions.as_string())
        log.debug("Configuration file path: %r", config.fileName())

        log.debug("User directory: %r", os.path.abspath(USER_DIR))

        # for compatibility with pre-1.3 plugins
        QtCore.QObject.tagger = self
        QtCore.QObject.config = config
        QtCore.QObject.log = log

        check_io_encoding()

        # Must be before config upgrade because upgrade dialogs need to be
        # translated
        setup_gettext(localedir, config.setting["ui_language"], log.debug)

        upgrade_config(config)

        self.webservice = WebService()
        self.mb_api = MBAPIHelper(self.webservice)

        load_user_collections()

        # Initialize fingerprinting
        acoustid_api = AcoustIdAPIHelper(self.webservice)
        self._acoustid = acoustid.AcoustIDClient(acoustid_api)
        self._acoustid.init()
        self.acoustidmanager = AcoustIDManager(acoustid_api)

        # Setup AcousticBrainz extraction
        self.ab_extractor = ABExtractor()

        self.enable_menu_icons(config.setting['show_menu_icons'])

        # Load plugins
        self.pluginmanager = PluginManager()
        if not self._no_plugins:
            for plugin_dir in plugin_dirs():
                self.pluginmanager.load_plugins_from_directory(plugin_dir)

        self.browser_integration = BrowserIntegration()
        self.browser_integration.listen_port_changed.connect(
            self.listen_port_changed)

        self._pending_files_count = 0
        self.files = {}
        self.clusters = ClusterList()
        self.albums = {}
        self.release_groups = {}
        self.mbid_redirects = {}
        self.unclustered_files = UnclusteredFiles()
        self.nats = None
        self.window = MainWindow(disable_player=picard_args.no_player)
        self.exit_cleanup = []
        self.stopping = False

        # Load release version information
        if self.autoupdate_enabled:
            self.updatecheckmanager = UpdateCheckManager(parent=self.window)
Beispiel #9
0
class MBAPITest(PicardTestCase):
    def setUp(self):
        super().setUp()
        self.config = {'server_host': "mb.org", "server_port": 443}
        self.set_config_values(self.config)
        self.ws = MagicMock(auto_spec=WebService)
        self.api = MBAPIHelper(self.ws)

    def _test_ws_function_args(self, ws_function):
        self.assertGreater(ws_function.call_count, 0)
        self.assertEqual(ws_function.call_args[0][0],
                         self.config['server_host'])
        self.assertEqual(ws_function.call_args[0][1],
                         self.config['server_port'])
        self.assertIn("/ws/2/", ws_function.call_args[0][2])

    def assertInPath(self, ws_function, path):
        self.assertIn(path, ws_function.call_args[0][2])

    def assertNotInPath(self, ws_function, path):
        self.assertNotIn(path, ws_function.call_args[0][2])

    def assertInQuery(self, ws_function, argname, value=None):
        query_args = ws_function.call_args[1]['queryargs']
        self.assertIn(argname, query_args)
        self.assertEqual(value, query_args[argname])

    def _test_inc_args(self, ws_function, arg_list):
        self.assertInQuery(self.ws.get, 'inc', "+".join(arg_list))

    def test_get_release(self):
        inc_args_list = ['test']
        self.api.get_release_by_id("1", None, inc=inc_args_list)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "/release/1")
        self._test_inc_args(self.ws.get, inc_args_list)

    def test_get_track(self):
        inc_args_list = ['test']
        self.api.get_track_by_id("1", None, inc=inc_args_list)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "/recording/1")
        self._test_inc_args(self.ws.get, inc_args_list)

    def test_get_collection(self):
        inc_args_list = ["releases", "artist-credits", "media"]
        self.api.get_collection("1", None)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "collection")
        self.assertInPath(self.ws.get, "1/releases")
        self._test_inc_args(self.ws.get, inc_args_list)

    def test_get_collection_list(self):
        self.api.get_collection_list(None)
        self._test_ws_function_args(self.ws.get)
        self.assertInPath(self.ws.get, "collection")
        self.assertNotInPath(self.ws.get, "releases")

    def test_put_collection(self):
        self.api.put_to_collection("1", ["1", "2", "3"], None)
        self._test_ws_function_args(self.ws.put)
        self.assertInPath(self.ws.put, "collection/1/releases/1;2;3")

    def test_delete_collection(self):
        self.api.delete_from_collection("1", ["1", "2", "3", "4"] * 200, None)
        collection_string = ";".join(["1", "2", "3", "4"] * 100)
        self._test_ws_function_args(self.ws.delete)
        self.assertInPath(self.ws.delete,
                          "collection/1/releases/" + collection_string)
        self.assertNotInPath(self.ws.delete,
                             collection_string + ";" + collection_string)
        self.assertEqual(self.ws.delete.call_count, 2)

    def test_xml_ratings_empty(self):
        ratings = dict()
        xmldata = self.api._xml_ratings(ratings)
        self.assertEqual(
            xmldata, '<?xml version="1.0" encoding="UTF-8"?>'
            '<metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#">'
            '<recording-list></recording-list>'
            '</metadata>')

    def test_xml_ratings_one(self):
        ratings = {("recording", 'a'): 1}
        xmldata = self.api._xml_ratings(ratings)
        self.assertEqual(
            xmldata, '<?xml version="1.0" encoding="UTF-8"?>'
            '<metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#">'
            '<recording-list>'
            '<recording id="a"><user-rating>20</user-rating></recording>'
            '</recording-list>'
            '</metadata>')

    def test_xml_ratings_multiple(self):
        ratings = {
            ("recording", 'a'): 1,
            ("recording", 'b'): 2,
            ("nonrecording", 'c'): 3,
        }
        xmldata = self.api._xml_ratings(ratings)
        self.assertEqual(
            xmldata, '<?xml version="1.0" encoding="UTF-8"?>'
            '<metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#">'
            '<recording-list>'
            '<recording id="a"><user-rating>20</user-rating></recording>'
            '<recording id="b"><user-rating>40</user-rating></recording>'
            '</recording-list>'
            '</metadata>')

    def test_xml_ratings_encode(self):
        ratings = {("recording", '<a&"\'>'): 0}
        xmldata = self.api._xml_ratings(ratings)
        self.assertEqual(
            xmldata, '<?xml version="1.0" encoding="UTF-8"?>'
            '<metadata xmlns="http://musicbrainz.org/ns/mmd-2.0#">'
            '<recording-list>'
            '<recording id="&lt;a&amp;&quot;\'&gt;"><user-rating>0</user-rating></recording>'
            '</recording-list>'
            '</metadata>')

    def test_xml_ratings_raises_value_error(self):
        ratings = {("recording", 'a'): 'foo'}
        self.assertRaises(ValueError, self.api._xml_ratings, ratings)

    def test_collection_request(self):
        releases = tuple("r" + str(i) for i in range(13))
        generator = self.api._collection_request("test", releases, batchsize=5)
        batch = next(generator)
        self.assertEqual(batch,
                         ('collection', 'test', 'releases', 'r0;r1;r2;r3;r4'))
        batch = next(generator)
        self.assertEqual(batch,
                         ('collection', 'test', 'releases', 'r5;r6;r7;r8;r9'))
        batch = next(generator)
        self.assertEqual(batch,
                         ('collection', 'test', 'releases', 'r10;r11;r12'))
        with self.assertRaises(StopIteration):
            next(generator)
Beispiel #10
0
    def callback(self, album):
        if not album:
            log.error("{0}: No album specified for submitting ISRCs.".format(PLUGIN_NAME,))
            return

        log.info("{0}: Submitting ISRCs for: {1}".format(PLUGIN_NAME, album[0].metadata['album'],))
        if not album[0].tracks:
            log.debug("{0}: No tracks found in album: {1}".format(PLUGIN_NAME, album[0].metadata['album'],))
            show_popup('Error', 'No tracks found in the album.')
            return

        isrcs = {}
        multi_isrcs = []
        for track in album[0].tracks:
            if not track.files:
                continue
            audio_file = track.files[0]
            metadata = track.metadata
            file_metadata = audio_file.orig_metadata

            # No ISRC found in the file
            if 'isrc' not in file_metadata:
                continue

            # Get string of existing ISRCs on MusicBrainz
            if 'isrc' in metadata:
                mb_isrc = metadata['isrc'].upper()
            else:
                mb_isrc = ''

            # Get ISRC string from the file
            file_isrc = file_metadata['isrc']

            # Multiple ISRCs found in the file (don't process)
            if ';' in file_isrc:
                multi_isrcs.append('  {0} - {1}'.format(metadata['tracknumber'], metadata['title']))
                log.info("{0}: Multiple ISRCs found on track {1} (not processed): {2}".format(PLUGIN_NAME, metadata['tracknumber'], file_isrc))
                continue

            isrc = validate_isrc(file_isrc)

            # ISRC does not pass validation test
            if not isrc:
                log.debug("{0}: Invalid ISRC found on track {1}: {2}".format(PLUGIN_NAME, metadata['tracknumber'], file_isrc))
                show_popup('Error', "Invalid ISRC found on track {0}: '{1}'".format(metadata['tracknumber'], file_isrc))
                return

            # ISRC already found on another track for this album
            if isrc in isrcs:
                log.debug("{0}: Duplicate ISRC found on track {1}: {2}".format(PLUGIN_NAME, metadata['tracknumber'], file_isrc))
                show_popup('Error', "Duplicate ISRC found on track {0}: '{1}'".format(metadata['tracknumber'], file_isrc))
                return

            # ISRC already associated with that track (MusicBrainz recording)
            if isrc in mb_isrc:
                continue

            # New ISRC added for submission
            log.debug("{0}: Adding ISRC '{1}' for track {2} - \"{3}\"".format(PLUGIN_NAME, isrc, metadata['tracknumber'], metadata['title'],))
            isrcs[isrc] = metadata['musicbrainz_recordingid']

        if multi_isrcs:
            multiple_msg = '\n\nThe following track audio files contained multiple ISRCs (not submitted):\n' + '\n'.join(multi_isrcs)
        else:
            multiple_msg = ''

        # Save count of new ISRCs to display in success message
        self.isrc_count = len(isrcs)

        # Nothing to submit
        if not isrcs:
            log.debug("{0}: No new ISRCs found in album: {1}".format(PLUGIN_NAME, album[0].metadata['album'],))
            show_popup('Error', 'No new ISRCs found for the tracks in the album.{0}'.format(multiple_msg,))
            return

        if multiple_msg:
            show_popup('Submitting', 'Submitting {0} ISRC{1}.{2}'.format(self.isrc_count, '' if self.isrc_count == 1 else 's', multiple_msg,))

        # Build the xml data payload
        xml_items = [XML_HEADER]
        for isrc, recording in isrcs.items():
            xml_items.append(XML_TEMPLATE.format(recording, isrc))
        xml_items.append(XML_FOOTER)
        data = _wrap_xml_metadata(''.join(xml_items))

        # Initialize the MusicBrainz API Helper
        webservice = album[0].tagger.webservice
        helper = MBAPIHelper(webservice)

        # Set up parameters for the helper
        client_string = 'Picard_Plugin_{0}-v{1}'.format(PLUGIN_NAME, PLUGIN_VERSION).replace(' ', '_')
        handler = self.submission_handler
        path_list = ['recording']
        params = {"client": client_string}

        return helper.post(path_list, data, handler, priority=True,
                         queryargs=params, parse_response_type="xml",
                         request_mimetype="application/xml; charset=utf-8")