Skip to content

Commit 21eaa85

Browse files
committed
PICARD-2729: Allow disabling date sanitazation for APE and Vorbis tags
1 parent 1222821 commit 21eaa85

File tree

10 files changed

+142
-9
lines changed

10 files changed

+142
-9
lines changed

picard/formats/apev2.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ class APEv2File(File):
125125
'replaygain_reference_loudness': 'REPLAYGAIN_REFERENCE_LOUDNESS',
126126
}
127127
__rtranslate = {v.lower(): k for k, v in __translate.items()}
128+
sanitize_date = sanitize_date
128129

129130
def __init__(self, filename):
130131
super().__init__(filename)
@@ -136,6 +137,8 @@ def _load(self, filename):
136137
file = self._File(encode_filename(filename))
137138
metadata = Metadata()
138139
if file.tags:
140+
config = get_config()
141+
date_sanitize = self.NAME not in config.setting['formats_to_disable_date_sanitize']
139142
for origname, values in file.tags.items():
140143
name_lower = origname.lower()
141144
if (values.kind == mutagen.apev2.BINARY
@@ -160,7 +163,8 @@ def _load(self, filename):
160163
name = name_lower
161164
if name == 'year':
162165
name = 'date'
163-
value = sanitize_date(value)
166+
if date_sanitize:
167+
value = sanitize_date(value)
164168
elif name == 'track':
165169
name = 'tracknumber'
166170
track = value.split('/')

picard/formats/id3.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ class ID3File(File):
248248
__lrc_line_re_parse = re.compile(r'(\[\d\d:\d\d\.\d\d\d\])')
249249
__lrc_syllable_re_parse = re.compile(r'(<\d\d:\d\d\.\d\d\d>)')
250250
__lrc_both_re_parse = re.compile(r'(\[\d\d:\d\d\.\d\d\d\]|<\d\d:\d\d\.\d\d\d>)')
251+
sanitize_date = sanitize_date
251252

252253
def __init__(self, filename):
253254
super().__init__(filename)
@@ -278,6 +279,7 @@ def _load(self, filename):
278279
f = tags.pop(old)
279280
tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text))
280281
metadata = Metadata()
282+
date_sanitize = self.NAME not in config.setting['formats_to_disable_date_sanitize']
281283
for frame in tags.values():
282284
frameid = frame.FrameID
283285
if frameid in self.__translate:
@@ -395,9 +397,12 @@ def _load(self, filename):
395397
metadata.add('~rating', rating)
396398

397399
if 'date' in metadata:
398-
sanitized = sanitize_date(metadata.getall('date')[0])
399-
if sanitized:
400-
metadata['date'] = sanitized
400+
if date_sanitize:
401+
sanitized = sanitize_date(metadata.getall('date')[0])
402+
if sanitized:
403+
metadata['date'] = sanitized
404+
else:
405+
metadata['date'] = metadata.getall('date')[0]
401406

402407
self._info(metadata, file)
403408
return metadata

picard/formats/util.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ def supported_formats():
4545
return [(file_format.EXTENSIONS, file_format.NAME) for file_format in _formats]
4646

4747

48+
def formats_with_sanitize_date():
49+
for fmt in _formats:
50+
if hasattr(fmt, 'sanitize_date'):
51+
yield fmt
52+
53+
4854
def supported_extensions():
4955
"""Returns list of supported extensions."""
5056
return [ext for exts, name in supported_formats() for ext in exts]

picard/formats/vorbis.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,20 +129,24 @@ class VCommentFile(File):
129129
'waveformatextensible_channel_mask': '~waveformatextensible_channel_mask',
130130
}
131131
__rtranslate = {v: k for k, v in __translate.items()}
132+
sanitize_date = sanitize_date
132133

133134
def _load(self, filename):
134135
log.debug("Loading file %r", filename)
135136
config = get_config()
136137
file = self._File(encode_filename(filename))
137138
file.tags = file.tags or {}
138139
metadata = Metadata()
140+
config = get_config()
141+
date_sanitize = self.NAME not in config.setting['formats_to_disable_date_sanitize']
139142
for origname, values in file.tags.items():
140143
for value in values:
141144
value = value.rstrip('\0')
142145
name = origname
143146
if name in {'date', 'originaldate', 'releasedate'}:
144147
# YYYY-00-00 => YYYY
145-
value = sanitize_date(value)
148+
if date_sanitize:
149+
value = sanitize_date(value)
146150
elif name == 'performer' or name == 'comment':
147151
# transform "performer=Joe Barr (Piano)" to "performer:Piano=Joe Barr"
148152
name += ':'
@@ -280,7 +284,8 @@ def _save(self, filename, metadata):
280284
name = 'lyrics'
281285
elif name in {'date', 'originaldate', 'releasedate'}:
282286
# YYYY-00-00 => YYYY
283-
value = sanitize_date(value)
287+
if self.NAME not in config.setting['formats_to_disable_date_sanitize']:
288+
value = sanitize_date(value)
284289
elif name.startswith('performer:') or name.startswith('comment:'):
285290
# transform "performer:Piano=Joe Barr" to "performer=Joe Barr (Piano)"
286291
name, desc = name.split(':', 1)

picard/profile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class UserProfileGroups():
6969
SettingDesc('convert_punctuation', ['convert_punctuation']),
7070
SettingDesc('release_ars', ['release_ars']),
7171
SettingDesc('track_ars', ['track_ars']),
72+
SettingDesc('formats_to_disable_date_sanitize', ['selected_formats']),
7273
SettingDesc('guess_tracknumber_and_title', ['guess_tracknumber_and_title']),
7374
SettingDesc('va_name', ['va_name']),
7475
SettingDesc('nat_name', ['nat_name']),

picard/ui/options/metadata.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
SCRIPTS,
4545
scripts_sorted_by_localized_name,
4646
)
47+
from picard.formats.util import formats_with_sanitize_date
4748
from picard.i18n import (
4849
N_,
4950
gettext as _,
@@ -60,6 +61,7 @@
6061
from picard.ui.ui_multi_locale_selector import Ui_MultiLocaleSelector
6162
from picard.ui.ui_options_metadata import Ui_MetadataOptionsPage
6263
from picard.ui.util import qlistwidget_items
64+
from picard.ui.widgets.multicombobox import MultiComboBox
6365

6466

6567
def iter_sorted_locales(locales):
@@ -97,6 +99,7 @@ class MetadataOptionsPage(OptionsPage):
9799
ListOption('setting', 'script_exceptions', [], title=N_("Translation script exceptions")),
98100
BoolOption('setting', 'release_ars', True, title=N_("Use release relationships")),
99101
BoolOption('setting', 'track_ars', False, title=N_("Use track and release relationships")),
102+
Option('setting', 'formats_to_disable_date_sanitize', set(), title=N_("Formats to disable date sanitize")),
100103
BoolOption('setting', 'convert_punctuation', False, title=N_("Convert Unicode punctuation characters to ASCII")),
101104
BoolOption('setting', 'standardize_artists', False, title=N_("Use standardized artist names")),
102105
BoolOption('setting', 'standardize_instruments', True, title=N_("Use standardized instrument and vocal credits")),
@@ -122,6 +125,13 @@ def load(self):
122125
self.current_scripts = config.setting['script_exceptions']
123126
self.make_scripts_text()
124127
self.ui.translate_artist_names_script_exception.setChecked(config.setting['translate_artist_names_script_exception'])
128+
self.current_formats = config.setting['formats_to_disable_date_sanitize']
129+
fmt_names = sorted(fmt.NAME for fmt in formats_with_sanitize_date())
130+
dummy_widget = self.ui.selected_formats
131+
self.selected_formats = MultiComboBox(self)
132+
self.selected_formats.addItems(fmt_names)
133+
self.ui.verticalLayout_3.replaceWidget(dummy_widget, self.selected_formats)
134+
dummy_widget.deleteLater()
125135

126136
self.ui.convert_punctuation.setChecked(config.setting['convert_punctuation'])
127137
self.ui.release_ars.setChecked(config.setting['release_ars'])
@@ -157,6 +167,7 @@ def save(self):
157167
config.setting['convert_punctuation'] = self.ui.convert_punctuation.isChecked()
158168
config.setting['release_ars'] = self.ui.release_ars.isChecked()
159169
config.setting['track_ars'] = self.ui.track_ars.isChecked()
170+
config.setting['formats_to_disable_date_sanitize'] = self.current_formats
160171
config.setting['va_name'] = self.ui.va_name.text()
161172
nat_name = self.ui.nat_name.text()
162173
if nat_name != config.setting['nat_name']:

picard/ui/ui_options_metadata.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
#
33
# Created by: PyQt6 UI code generator 6.6.1
44
#
5-
# Automatically generated - do not edit.
6-
# Use `python setup.py build_ui` to update it.
5+
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
6+
# run again. Do not edit this file unless you know what you are doing.
77

88
from PyQt6 import (
99
QtCore,
@@ -77,6 +77,12 @@ def setupUi(self, MetadataOptionsPage):
7777
self.guess_tracknumber_and_title = QtWidgets.QCheckBox(parent=self.metadata_groupbox)
7878
self.guess_tracknumber_and_title.setObjectName("guess_tracknumber_and_title")
7979
self.verticalLayout_3.addWidget(self.guess_tracknumber_and_title)
80+
self.selected_formats_label = QtWidgets.QLabel(parent=self.metadata_groupbox)
81+
self.selected_formats_label.setObjectName("selected_formats_label")
82+
self.verticalLayout_3.addWidget(self.selected_formats_label)
83+
self.selected_formats = QtWidgets.QWidget(parent=self.metadata_groupbox)
84+
self.selected_formats.setObjectName("selected_formats")
85+
self.verticalLayout_3.addWidget(self.selected_formats)
8086
self.verticalLayout.addWidget(self.metadata_groupbox)
8187
self.custom_fields_groupbox = QtWidgets.QGroupBox(parent=MetadataOptionsPage)
8288
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum)
@@ -126,7 +132,8 @@ def setupUi(self, MetadataOptionsPage):
126132
MetadataOptionsPage.setTabOrder(self.convert_punctuation, self.release_ars)
127133
MetadataOptionsPage.setTabOrder(self.release_ars, self.track_ars)
128134
MetadataOptionsPage.setTabOrder(self.track_ars, self.guess_tracknumber_and_title)
129-
MetadataOptionsPage.setTabOrder(self.guess_tracknumber_and_title, self.va_name)
135+
MetadataOptionsPage.setTabOrder(self.guess_tracknumber_and_title, self.selected_formats)
136+
MetadataOptionsPage.setTabOrder(self.selected_formats, self.va_name)
130137
MetadataOptionsPage.setTabOrder(self.va_name, self.va_name_default)
131138
MetadataOptionsPage.setTabOrder(self.va_name_default, self.nat_name)
132139
MetadataOptionsPage.setTabOrder(self.nat_name, self.nat_name_default)
@@ -143,6 +150,7 @@ def retranslateUi(self, MetadataOptionsPage):
143150
self.release_ars.setText(_("Use release relationships"))
144151
self.track_ars.setText(_("Use track relationships"))
145152
self.guess_tracknumber_and_title.setText(_("Guess track number and title from filename if empty"))
153+
self.selected_formats_label.setText(_("Disable date sanitization for:"))
146154
self.custom_fields_groupbox.setTitle(_("Custom Fields"))
147155
self.label_6.setText(_("Various artists:"))
148156
self.label_7.setText(_("Standalone recordings:"))

picard/ui/widgets/multicombobox.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Picard, the next-generation MusicBrainz tagger
4+
#
5+
# Copyright (C) 2024 Shubham Patel
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License
9+
# as published by the Free Software Foundation; either version 2
10+
# of the License, or (at your option) any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU General Public License
18+
# along with this program; if not, write to the Free Software
19+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20+
21+
22+
from PyQt6.QtCore import Qt
23+
from PyQt6.QtGui import (
24+
QStandardItem,
25+
QStandardItemModel,
26+
)
27+
from PyQt6.QtWidgets import QComboBox
28+
29+
30+
class MultiComboBox(QComboBox):
31+
def __init__(self, parent=None):
32+
super().__init__(parent)
33+
self.setEditable(True)
34+
self.lineEdit().setReadOnly(True)
35+
self.setModel(QStandardItemModel(self))
36+
37+
# Connect to the dataChanged signal to update the text
38+
self.model().dataChanged.connect(self.updateText)
39+
40+
def addItem(self, text: str, data=None):
41+
item = QStandardItem()
42+
item.setText(text)
43+
item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable)
44+
item.setData(Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole)
45+
self.model().appendRow(item)
46+
47+
def addItems(self, items_list: list):
48+
for text in items_list:
49+
self.addItem(text)
50+
51+
def updateText(self):
52+
selected_items = [self.model().item(i).text() for i in range(self.model().rowCount())
53+
if self.model().item(i).checkState() == Qt.CheckState.Checked]
54+
self.lineEdit().setText(", ".join(selected_items))
55+
56+
def show_selected_items(self):
57+
selected_items = [self.model().item(i).text() for i in range(self.model().rowCount())
58+
if self.model().item(i).checkState() == Qt.CheckState.Checked]
59+
return selected_items
60+
61+
def showPopup(self):
62+
super().showPopup()
63+
# Set the state of each item in the dropdown
64+
for i in range(self.model().rowCount()):
65+
item = self.model().item(i)
66+
combo_box_view = self.view()
67+
combo_box_view.setRowHidden(i, False)
68+
check_box = combo_box_view.indexWidget(item.index())
69+
if check_box:
70+
check_box.setChecked(item.checkState() == Qt.CheckState.Checked)
71+
72+
def hidePopup(self):
73+
# Update the check state of each item based on the checkbox state
74+
for i in range(self.model().rowCount()):
75+
item = self.model().item(i)
76+
combo_box_view = self.view()
77+
check_box = combo_box_view.indexWidget(item.index())
78+
if check_box:
79+
item.setCheckState(Qt.CheckState.Checked if check_box.isChecked() else Qt.CheckState.Unchecked)
80+
super().hidePopup()

test/formats/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
'replace_spaces_with_underscores': False,
6666
'replace_dir_separator': '_',
6767
'win_compat_replacements': {},
68+
'formats_to_disable_date_sanitize': [],
6869
}
6970

7071

ui/options_metadata.ui

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@
130130
</property>
131131
</widget>
132132
</item>
133+
<item>
134+
<widget class="QLabel" name="selected_formats_label">
135+
<property name="text">
136+
<string>Disable date sanitization for:</string>
137+
</property>
138+
</widget>
139+
</item>
140+
<item>
141+
<widget class="QWidget" name="selected_formats">
142+
</widget>
143+
</item>
133144
</layout>
134145
</widget>
135146
</item>
@@ -225,6 +236,7 @@
225236
<tabstop>release_ars</tabstop>
226237
<tabstop>track_ars</tabstop>
227238
<tabstop>guess_tracknumber_and_title</tabstop>
239+
<tabstop>selected_formats</tabstop>
228240
<tabstop>va_name</tabstop>
229241
<tabstop>va_name_default</tabstop>
230242
<tabstop>nat_name</tabstop>

0 commit comments

Comments
 (0)