Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions picard/const/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
('Local', False),
]
DEFAULT_COVER_IMAGE_FILENAME = 'cover'
DEFAULT_LOCAL_COVER_ART_SCRIPT = 'cover'

DEFAULT_FPCALC_THREADS = 2
DEFAULT_PROGRAM_UPDATE_LEVEL = 0
Expand Down
142 changes: 129 additions & 13 deletions picard/coverart/providers/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@
ProviderOptions,
)
from picard.coverart.utils import CAA_TYPES
from picard.i18n import N_
from picard.i18n import (
N_,
gettext as _,
)
from picard.metadata import Metadata
from picard.util.scripttofilename import script_to_filename

from picard.ui.forms.ui_provider_options_local import Ui_LocalOptions

Expand All @@ -49,14 +54,86 @@ class ProviderOptionsLocal(ProviderOptions):
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_use_script.toggled.connect(self._update_visibility)
self.ui.local_cover_script_edit.textChanged.connect(self._update_script_preview)
self._update_visibility()

def _get_example_metadata(self):
"""Create sample metadata for script preview."""
metadata = Metadata()
metadata['album'] = 'Abbey Road'
metadata['albumartist'] = 'The Beatles'
metadata['artist'] = 'The Beatles'
metadata['title'] = 'Come Together'
metadata['date'] = '1969'
metadata['originaldate'] = '1969-09-26'
metadata['releasetype'] = 'album'
metadata['releasestatus'] = 'official'
metadata['~releasecomment'] = 'releasecomment'
metadata['releasecountry'] = 'GB'
metadata['label'] = 'Apple Records'
metadata['catalognumber'] = 'PCS 7088'
metadata['barcode'] = 'barcode'
metadata['media'] = 'CD'
metadata['discnumber'] = '1'
metadata['totaldiscs'] = '1'
metadata['tracknumber'] = '1'
metadata['totaltracks'] = '17'
return metadata

def _update_script_preview(self):
"""Update the script preview with sample metadata."""
script = self.ui.local_cover_script_edit.toPlainText()
if not script:
self.ui.script_preview_value.setText("")
self.ui.script_preview_value.setStyleSheet("")
return

try:
result = script_to_filename(script, self._get_example_metadata())
if result:
self.ui.script_preview_value.setText(result)
self.ui.script_preview_value.setStyleSheet("font-weight: bold;")
else:
self.ui.script_preview_value.setText(_("(empty result - script will not match any files)"))
self.ui.script_preview_value.setStyleSheet(self.STYLESHEET_ERROR)
except Exception as e:
self.ui.script_preview_value.setText(_("Error: %s") % str(e))
self.ui.script_preview_value.setStyleSheet(self.STYLESHEET_ERROR)

def _update_visibility(self):
use_script = self.ui.local_cover_use_script.isChecked()
# Toggle visibility based on mode
for widget in (
self.ui.local_cover_regex_label,
self.ui.local_cover_regex_edit,
self.ui.local_cover_regex_error,
self.ui.regex_note,
):
widget.setVisible(not use_script)
for widget in (
self.ui.local_cover_script_label,
self.ui.local_cover_script_edit,
self.ui.script_preview_label,
self.ui.script_preview_value,
self.ui.script_note,
):
widget.setVisible(use_script)
if use_script:
self._update_script_preview()

def load(self):
config = get_config()
self.ui.local_cover_regex_edit.setText(config.setting['local_cover_regex'])
self.ui.local_cover_script_edit.setPlainText(config.setting['local_cover_script'])
self.ui.local_cover_use_script.setChecked(config.setting['local_cover_use_script'])
self._update_visibility()

def save(self):
config = get_config()
config.setting['local_cover_regex'] = self.ui.local_cover_regex_edit.text()
config.setting['local_cover_script'] = self.ui.local_cover_script_edit.toPlainText()
config.setting['local_cover_use_script'] = self.ui.local_cover_use_script.isChecked()


class CoverArtProviderLocal(CoverArtProvider):
Expand All @@ -72,20 +149,45 @@ class CoverArtProviderLocal(CoverArtProvider):

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)

if config.setting['local_cover_use_script']:
value = config.setting['local_cover_script']
queue_method = self._queue_images_script
else:
value = config.setting['local_cover_regex']
queue_method = self._queue_images_regex

if value:
queue_method(value)

return CoverArtProvider.QueueState.FINISHED

def _queue_images_regex(self, 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)

def _queue_images_script(self, script):
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)

expected_filename = script_to_filename(script, file.metadata)
if expected_filename:
for image in self.find_local_images_by_script(current_dir, expected_filename):
self.queue_put(image)

def get_types(self, string):
found = {x.lower() for x in self._types_split_re.split(string) if x}
return list(found.intersection(self._known_types))
Expand All @@ -109,3 +211,17 @@ def find_local_images(self, current_dir, match_re):
support_types=True,
support_multi_types=True,
)

def find_local_images_by_script(self, current_dir, expected_filename):
for root, _dirs, files in os.walk(current_dir):
for filename in files:
name_without_ext, ext = os.path.splitext(filename)
if name_without_ext == expected_filename:
filepath = os.path.join(current_dir, root, filename)
if os.path.exists(filepath):
yield LocalFileCoverArtImage(
filepath,
types=self._default_types,
support_types=True,
support_multi_types=True,
)
3 changes: 3 additions & 0 deletions picard/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
DEFAULT_FILTER_COLUMNS,
DEFAULT_FPCALC_THREADS,
DEFAULT_LOCAL_COVER_ART_REGEX,
DEFAULT_LOCAL_COVER_ART_SCRIPT,
DEFAULT_LONG_PATHS,
DEFAULT_MUSIC_DIR,
DEFAULT_PROGRAM_UPDATE_LEVEL,
Expand Down Expand Up @@ -115,6 +116,8 @@
# picard/coverart/providers/local.py
# Local Files
TextOption('setting', 'local_cover_regex', DEFAULT_LOCAL_COVER_ART_REGEX)
TextOption('setting', 'local_cover_script', DEFAULT_LOCAL_COVER_ART_SCRIPT)
BoolOption('setting', 'local_cover_use_script', False)

# picard/ui/cdlookup.py
#
Expand Down
43 changes: 37 additions & 6 deletions picard/ui/forms/ui_provider_options_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ def setupUi(self, LocalOptions):
LocalOptions.resize(472, 215)
self.verticalLayout = QtWidgets.QVBoxLayout(LocalOptions)
self.verticalLayout.setObjectName("verticalLayout")
self.local_cover_use_script = QtWidgets.QCheckBox(parent=LocalOptions)
self.local_cover_use_script.setObjectName("local_cover_use_script")
self.verticalLayout.addWidget(self.local_cover_use_script)
self.local_cover_regex_label = QtWidgets.QLabel(parent=LocalOptions)
self.local_cover_regex_label.setObjectName("local_cover_regex_label")
self.verticalLayout.addWidget(self.local_cover_regex_label)
Expand All @@ -33,13 +36,36 @@ def setupUi(self, LocalOptions):
self.local_cover_regex_error.setObjectName("local_cover_regex_error")
self.horizontalLayout_2.addWidget(self.local_cover_regex_error)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.note = QtWidgets.QLabel(parent=LocalOptions)
self.regex_note = QtWidgets.QLabel(parent=LocalOptions)
font = QtGui.QFont()
font.setItalic(True)
self.note.setFont(font)
self.note.setWordWrap(True)
self.note.setObjectName("note")
self.verticalLayout.addWidget(self.note)
self.regex_note.setFont(font)
self.regex_note.setWordWrap(True)
self.regex_note.setObjectName("regex_note")
self.verticalLayout.addWidget(self.regex_note)
self.local_cover_script_label = QtWidgets.QLabel(parent=LocalOptions)
self.local_cover_script_label.setObjectName("local_cover_script_label")
self.verticalLayout.addWidget(self.local_cover_script_label)
self.local_cover_script_edit = ScriptTextEdit(parent=LocalOptions)
self.local_cover_script_edit.setMaximumSize(QtCore.QSize(16777215, 100))
self.local_cover_script_edit.setObjectName("local_cover_script_edit")
self.verticalLayout.addWidget(self.local_cover_script_edit)
self.script_preview_label = QtWidgets.QLabel(parent=LocalOptions)
self.script_preview_label.setObjectName("script_preview_label")
self.verticalLayout.addWidget(self.script_preview_label)
self.script_preview_value = QtWidgets.QLabel(parent=LocalOptions)
self.script_preview_value.setText("")
self.script_preview_value.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextSelectableByMouse)
self.script_preview_value.setWordWrap(True)
self.script_preview_value.setObjectName("script_preview_value")
self.verticalLayout.addWidget(self.script_preview_value)
self.script_note = QtWidgets.QLabel(parent=LocalOptions)
font = QtGui.QFont()
font.setItalic(True)
self.script_note.setFont(font)
self.script_note.setWordWrap(True)
self.script_note.setObjectName("script_note")
self.verticalLayout.addWidget(self.script_note)
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
self.verticalLayout.addItem(spacerItem)

Expand All @@ -48,5 +74,10 @@ def setupUi(self, LocalOptions):

def retranslateUi(self, LocalOptions):
LocalOptions.setWindowTitle(_("Form"))
self.local_cover_use_script.setText(_("Use scripting syntax instead of regular expression"))
self.local_cover_regex_label.setText(_("Local cover art files match the following regular expression:"))
self.note.setText(_("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."))
self.regex_note.setText(_("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."))
self.local_cover_script_label.setText(_("Local cover art files match the following script:"))
self.script_preview_label.setText(_("Example:"))
self.script_note.setText(_("The script will be evaluated for each file\'s metadata. Use variables like %albumartist%, %album%, etc."))
from picard.ui.widgets.scripttextedit import ScriptTextEdit
68 changes: 67 additions & 1 deletion ui/provider_options_local.ui
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="local_cover_use_script">
<property name="text">
<string>Use scripting syntax instead of regular expression</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="local_cover_regex_label">
<property name="text">
Expand All @@ -36,7 +43,7 @@
</layout>
</item>
<item>
<widget class="QLabel" name="note">
<widget class="QLabel" name="regex_note">
<property name="font">
<font>
<italic>true</italic>
Expand All @@ -50,6 +57,58 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="local_cover_script_label">
<property name="text">
<string>Local cover art files match the following script:</string>
</property>
</widget>
</item>
<item>
<widget class="ScriptTextEdit" name="local_cover_script_edit">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="script_preview_label">
<property name="text">
<string>Example:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="script_preview_value">
<property name="text">
<string/>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByMouse</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="script_note">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>The script will be evaluated for each file's metadata. Use variables like %albumartist%, %album%, etc.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
Expand All @@ -65,6 +124,13 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ScriptTextEdit</class>
<extends>QTextEdit</extends>
<header>picard.ui.widgets.scripttextedit</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
Loading