Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PICARD-2729: Allow disabling date sanitization for APE and Vorbis tags #2406

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
6 changes: 5 additions & 1 deletion picard/formats/apev2.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class APEv2File(File):
'replaygain_reference_loudness': 'REPLAYGAIN_REFERENCE_LOUDNESS',
}
__rtranslate = {v.lower(): k for k, v in __translate.items()}
sanitize_date = sanitize_date

def __init__(self, filename):
super().__init__(filename)
Expand All @@ -136,6 +137,8 @@ def _load(self, filename):
file = self._File(encode_filename(filename))
metadata = Metadata()
if file.tags:
config = get_config()
date_sanitize = self.NAME not in config.setting['formats_to_disable_date_sanitize']
for origname, values in file.tags.items():
name_lower = origname.lower()
if (values.kind == mutagen.apev2.BINARY
Expand All @@ -160,7 +163,8 @@ def _load(self, filename):
name = name_lower
if name == 'year':
name = 'date'
value = sanitize_date(value)
if date_sanitize:
value = sanitize_date(value)
elif name == 'track':
name = 'tracknumber'
track = value.split('/')
Expand Down
11 changes: 8 additions & 3 deletions picard/formats/id3.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ class ID3File(File):
__lrc_line_re_parse = re.compile(r'(\[\d\d:\d\d\.\d\d\d\])')
__lrc_syllable_re_parse = re.compile(r'(<\d\d:\d\d\.\d\d\d>)')
__lrc_both_re_parse = re.compile(r'(\[\d\d:\d\d\.\d\d\d\]|<\d\d:\d\d\.\d\d\d>)')
sanitize_date = sanitize_date

def __init__(self, filename):
super().__init__(filename)
Expand Down Expand Up @@ -278,6 +279,7 @@ def _load(self, filename):
f = tags.pop(old)
tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text))
metadata = Metadata()
date_sanitize = self.NAME not in config.setting['formats_to_disable_date_sanitize']
for frame in tags.values():
frameid = frame.FrameID
if frameid in self.__translate:
Expand Down Expand Up @@ -395,9 +397,12 @@ def _load(self, filename):
metadata.add('~rating', rating)

if 'date' in metadata:
sanitized = sanitize_date(metadata.getall('date')[0])
if sanitized:
metadata['date'] = sanitized
if date_sanitize:
sanitized = sanitize_date(metadata.getall('date')[0])
if sanitized:
metadata['date'] = sanitized
else:
metadata['date'] = metadata.getall('date')[0]

self._info(metadata, file)
return metadata
Expand Down
6 changes: 6 additions & 0 deletions picard/formats/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ def supported_formats():
return [(file_format.EXTENSIONS, file_format.NAME) for file_format in _formats]


def formats_with_sanitize_date():
for fmt in _formats:
if hasattr(fmt, 'sanitize_date'):
yield fmt


def supported_extensions():
"""Returns list of supported extensions."""
return [ext for exts, name in supported_formats() for ext in exts]
Expand Down
9 changes: 7 additions & 2 deletions picard/formats/vorbis.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,24 @@ class VCommentFile(File):
'waveformatextensible_channel_mask': '~waveformatextensible_channel_mask',
}
__rtranslate = {v: k for k, v in __translate.items()}
sanitize_date = sanitize_date

def _load(self, filename):
log.debug("Loading file %r", filename)
config = get_config()
file = self._File(encode_filename(filename))
file.tags = file.tags or {}
metadata = Metadata()
config = get_config()
date_sanitize = self.NAME not in config.setting['formats_to_disable_date_sanitize']
for origname, values in file.tags.items():
for value in values:
value = value.rstrip('\0')
name = origname
if name in {'date', 'originaldate', 'releasedate'}:
# YYYY-00-00 => YYYY
value = sanitize_date(value)
if date_sanitize:
value = sanitize_date(value)
elif name == 'performer' or name == 'comment':
# transform "performer=Joe Barr (Piano)" to "performer:Piano=Joe Barr"
name += ':'
Expand Down Expand Up @@ -280,7 +284,8 @@ def _save(self, filename, metadata):
name = 'lyrics'
elif name in {'date', 'originaldate', 'releasedate'}:
# YYYY-00-00 => YYYY
value = sanitize_date(value)
if self.NAME not in config.setting['formats_to_disable_date_sanitize']:
value = sanitize_date(value)
elif name.startswith('performer:') or name.startswith('comment:'):
# transform "performer:Piano=Joe Barr" to "performer=Joe Barr (Piano)"
name, desc = name.split(':', 1)
Expand Down
1 change: 1 addition & 0 deletions picard/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@
BoolOption('setting', 'standardize_artists', False, title=N_("Use standardized artist names"))
BoolOption('setting', 'standardize_instruments', True, title=N_("Use standardized instrument and vocal credits"))
BoolOption('setting', 'track_ars', False, title=N_("Use track and release relationships"))
Option('setting', 'formats_to_disable_date_sanitize', set(), title=N_("Formats to disable date sanitize"))
BoolOption('setting', 'translate_artist_names', False, title=N_("Translate artist names"))
BoolOption('setting', 'translate_artist_names_script_exception', False, title=N_("Translate artist names exception"))
TextOption('setting', 'va_name', "Various Artists", title=N_("Various Artists name"))
Expand Down
11 changes: 11 additions & 0 deletions picard/ui/options/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
SCRIPTS,
scripts_sorted_by_localized_name,
)
from picard.formats.util import formats_with_sanitize_date
from picard.i18n import (
N_,
gettext as _,
Expand All @@ -57,6 +58,7 @@
from picard.ui.ui_multi_locale_selector import Ui_MultiLocaleSelector
from picard.ui.ui_options_metadata import Ui_MetadataOptionsPage
from picard.ui.util import qlistwidget_items
from picard.ui.widgets.multicombobox import MultiComboBox


def iter_sorted_locales(locales):
Expand Down Expand Up @@ -105,6 +107,7 @@ def __init__(self, parent=None):
self.register_setting('convert_punctuation', ['convert_punctuation'])
self.register_setting('release_ars', ['release_ars'])
self.register_setting('track_ars', ['track_ars'])
self.register_setting('formats_to_disable_date_sanitize', ['selected_formats'])
self.register_setting('guess_tracknumber_and_title', ['guess_tracknumber_and_title'])
self.register_setting('va_name', ['va_name'])
self.register_setting('nat_name', ['nat_name'])
Expand All @@ -117,6 +120,13 @@ def load(self):
self.current_scripts = config.setting['script_exceptions']
self.make_scripts_text()
self.ui.translate_artist_names_script_exception.setChecked(config.setting['translate_artist_names_script_exception'])
self.current_formats = config.setting['formats_to_disable_date_sanitize']
fmt_names = sorted(fmt.NAME for fmt in formats_with_sanitize_date())
dummy_widget = self.ui.selected_formats
self.selected_formats = MultiComboBox(self)
self.selected_formats.addItems(fmt_names)
self.ui.verticalLayout_3.replaceWidget(dummy_widget, self.selected_formats)
dummy_widget.deleteLater()

self.ui.convert_punctuation.setChecked(config.setting['convert_punctuation'])
self.ui.release_ars.setChecked(config.setting['release_ars'])
Expand Down Expand Up @@ -152,6 +162,7 @@ def save(self):
config.setting['convert_punctuation'] = self.ui.convert_punctuation.isChecked()
config.setting['release_ars'] = self.ui.release_ars.isChecked()
config.setting['track_ars'] = self.ui.track_ars.isChecked()
config.setting['formats_to_disable_date_sanitize'] = self.current_formats
config.setting['va_name'] = self.ui.va_name.text()
nat_name = self.ui.nat_name.text()
if nat_name != config.setting['nat_name']:
Expand Down
10 changes: 9 additions & 1 deletion picard/ui/ui_options_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ def setupUi(self, MetadataOptionsPage):
self.guess_tracknumber_and_title = QtWidgets.QCheckBox(parent=self.metadata_groupbox)
self.guess_tracknumber_and_title.setObjectName("guess_tracknumber_and_title")
self.verticalLayout_3.addWidget(self.guess_tracknumber_and_title)
self.selected_formats_label = QtWidgets.QLabel(parent=self.metadata_groupbox)
self.selected_formats_label.setObjectName("selected_formats_label")
self.verticalLayout_3.addWidget(self.selected_formats_label)
self.selected_formats = QtWidgets.QWidget(parent=self.metadata_groupbox)
self.selected_formats.setObjectName("selected_formats")
self.verticalLayout_3.addWidget(self.selected_formats)
self.verticalLayout.addWidget(self.metadata_groupbox)
self.custom_fields_groupbox = QtWidgets.QGroupBox(parent=MetadataOptionsPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum)
Expand Down Expand Up @@ -126,7 +132,8 @@ def setupUi(self, MetadataOptionsPage):
MetadataOptionsPage.setTabOrder(self.convert_punctuation, self.release_ars)
MetadataOptionsPage.setTabOrder(self.release_ars, self.track_ars)
MetadataOptionsPage.setTabOrder(self.track_ars, self.guess_tracknumber_and_title)
MetadataOptionsPage.setTabOrder(self.guess_tracknumber_and_title, self.va_name)
MetadataOptionsPage.setTabOrder(self.guess_tracknumber_and_title, self.selected_formats)
MetadataOptionsPage.setTabOrder(self.selected_formats, self.va_name)
MetadataOptionsPage.setTabOrder(self.va_name, self.va_name_default)
MetadataOptionsPage.setTabOrder(self.va_name_default, self.nat_name)
MetadataOptionsPage.setTabOrder(self.nat_name, self.nat_name_default)
Expand All @@ -143,6 +150,7 @@ def retranslateUi(self, MetadataOptionsPage):
self.release_ars.setText(_("Use release relationships"))
self.track_ars.setText(_("Use track relationships"))
self.guess_tracknumber_and_title.setText(_("Guess track number and title from filename if empty"))
self.selected_formats_label.setText(_("Disable date sanitization for:"))
self.custom_fields_groupbox.setTitle(_("Custom Fields"))
self.label_6.setText(_("Various artists:"))
self.label_7.setText(_("Standalone recordings:"))
Expand Down
80 changes: 80 additions & 0 deletions picard/ui/widgets/multicombobox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2024 Shubham Patel
#
# 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 PyQt6.QtCore import Qt
from PyQt6.QtGui import (
QStandardItem,
QStandardItemModel,
)
from PyQt6.QtWidgets import QComboBox


class MultiComboBox(QComboBox):
def __init__(self, parent=None):
super().__init__(parent)
self.setEditable(True)
self.lineEdit().setReadOnly(True)
self.setModel(QStandardItemModel(self))

# Connect to the dataChanged signal to update the text
self.model().dataChanged.connect(self.updateText)

def addItem(self, text: str, data=None):
item = QStandardItem()
item.setText(text)
item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable)
item.setData(Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole)
self.model().appendRow(item)

def addItems(self, items_list: list):
for text in items_list:
self.addItem(text)

def updateText(self):
selected_items = [self.model().item(i).text() for i in range(self.model().rowCount())
if self.model().item(i).checkState() == Qt.CheckState.Checked]
self.lineEdit().setText(", ".join(selected_items))
zas marked this conversation as resolved.
Show resolved Hide resolved

def show_selected_items(self):
selected_items = [self.model().item(i).text() for i in range(self.model().rowCount())
if self.model().item(i).checkState() == Qt.CheckState.Checked]
return selected_items

def showPopup(self):
super().showPopup()
# Set the state of each item in the dropdown
for i in range(self.model().rowCount()):
item = self.model().item(i)
combo_box_view = self.view()
combo_box_view.setRowHidden(i, False)
check_box = combo_box_view.indexWidget(item.index())
if check_box:
check_box.setChecked(item.checkState() == Qt.CheckState.Checked)

def hidePopup(self):
# Update the check state of each item based on the checkbox state
for i in range(self.model().rowCount()):
item = self.model().item(i)
combo_box_view = self.view()
check_box = combo_box_view.indexWidget(item.index())
if check_box:
item.setCheckState(Qt.CheckState.Checked if check_box.isChecked() else Qt.CheckState.Unchecked)
super().hidePopup()
1 change: 1 addition & 0 deletions test/formats/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
'replace_spaces_with_underscores': False,
'replace_dir_separator': '_',
'win_compat_replacements': {},
'formats_to_disable_date_sanitize': [],
}


Expand Down
12 changes: 12 additions & 0 deletions ui/options_metadata.ui
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="selected_formats_label">
<property name="text">
<string>Disable date sanitization for:</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="selected_formats">
</widget>
</item>
</layout>
</widget>
</item>
Expand Down Expand Up @@ -225,6 +236,7 @@
<tabstop>release_ars</tabstop>
<tabstop>track_ars</tabstop>
<tabstop>guess_tracknumber_and_title</tabstop>
<tabstop>selected_formats</tabstop>
<tabstop>va_name</tabstop>
<tabstop>va_name_default</tabstop>
<tabstop>nat_name</tabstop>
Expand Down
Loading