mirror of
https://github.com/fergalmoran/picard.git
synced 2026-01-06 08:34:01 +00:00
Refactor picard.formats and picard.coverart.providers to avoid circular dependencies
This commit is contained in:
@@ -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(<CoverArtImage object>).
|
||||
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,
|
||||
|
||||
@@ -57,7 +57,7 @@ from picard.coverart.image import (
|
||||
CaaCoverArtImage,
|
||||
CaaThumbnailCoverArtImage,
|
||||
)
|
||||
from picard.coverart.providers import (
|
||||
from picard.coverart.providers.provider import (
|
||||
CoverArtProvider,
|
||||
ProviderOptions,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
134
picard/coverart/providers/provider.py
Normal file
134
picard/coverart/providers/provider.py
Normal file
@@ -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(<CoverArtImage object>).
|
||||
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())
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
98
picard/formats/util.py
Normal file
98
picard/formats/util.py
Normal file
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user