diff --git a/picard/const/__init__.py b/picard/const/__init__.py index d4253a984c..b698e32f41 100644 --- a/picard/const/__init__.py +++ b/picard/const/__init__.py @@ -189,6 +189,7 @@ DEFAULT_NUMBERED_SCRIPT_NAME = N_("My script %d") DEFAULT_SCRIPT_NAME = N_("My script") DEFAULT_COVER_IMAGE_FILENAME = "cover" +DEFAULT_LOCAL_COVER_ART_REGEX = r'^(?:cover|folder|albumart)(.*)\.(?:jpe?g|png|gif|tiff?|webp)$' DEFAULT_NUMBERED_PROFILE_NAME = N_("My profile %d") DEFAULT_PROFILE_NAME = N_("My profile") diff --git a/picard/coverart/image.py b/picard/coverart/image.py index 66bc7d34e0..db7318e85e 100644 --- a/picard/coverart/image.py +++ b/picard/coverart/image.py @@ -480,3 +480,6 @@ def __init__(self, filepath, types=None, comment='', super().__init__(url=url, types=types, comment=comment) self.support_types = support_types self.support_multi_types = support_multi_types + path = self.url.toLocalFile() + with open(path, 'rb') as file: + self.set_data(file.read()) diff --git a/picard/coverart/providers/__init__.py b/picard/coverart/providers/__init__.py index e4117ca384..16680a8b05 100644 --- a/picard/coverart/providers/__init__.py +++ b/picard/coverart/providers/__init__.py @@ -35,7 +35,6 @@ 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, @@ -92,7 +91,6 @@ def label(p): __providers = [ - CoverArtProviderLocal, CoverArtProviderCaa, CoverArtProviderUrlRelationships, CoverArtProviderCaaReleaseGroup, diff --git a/picard/coverart/providers/local.py b/picard/coverart/providers/local.py deleted file mode 100644 index 281b047ea4..0000000000 --- a/picard/coverart/providers/local.py +++ /dev/null @@ -1,123 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Picard, the next-generation MusicBrainz tagger -# -# Copyright (C) 2015, 2018-2020 Laurent Monin -# Copyright (C) 2016-2017 Sambhav Kothari -# Copyright (C) 2017 Ville Skyttä -# Copyright (C) 2019-2021 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 os -import re - -from picard.config import ( - TextOption, - get_config, -) -from picard.coverart.image import LocalFileCoverArtImage -from picard.coverart.providers.provider import ( - CoverArtProvider, - ProviderOptions, -) -from picard.coverart.utils import CAA_TYPES - -from picard.ui.ui_provider_options_local import Ui_LocalOptions - - -class ProviderOptionsLocal(ProviderOptions): - """ - Options for Local Files cover art provider - """ - - HELP_URL = '/config/options_local_files.html' - _DEFAULT_LOCAL_COVER_ART_REGEX = r'^(?:cover|folder|albumart)(.*)\.(?:jpe?g|png|gif|tiff?|webp)$' - - options = [ - TextOption("setting", "local_cover_regex", _DEFAULT_LOCAL_COVER_ART_REGEX), - ] - - _options_ui = Ui_LocalOptions - - def __init__(self, parent=None): - super().__init__(parent) - self.init_regex_checker(self.ui.local_cover_regex_edit, self.ui.local_cover_regex_error) - self.ui.local_cover_regex_default.clicked.connect(self.set_local_cover_regex_default) - - def set_local_cover_regex_default(self): - self.ui.local_cover_regex_edit.setText(self._DEFAULT_LOCAL_COVER_ART_REGEX) - - def load(self): - config = get_config() - self.ui.local_cover_regex_edit.setText(config.setting["local_cover_regex"]) - - def save(self): - config = get_config() - config.setting["local_cover_regex"] = self.ui.local_cover_regex_edit.text() - - -class CoverArtProviderLocal(CoverArtProvider): - - """Get cover art from local files""" - - NAME = "Local Files" - TITLE = N_("Local Files") - OPTIONS = ProviderOptionsLocal - - _types_split_re = re.compile('[^a-z0-9]', re.IGNORECASE) - _known_types = set([t['name'] for t in CAA_TYPES]) - _default_types = ['front'] - - def queue_images(self): - config = get_config() - regex = config.setting['local_cover_regex'] - if regex: - _match_re = re.compile(regex, re.IGNORECASE) - dirs_done = set() - - for file in self.album.iterfiles(): - current_dir = os.path.dirname(file.filename) - if current_dir in dirs_done: - continue - dirs_done.add(current_dir) - for image in self.find_local_images(current_dir, _match_re): - self.queue_put(image) - return CoverArtProvider.FINISHED - - def get_types(self, string): - found = set([x.lower() for x in self._types_split_re.split(string) if x]) - return list(found.intersection(self._known_types)) - - def find_local_images(self, current_dir, match_re): - for root, dirs, files in os.walk(current_dir): - for filename in files: - m = match_re.search(filename) - if not m: - continue - filepath = os.path.join(current_dir, root, filename) - if not os.path.exists(filepath): - continue - try: - type_from_filename = self.get_types(m.group(1)) - except IndexError: - type_from_filename = [] - yield LocalFileCoverArtImage( - filepath, - types=type_from_filename or self._default_types, - support_types=True, - support_multi_types=True - ) diff --git a/picard/file.py b/picard/file.py index 2a99275841..014402db7b 100644 --- a/picard/file.py +++ b/picard/file.py @@ -63,6 +63,11 @@ IS_MACOS, IS_WIN, ) +from picard.coverart.image import ( + CoverArtImageError, + LocalFileCoverArtImage +) +from picard.coverart.utils import CAA_TYPES from picard.metadata import ( Metadata, SimMatchTrack, @@ -240,6 +245,11 @@ def _loading_finished(self, callback, result=None, error=None): postprocessors = [] if config.setting["guess_tracknumber_and_title"]: postprocessors.append(self._guess_tracknumber_and_title) + # If no cover art was loaded from file tags, try loading from a local file + # TODO: should this be a preference? should we load from both sources? + log.debug("load local cover art: %r", config.setting["load_local_cover_art"]) + if config.setting["load_local_cover_art"] and len(result.images) == 0: + self._load_local_cover_art(result, config) self._copy_loaded_metadata(result, postprocessors) # use cached fingerprint from file metadata if not config.setting["ignore_existing_acoustid_fingerprints"]: @@ -250,6 +260,48 @@ def _loading_finished(self, callback, result=None, error=None): self.update() callback(self) + _types_split_re = re.compile('[^a-z0-9]', re.IGNORECASE) + _known_types = set([t['name'] for t in CAA_TYPES]) + _default_types = ['front'] + + def get_types(self, string): + found = set([x.lower() for x in self._types_split_re.split(string) if x]) + return list(found.intersection(self._known_types)) + + def _load_local_cover_art(self, metadata, config): + log.debug("Attempting to load cover art from local files") + match_re = re.compile(config.setting['local_cover_regex'], re.IGNORECASE) + current_dir = os.path.dirname(self.filename) + for root, dirs, files in os.walk(current_dir): + for filename in files: + m = match_re.search(filename) + if not m: + continue + filepath = os.path.join(current_dir, root, filename) + if not os.path.exists(filepath): + continue + try: + type_from_filename = self.get_types(m.group(1)) + except IndexError: + type_from_filename = [] + try: + coverartimage = LocalFileCoverArtImage( + filepath, + types=type_from_filename or self._default_types, + support_types=True, + support_multi_types=False + ) + + log.debug("Loaded local cover art image: %r", coverartimage) + except OSError as exc: + (errnum, errmsg) = exc.args + log.error("Failed to read %r: %s (%d)" % + (filepath, errmsg, errnum)) + except (CoverArtImageError) as e: + log.error('Cannot load image from %r: %s' % (filename, e)) + else: + metadata.images.append(coverartimage) + def _copy_loaded_metadata(self, metadata, postprocessors=None): metadata['~length'] = format_time(metadata.length) if postprocessors: diff --git a/picard/ui/options/cover.py b/picard/ui/options/cover.py index a7387f9f25..3a240cad89 100644 --- a/picard/ui/options/cover.py +++ b/picard/ui/options/cover.py @@ -32,7 +32,10 @@ TextOption, get_config, ) -from picard.const import DEFAULT_COVER_IMAGE_FILENAME +from picard.const import ( + DEFAULT_COVER_IMAGE_FILENAME, + DEFAULT_LOCAL_COVER_ART_REGEX, +) from picard.coverart.providers import cover_art_providers from picard.ui.checkbox_list_item import CheckboxListItem @@ -61,6 +64,8 @@ class CoverOptionsPage(OptionsPage): BoolOption("setting", "save_images_overwrite", False), BoolOption("setting", "save_only_one_front_image", False), BoolOption("setting", "image_type_as_filename", False), + BoolOption("setting", "load_local_cover_art", False), + TextOption("setting", "local_cover_regex", DEFAULT_LOCAL_COVER_ART_REGEX), ListOption("setting", "ca_providers", [ ('Cover Art Archive', True), ('UrlRelationships', True), diff --git a/ui/provider_options_local.ui b/ui/provider_options_local.ui deleted file mode 100644 index ee17b875f5..0000000000 --- a/ui/provider_options_local.ui +++ /dev/null @@ -1,83 +0,0 @@ - - - LocalOptions - - - - 0 - 0 - 472 - 215 - - - - Form - - - - - - Local cover art files match the following regular expression: - - - - - - - - - - - - - - - - - - - - 0 - 0 - - - - Default - - - - - - - - - - true - - - - First group in the regular expression, if any, will be used as type, ie. cover-back-spine.jpg will be set as types Back + Spine. If no type is found, it will default to Front type. - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - -