Merge pull request #1798 from phw/PICARD-2171-optional-guess-title

PICARD-2171: Optional guess title and tracknumber
This commit is contained in:
Philipp Wolfer
2021-05-01 08:50:08 +02:00
committed by GitHub
6 changed files with 100 additions and 22 deletions

View File

@@ -79,7 +79,7 @@ from picard.util import (
is_absolute_path,
samefile,
thread,
tracknum_from_filename,
tracknum_and_title_from_filename,
)
from picard.util.filenaming import (
make_short_filename,
@@ -198,6 +198,7 @@ class File(QtCore.QObject, Item):
def _loading_finished(self, callback, result=None, error=None):
if self.state != File.PENDING or self.tagger.stopping:
return
config = get_config()
if error is not None:
self.state = self.ERROR
self.error_append(str(error))
@@ -228,9 +229,11 @@ class File(QtCore.QObject, Item):
else:
self.clear_errors()
self.state = self.NORMAL
self._copy_loaded_metadata(result)
postprocessors = []
if config.setting["guess_tracknumber_and_title"]:
postprocessors.append(self._guess_tracknumber_and_title)
self._copy_loaded_metadata(result, postprocessors)
# use cached fingerprint from file metadata
config = get_config()
if not config.setting["ignore_existing_acoustid_fingerprints"]:
fingerprints = self.metadata.getall('acoustid_fingerprint')
if fingerprints:
@@ -239,24 +242,22 @@ class File(QtCore.QObject, Item):
self.update()
callback(self)
def _copy_loaded_metadata(self, metadata):
filename, _ = os.path.splitext(self.base_filename)
def _copy_loaded_metadata(self, metadata, postprocessors=None):
metadata['~length'] = format_time(metadata.length)
if 'tracknumber' not in metadata:
tracknumber = tracknum_from_filename(self.base_filename)
if tracknumber is not None:
tracknumber = str(tracknumber)
metadata['tracknumber'] = tracknumber
if 'title' not in metadata:
stripped_filename = filename.lstrip('0')
tnlen = len(tracknumber)
if stripped_filename[:tnlen] == tracknumber:
metadata['title'] = stripped_filename[tnlen:].lstrip()
if 'title' not in metadata:
metadata['title'] = filename
if postprocessors:
for processor in postprocessors:
processor(metadata)
self.orig_metadata = metadata
self.metadata.copy(metadata)
def _guess_tracknumber_and_title(self, metadata):
if 'tracknumber' not in metadata or 'title' not in metadata:
tracknumber, title = tracknum_and_title_from_filename(self.base_filename)
if 'tracknumber' not in metadata:
metadata['tracknumber'] = tracknumber
if 'title' not in metadata:
metadata['title'] = title
def copy_metadata(self, metadata, preserve_deleted=True):
acoustid = self.metadata["acoustid_id"]
saved_metadata = {}

View File

@@ -3,7 +3,7 @@
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2006-2008, 2011 Lukáš Lalinský
# Copyright (C) 2008-2009, 2018-2019 Philipp Wolfer
# Copyright (C) 2008-2009, 2018-2019, 2021 Philipp Wolfer
# Copyright (C) 2011 Johannes Weißl
# Copyright (C) 2011-2013 Michael Wiencek
# Copyright (C) 2013, 2018 Laurent Monin
@@ -75,6 +75,7 @@ class MetadataOptionsPage(OptionsPage):
BoolOption("setting", "convert_punctuation", True),
BoolOption("setting", "standardize_artists", False),
BoolOption("setting", "standardize_instruments", True),
BoolOption("setting", "guess_tracknumber_and_title", True),
]
def __init__(self, parent=None):
@@ -103,6 +104,7 @@ class MetadataOptionsPage(OptionsPage):
self.ui.nat_name.setText(config.setting["nat_name"])
self.ui.standardize_artists.setChecked(config.setting["standardize_artists"])
self.ui.standardize_instruments.setChecked(config.setting["standardize_instruments"])
self.ui.guess_tracknumber_and_title.setChecked(config.setting["guess_tracknumber_and_title"])
def save(self):
config = get_config()
@@ -119,6 +121,7 @@ class MetadataOptionsPage(OptionsPage):
self.tagger.nats.update()
config.setting["standardize_artists"] = self.ui.standardize_artists.isChecked()
config.setting["standardize_instruments"] = self.ui.standardize_instruments.isChecked()
config.setting["guess_tracknumber_and_title"] = self.ui.guess_tracknumber_and_title.isChecked()
def set_va_name_default(self):
self.ui.va_name.setText(self.options[0].default)

View File

@@ -3,8 +3,10 @@
# Automatically generated - don't edit.
# Use `python setup.py build_ui` to update it.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MetadataOptionsPage(object):
def setupUi(self, MetadataOptionsPage):
MetadataOptionsPage.setObjectName("MetadataOptionsPage")
@@ -43,6 +45,9 @@ class Ui_MetadataOptionsPage(object):
self.track_ars = QtWidgets.QCheckBox(self.metadata_groupbox)
self.track_ars.setObjectName("track_ars")
self.verticalLayout_3.addWidget(self.track_ars)
self.guess_tracknumber_and_title = QtWidgets.QCheckBox(self.metadata_groupbox)
self.guess_tracknumber_and_title.setObjectName("guess_tracknumber_and_title")
self.verticalLayout_3.addWidget(self.guess_tracknumber_and_title)
self.verticalLayout.addWidget(self.metadata_groupbox)
self.custom_fields_groupbox = QtWidgets.QGroupBox(MetadataOptionsPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Maximum)
@@ -100,9 +105,9 @@ class Ui_MetadataOptionsPage(object):
self.convert_punctuation.setText(_("Convert Unicode punctuation characters to ASCII"))
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.custom_fields_groupbox.setTitle(_("Custom Fields"))
self.label_6.setText(_("Various artists:"))
self.label_7.setText(_("Non-album tracks:"))
self.nat_name_default.setText(_("Default"))
self.va_name_default.setText(_("Default"))

View File

@@ -4,7 +4,7 @@
#
# Copyright (C) 2004 Robert Kaye
# Copyright (C) 2006-2009, 2011-2012, 2014 Lukáš Lalinský
# Copyright (C) 2008-2011, 2014, 2018-2020 Philipp Wolfer
# Copyright (C) 2008-2011, 2014, 2018-2021 Philipp Wolfer
# Copyright (C) 2009 Carlin Mangar
# Copyright (C) 2009 david
# Copyright (C) 2010 fatih
@@ -392,7 +392,7 @@ def tracknum_from_filename(base_filename):
"""Guess and extract track number from filename
Returns `None` if none found, the number as integer else
"""
filename, _ = os.path.splitext(base_filename)
filename, _ext = os.path.splitext(base_filename)
for r in _tracknum_regexps:
match = re.search(r, filename, re.I)
if match:
@@ -409,6 +409,26 @@ def tracknum_from_filename(base_filename):
return None
def tracknum_and_title_from_filename(base_filename):
"""Guess tracknumber and title from filename.
Uses `tracknum_from_filename` to guess the tracknumber. The filename is used
as the title. If the tracknumber is at the beginning of the title it gets stripped.
Returns a tuple `(tracknumber, title)`.
"""
filename, _ext = os.path.splitext(base_filename)
title = filename
tracknumber = tracknum_from_filename(base_filename)
if tracknumber is not None:
tracknumber = str(tracknumber)
stripped_filename = filename.lstrip('0')
tnlen = len(tracknumber)
if stripped_filename[:tnlen] == tracknumber:
title = stripped_filename[tnlen:].lstrip()
return (tracknumber, title)
def is_hidden(filepath):
"""Test whether a file or directory is hidden.
A file is considered hidden if it starts with a dot

View File

@@ -4,7 +4,7 @@
#
# Copyright (C) 2006-2007 Lukáš Lalinský
# Copyright (C) 2010 fatih
# Copyright (C) 2010-2011, 2014, 2018-2020 Philipp Wolfer
# Copyright (C) 2010-2011, 2014, 2018-2021 Philipp Wolfer
# Copyright (C) 2012, 2014, 2018 Wieland Hoffmann
# Copyright (C) 2013 Ionuț Ciocîrlan
# Copyright (C) 2013-2014, 2018-2020 Laurent Monin
@@ -46,6 +46,8 @@ from picard.util import (
iter_unique,
limited_join,
sort_by_similarity,
tracknum_and_title_from_filename,
tracknum_from_filename,
uniqify,
)
@@ -429,3 +431,43 @@ class IterUniqueTest(PicardTestCase):
result = iter_unique(items)
self.assertTrue(isinstance(result, Iterator))
self.assertEqual([1, 2, 3, 4], list(result))
class TracknumFromFilenameTest(PicardTestCase):
def test_returns_expected_tracknumber(self):
tests = (
(None, 'Foo.mp3'),
(1, 'Foo 0001.mp3'),
(99, '99 Foo.mp3'),
(42, '42. Foo.mp3'),
(None, '20000 Feet.mp3'),
(242, 'track no 242.mp3'),
(242, 'track-242.mp3'),
(242, 'track nr 242.mp3'),
(242, 'track_242.mp3'),
# (None, '30,000 Pounds of Bananas.mp3'),
# (None, 'Dalas 1 PM.mp3'),
# (None, "Don't Stop the 80's.mp3"),
# (None, '99 Luftballons.mp3'),
# (None, 'Symphony no. 5 in D minor.mp3'),
)
for expected, filename in tests:
tracknumber = tracknum_from_filename(filename)
self.assertEqual(expected, tracknumber)
class TracknumAndTitleFromFilenameTest(PicardTestCase):
def test_returns_expected_tracknumber(self):
tests = (
((None, 'Foo'), 'Foo.mp3'),
(('1', 'Track 0001'), 'Track 0001.mp3'),
(('99', 'Foo'), '99 Foo.mp3'),
(('42', 'Foo'), '0000042 Foo.mp3'),
((None, '20000 Feet'), '20000 Feet.mp3'),
# ((None, '20,000 Feet'), '20,000 Feet.mp3'),
)
for expected, filename in tests:
result = tracknum_and_title_from_filename(filename)
self.assertEqual(expected, result)

View File

@@ -77,6 +77,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="guess_tracknumber_and_title">
<property name="text">
<string>Guess track number and title from filename if empty</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>