diff --git a/picard/coverart/providers/__init__.py b/picard/coverart/providers/__init__.py index 23777be5d..97ab23c7a 100644 --- a/picard/coverart/providers/__init__.py +++ b/picard/coverart/providers/__init__.py @@ -7,7 +7,7 @@ # Copyright (C) 2016 Ville Skyttä # Copyright (C) 2016 Wieland Hoffmann # Copyright (C) 2017 Sambhav Kothari -# Copyright (C) 2019 Philipp Wolfer +# Copyright (C) 2019-2020 Philipp Wolfer # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -28,7 +28,6 @@ from collections import ( defaultdict, namedtuple, ) -import traceback from picard import ( config, @@ -39,51 +38,19 @@ from picard.coverart.providers.caa_release_group import ( CoverArtProviderCaaReleaseGroup, ) from picard.coverart.providers.local import CoverArtProviderLocal +from picard.coverart.providers.provider import ( # noqa: F401 # pylint: disable=unused-import + CoverArtProvider, + ProviderOptions, +) from picard.coverart.providers.whitelist import CoverArtProviderWhitelist from picard.plugin import ExtensionPoint -from picard.ui.options import ( - OptionsPage, - register_options_page, -) +from picard.ui.options import register_options_page _cover_art_providers = ExtensionPoint(label='cover_art_providers') -class ProviderOptions(OptionsPage): - - """ Template class for provider's options - - It works like OptionsPage for the most (options, load, save) - It will append the provider's options page as a child of the main - cover art's options page. - - The property _options_ui must be set to a valid Qt Ui class - containing the layout and widgets for defined provider's options. - - A specific provider class (inhereting from CoverArtProvider) has - to set the subclassed ProviderOptions as OPTIONS property. - Options will be registered at the same time as the provider. - - class MyProviderOptions(ProviderOptions): - _options_ui = Ui_MyProviderOptions - .... - - class MyProvider(CoverArtProvider): - OPTIONS = ProviderOptionsMyProvider - .... - - """ - - PARENT = "cover" - - def __init__(self, parent=None): - super().__init__(parent) - self.ui = self._options_ui() - self.ui.setupUi(self) - - def register_cover_art_provider(provider): _cover_art_providers.register(provider.__module__, provider) if hasattr(provider, 'OPTIONS') and provider.OPTIONS: @@ -123,79 +90,6 @@ def cover_art_providers(): yield ProviderTuple(name=p.name, title=p.title, enabled=order[p.name].enabled, cls=p) -class CoverArtProviderMetaClass(type): - """Provide default properties name & title for CoverArtProvider - It is recommended to use those in place of NAME and TITLE that might not be defined - """ - @property - def name(cls): - return getattr(cls, 'NAME', cls.__name__) - - @property - def title(cls): - return getattr(cls, 'TITLE', cls.name) - - -class CoverArtProvider(metaclass=CoverArtProviderMetaClass): - """Subclasses of this class need to reimplement at least `queue_images()`. - `__init__()` does not have to do anything. - `queue_images()` will be called if `enabled()` returns `True`. - `queue_images()` must return `FINISHED` when it finished to queue - potential cover art downloads (using `queue_put(). - If `queue_images()` delegates the job of queuing downloads to another - method (asynchronous) it should return `WAIT` and the other method has to - explicitly call `next_in_queue()`. - If `FINISHED` is returned, `next_in_queue()` will be automatically called - by CoverArt object. - """ - - # default state, internal use - _STARTED = 0 - # returned by queue_images(): - # next_in_queue() will be automatically called - FINISHED = 1 - # returned by queue_images(): - # next_in_queue() has to be called explicitly by provider - WAIT = 2 - - def __init__(self, coverart): - self.coverart = coverart - self.release = coverart.release - self.metadata = coverart.metadata - self.album = coverart.album - - def enabled(self): - return not self.coverart.front_image_found - - def queue_images(self): - # this method has to return CoverArtProvider.FINISHED or - # CoverArtProvider.WAIT - raise NotImplementedError - - def error(self, msg): - self.coverart.album.error_append(msg) - - def queue_put(self, what): - self.coverart.queue_put(what) - - def next_in_queue(self): - # must be called by provider if queue_images() returns WAIT - self.coverart.next_in_queue() - - def match_url_relations(self, relation_types, func): - """Execute `func` for each relation url matching type in - `relation_types` - """ - try: - if 'relations' in self.release: - for relation in self.release['relations']: - if relation['target-type'] == 'url': - if relation['type'] in relation_types: - func(relation['url']['resource']) - except AttributeError: - self.error(traceback.format_exc()) - - __providers = [ CoverArtProviderLocal, CoverArtProviderCaa, diff --git a/picard/coverart/providers/caa.py b/picard/coverart/providers/caa.py index 3cdb8d051..0bd9b5963 100644 --- a/picard/coverart/providers/caa.py +++ b/picard/coverart/providers/caa.py @@ -57,7 +57,7 @@ from picard.coverart.image import ( CaaCoverArtImage, CaaThumbnailCoverArtImage, ) -from picard.coverart.providers import ( +from picard.coverart.providers.provider import ( CoverArtProvider, ProviderOptions, ) diff --git a/picard/coverart/providers/local.py b/picard/coverart/providers/local.py index 51734f268..8236918db 100644 --- a/picard/coverart/providers/local.py +++ b/picard/coverart/providers/local.py @@ -27,7 +27,7 @@ import re from picard import config from picard.coverart.image import LocalFileCoverArtImage -from picard.coverart.providers import ( +from picard.coverart.providers.provider import ( CoverArtProvider, ProviderOptions, ) diff --git a/picard/coverart/providers/provider.py b/picard/coverart/providers/provider.py new file mode 100644 index 000000000..f6b9d89d5 --- /dev/null +++ b/picard/coverart/providers/provider.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# +# Copyright (C) 2014-2015, 2018-2019 Laurent Monin +# Copyright (C) 2015 Rahul Raturi +# Copyright (C) 2016 Ville Skyttä +# Copyright (C) 2016 Wieland Hoffmann +# Copyright (C) 2017 Sambhav Kothari +# Copyright (C) 2019-2020 Philipp Wolfer +# +# 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. + +import traceback + +from picard.ui.options import OptionsPage + + +class ProviderOptions(OptionsPage): + + """ Template class for provider's options + + It works like OptionsPage for the most (options, load, save) + It will append the provider's options page as a child of the main + cover art's options page. + + The property _options_ui must be set to a valid Qt Ui class + containing the layout and widgets for defined provider's options. + + A specific provider class (inhereting from CoverArtProvider) has + to set the subclassed ProviderOptions as OPTIONS property. + Options will be registered at the same time as the provider. + + class MyProviderOptions(ProviderOptions): + _options_ui = Ui_MyProviderOptions + .... + + class MyProvider(CoverArtProvider): + OPTIONS = ProviderOptionsMyProvider + .... + + """ + + PARENT = "cover" + + def __init__(self, parent=None): + super().__init__(parent) + self.ui = self._options_ui() + self.ui.setupUi(self) + + +class CoverArtProviderMetaClass(type): + """Provide default properties name & title for CoverArtProvider + It is recommended to use those in place of NAME and TITLE that might not be defined + """ + @property + def name(cls): + return getattr(cls, 'NAME', cls.__name__) + + @property + def title(cls): + return getattr(cls, 'TITLE', cls.name) + + +class CoverArtProvider(metaclass=CoverArtProviderMetaClass): + """Subclasses of this class need to reimplement at least `queue_images()`. + `__init__()` does not have to do anything. + `queue_images()` will be called if `enabled()` returns `True`. + `queue_images()` must return `FINISHED` when it finished to queue + potential cover art downloads (using `queue_put(). + If `queue_images()` delegates the job of queuing downloads to another + method (asynchronous) it should return `WAIT` and the other method has to + explicitly call `next_in_queue()`. + If `FINISHED` is returned, `next_in_queue()` will be automatically called + by CoverArt object. + """ + + # default state, internal use + _STARTED = 0 + # returned by queue_images(): + # next_in_queue() will be automatically called + FINISHED = 1 + # returned by queue_images(): + # next_in_queue() has to be called explicitly by provider + WAIT = 2 + + def __init__(self, coverart): + self.coverart = coverart + self.release = coverart.release + self.metadata = coverart.metadata + self.album = coverart.album + + def enabled(self): + return not self.coverart.front_image_found + + def queue_images(self): + # this method has to return CoverArtProvider.FINISHED or + # CoverArtProvider.WAIT + raise NotImplementedError + + def error(self, msg): + self.coverart.album.error_append(msg) + + def queue_put(self, what): + self.coverart.queue_put(what) + + def next_in_queue(self): + # must be called by provider if queue_images() returns WAIT + self.coverart.next_in_queue() + + def match_url_relations(self, relation_types, func): + """Execute `func` for each relation url matching type in + `relation_types` + """ + try: + if 'relations' in self.release: + for relation in self.release['relations']: + if relation['target-type'] == 'url': + if relation['type'] in relation_types: + func(relation['url']['resource']) + except AttributeError: + self.error(traceback.format_exc()) diff --git a/picard/coverart/providers/whitelist.py b/picard/coverart/providers/whitelist.py index 5a1c29632..285d63909 100644 --- a/picard/coverart/providers/whitelist.py +++ b/picard/coverart/providers/whitelist.py @@ -27,7 +27,7 @@ from picard import log from picard.coverart.image import CoverArtImage -from picard.coverart.providers import CoverArtProvider +from picard.coverart.providers.provider import CoverArtProvider class CoverArtProviderWhitelist(CoverArtProvider): diff --git a/picard/file.py b/picard/file.py index 41a6b1b5f..a9dae17e8 100644 --- a/picard/file.py +++ b/picard/file.py @@ -172,7 +172,7 @@ class File(QtCore.QObject, Item): # Try loading based on extension first return self._load(filename) except Exception: - from picard.formats import guess_format + from picard.formats.util import guess_format # If it fails, force format guessing and try loading again file_format = guess_format(filename) if not file_format and type(file_format) == type(self): diff --git a/picard/formats/__init__.py b/picard/formats/__init__.py index 7a3751edf..b7e17738b 100644 --- a/picard/formats/__init__.py +++ b/picard/formats/__init__.py @@ -4,7 +4,7 @@ # # Copyright (C) 2006-2008, 2012 Lukáš Lalinský # Copyright (C) 2008 Will -# Copyright (C) 2010, 2014, 2018-2019 Philipp Wolfer +# Copyright (C) 2010, 2014, 2018-2020 Philipp Wolfer # Copyright (C) 2013 Michael Wiencek # Copyright (C) 2013, 2017-2019 Laurent Monin # Copyright (C) 2016-2018 Sambhav Kothari @@ -27,7 +27,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -from picard import log from picard.formats.ac3 import AC3File from picard.formats.apev2 import ( AACFile, @@ -46,6 +45,14 @@ from picard.formats.id3 import ( ) from picard.formats.midi import MIDIFile from picard.formats.mp4 import MP4File +from picard.formats.util import ( # noqa: F401 # pylint: disable=unused-import + ext_to_format, + guess_format, + open_, + register_format, + supported_extensions, + supported_formats, +) from picard.formats.vorbis import ( FLACFile, OggAudioFile, @@ -57,76 +64,6 @@ from picard.formats.vorbis import ( OggVorbisFile, ) from picard.formats.wav import WAVFile -from picard.plugin import ExtensionPoint - - -_formats = ExtensionPoint(label='formats') -_extensions = {} - - -def register_format(file_format): - _formats.register(file_format.__module__, file_format) - for ext in file_format.EXTENSIONS: - _extensions[ext[1:]] = file_format - - -def supported_formats(): - """Returns list of supported formats.""" - return [(file_format.EXTENSIONS, file_format.NAME) for file_format in _formats] - - -def supported_extensions(): - """Returns list of supported extensions.""" - return [ext for exts, name in supported_formats() for ext in exts] - - -def ext_to_format(ext): - return _extensions.get(ext, None) - - -def guess_format(filename, options=_formats): - """Select the best matching file type amongst supported formats.""" - results = [] - # Since we are reading only 128 bytes and then immediately closing the file, - # use unbuffered mode. - with open(filename, "rb", 0) as fileobj: - header = fileobj.read(128) - # Calls the score method of a particular format's associated filetype - # and assigns a positive score depending on how closely the fileobj's header matches - # the header for a particular file format. - results = [(option._File.score(filename, fileobj, header), option.__name__, option) - for option in options - if getattr(option, "_File", None)] - if results: - results.sort() - if results[-1][0] > 0: - # return the format with the highest matching score - return results[-1][2](filename) - - # No positive score i.e. the fileobj's header did not match any supported format - return None - - -def open_(filename): - """Open the specified file and return a File instance with the appropriate format handler, or None.""" - try: - # Use extension based opening as default - i = filename.rfind(".") - if i >= 0: - ext = filename[i+1:].lower() - audio_file = _extensions[ext](filename) - else: - # If there is no extension, try to guess the format based on file headers - audio_file = guess_format(filename) - if not audio_file: - return None - return audio_file - except KeyError: - # None is returned if both the methods fail - return None - except Exception as error: - log.error("Error occurred:\n{}".format(error)) - return None register_format(AACFile) diff --git a/picard/formats/util.py b/picard/formats/util.py new file mode 100644 index 000000000..4eedaee74 --- /dev/null +++ b/picard/formats/util.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# +# Copyright (C) 2006-2008, 2012 Lukáš Lalinský +# Copyright (C) 2008 Will +# Copyright (C) 2010, 2014, 2018-2020 Philipp Wolfer +# Copyright (C) 2013 Michael Wiencek +# Copyright (C) 2013, 2017-2019 Laurent Monin +# Copyright (C) 2016-2018 Sambhav Kothari +# Copyright (C) 2017 Sophist-UK +# Copyright (C) 2017 Ville Skyttä +# +# 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. + +from picard import log +from picard.plugin import ExtensionPoint + + +_formats = ExtensionPoint(label='formats') +_extensions = {} + + +def register_format(file_format): + _formats.register(file_format.__module__, file_format) + for ext in file_format.EXTENSIONS: + _extensions[ext[1:]] = file_format + + +def supported_formats(): + """Returns list of supported formats.""" + return [(file_format.EXTENSIONS, file_format.NAME) for file_format in _formats] + + +def supported_extensions(): + """Returns list of supported extensions.""" + return [ext for exts, name in supported_formats() for ext in exts] + + +def ext_to_format(ext): + return _extensions.get(ext, None) + + +def guess_format(filename, options=_formats): + """Select the best matching file type amongst supported formats.""" + results = [] + # Since we are reading only 128 bytes and then immediately closing the file, + # use unbuffered mode. + with open(filename, "rb", 0) as fileobj: + header = fileobj.read(128) + # Calls the score method of a particular format's associated filetype + # and assigns a positive score depending on how closely the fileobj's header matches + # the header for a particular file format. + results = [(option._File.score(filename, fileobj, header), option.__name__, option) + for option in options + if getattr(option, "_File", None)] + if results: + results.sort() + if results[-1][0] > 0: + # return the format with the highest matching score + return results[-1][2](filename) + + # No positive score i.e. the fileobj's header did not match any supported format + return None + + +def open_(filename): + """Open the specified file and return a File instance with the appropriate format handler, or None.""" + try: + # Use extension based opening as default + i = filename.rfind(".") + if i >= 0: + ext = filename[i+1:].lower() + audio_file = _extensions[ext](filename) + else: + # If there is no extension, try to guess the format based on file headers + audio_file = guess_format(filename) + if not audio_file: + return None + return audio_file + except KeyError: + # None is returned if both the methods fail + return None + except Exception as error: + log.error("Error occurred:\n{}".format(error)) + return None diff --git a/picard/formats/vorbis.py b/picard/formats/vorbis.py index d0f9f3816..54dcb0290 100644 --- a/picard/formats/vorbis.py +++ b/picard/formats/vorbis.py @@ -47,11 +47,11 @@ from picard.coverart.image import ( TagCoverArtImage, ) from picard.file import File -from picard.formats import guess_format from picard.formats.id3 import ( image_type_as_id3_num, types_from_id3, ) +from picard.formats.util import guess_format from picard.metadata import Metadata from picard.util import ( encode_filename, diff --git a/test/formats/common.py b/test/formats/common.py index cfbe0c191..566ba6aa2 100644 --- a/test/formats/common.py +++ b/test/formats/common.py @@ -34,6 +34,7 @@ from picard.formats import ext_to_format from picard.formats.mutagenext.aac import AACAPEv2 from picard.formats.mutagenext.ac3 import AC3APEv2 from picard.formats.mutagenext.tak import TAK +from picard.formats.util import guess_format from picard.metadata import Metadata @@ -450,7 +451,7 @@ class CommonTests: @skipUnlessTestfile def test_guess_format(self): temp_file = self.copy_of_original_testfile() - audio = picard.formats.guess_format(temp_file) + audio = guess_format(temp_file) audio_original = picard.formats.open_(self.filename) self.assertEqual(type(audio), type(audio_original))