From b5216a06f489b5da8c1e66b86603eedfc9d15856 Mon Sep 17 00:00:00 2001 From: Vrihub Date: Sat, 15 Jun 2024 20:52:55 +0200 Subject: [PATCH 01/11] Proposed fix for issue #5218 Check for existence of "title" matching group before using it --- beetsplug/fromfilename.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index 103e82901..70b9d41fb 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -98,6 +98,7 @@ def apply_matches(d, log): # Given both an "artist" and "title" field, assume that one is # *actually* the artist, which must be uniform, and use the other # for the title. This, of course, won't work for VA albums. + # Only check for "artist": patterns containing it, also contain "title" if "artist" in keys: if equal_fields(d, "artist"): artist = some_map["artist"] @@ -113,14 +114,15 @@ def apply_matches(d, log): if not item.artist: item.artist = artist log.info("Artist replaced with: {}".format(item.artist)) - - # No artist field: remaining field is the title. - else: + # otherwise, if the pattern contains "title", use that for title_field + elif "title" in keys: title_field = "title" + else: + title_field = None - # Apply the title and track. + # Apply the title and track, if any. for item in d: - if bad_title(item.title): + if title_field and bad_title(item.title): item.title = str(d[item][title_field]) log.info("Title replaced with: {}".format(item.title)) From 09660426a887c69fc6269c20a4548cd65a308df3 Mon Sep 17 00:00:00 2001 From: Vrihub Date: Sat, 15 Jun 2024 20:56:40 +0200 Subject: [PATCH 02/11] Logging: add message about the pattern being tried --- beetsplug/fromfilename.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index 70b9d41fb..598cbeda9 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -162,6 +162,7 @@ class FromFilenamePlugin(plugins.BeetsPlugin): # Look for useful information in the filenames. for pattern in PATTERNS: + self._log.debug("Trying pattern: {}".format(pattern)) d = all_matches(names, pattern) if d: apply_matches(d, self._log) From e6b773561b1cb588e6e3204030092af62859bd1a Mon Sep 17 00:00:00 2001 From: Vrihub Date: Sat, 15 Jun 2024 20:58:41 +0200 Subject: [PATCH 03/11] Refactor regexps in PATTERNS --- beetsplug/fromfilename.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index 598cbeda9..0f34fd09c 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -25,12 +25,10 @@ from beets.util import displayable_path # Filename field extraction patterns. PATTERNS = [ # Useful patterns. - r"^(?P.+)[\-_](?P.+)[\-_](?P<tag>.*)$", - r"^(?P<track>\d+)[\s.\-_]+(?P<artist>.+)[\-_](?P<title>.+)[\-_](?P<tag>.*)$", - r"^(?P<artist>.+)[\-_](?P<title>.+)$", - r"^(?P<track>\d+)[\s.\-_]+(?P<artist>.+)[\-_](?P<title>.+)$", - r"^(?P<track>\d+)[\s.\-_]+(?P<title>.+)$", - r"^(?P<track>\d+)\s+(?P<title>.+)$", + (r"^(?P<track>\d+)\.?\s*-\s*(?P<artist>.+?)\s*-\s*(?P<title>.+?)" + r"(\s*-\s*(?P<tag>.*))?$"), + r"^(?P<artist>.+?)\s*-\s*(?P<title>.+?)(\s*-\s*(?P<tag>.*))?$", + r"^(?P<track>\d+)\.?[\s\-_]+(?P<title>.+)$", r"^(?P<title>.+) by (?P<artist>.+)$", r"^(?P<track>\d+).*$", r"^(?P<title>.+)$", From 2f2680fa8d9431e3f2bebef82c2feb7aad9b1248 Mon Sep 17 00:00:00 2001 From: Vrihub <Vrihub@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:02:33 +0200 Subject: [PATCH 04/11] Added tests for the fromfilename plugin --- test/plugins/test_fromfilename.py | 143 ++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 test/plugins/test_fromfilename.py diff --git a/test/plugins/test_fromfilename.py b/test/plugins/test_fromfilename.py new file mode 100644 index 000000000..3dc600ced --- /dev/null +++ b/test/plugins/test_fromfilename.py @@ -0,0 +1,143 @@ +# This file is part of beets. +# Copyright 2016, Jan-Erik Dahlin. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Tests for the fromfilename plugin. +""" + +import unittest +from unittest.mock import Mock +from beetsplug import fromfilename + + +class FromfilenamePluginTest(unittest.TestCase): + + def setUp(self): + """Create mock objects for import session and task.""" + self.session = Mock() + + item1config = {'path': '', 'track': 0, 'artist': '', 'title': ''} + self.item1 = Mock(**item1config) + + item2config = {'path': '', 'track': 0, 'artist': '', 'title': ''} + self.item2 = Mock(**item2config) + + taskconfig = {'is_album': True, 'items': [self.item1, self.item2]} + self.task = Mock(**taskconfig) + + def tearDown(self): + del self.session, self.task, self.item1, self.item2 + + def test_sep_sds(self): + """Test filenames that use " - " as separator.""" + + self.item1.path = "/music/files/01 - Artist Name - Song One.m4a" + self.item2.path = "/music/files/02. - Artist Name - Song Two.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 1) + self.assertEqual(self.task.items[1].track, 2) + self.assertEqual(self.task.items[0].artist, "Artist Name") + self.assertEqual(self.task.items[1].artist, "Artist Name") + self.assertEqual(self.task.items[0].title, "Song One") + self.assertEqual(self.task.items[1].title, "Song Two") + + def test_sep_dash(self): + """Test filenames that use "-" as separator.""" + + self.item1.path = "/music/files/01-Artist_Name-Song_One.m4a" + self.item2.path = "/music/files/02.-Artist_Name-Song_Two.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 1) + self.assertEqual(self.task.items[1].track, 2) + self.assertEqual(self.task.items[0].artist, "Artist_Name") + self.assertEqual(self.task.items[1].artist, "Artist_Name") + self.assertEqual(self.task.items[0].title, "Song_One") + self.assertEqual(self.task.items[1].title, "Song_Two") + + def test_track_title(self): + """Test filenames including track and title.""" + + self.item1.path = "/music/files/01 - Song_One.m4a" + self.item2.path = "/music/files/02. Song_Two.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 1) + self.assertEqual(self.task.items[1].track, 2) + self.assertEqual(self.task.items[0].artist, "") + self.assertEqual(self.task.items[1].artist, "") + self.assertEqual(self.task.items[0].title, "Song_One") + self.assertEqual(self.task.items[1].title, "Song_Two") + + def test_title_by_artist(self): + """Test filenames including title by artist.""" + + self.item1.path = "/music/files/Song One by The Artist.m4a" + self.item2.path = "/music/files/Song Two by The Artist.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 0) + self.assertEqual(self.task.items[1].track, 0) + self.assertEqual(self.task.items[0].artist, "The Artist") + self.assertEqual(self.task.items[1].artist, "The Artist") + self.assertEqual(self.task.items[0].title, "Song One") + self.assertEqual(self.task.items[1].title, "Song Two") + + def test_track_only(self): + """Test filenames including only track.""" + + self.item1.path = "/music/files/01.m4a" + self.item2.path = "/music/files/02.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 1) + self.assertEqual(self.task.items[1].track, 2) + self.assertEqual(self.task.items[0].artist, "") + self.assertEqual(self.task.items[1].artist, "") + self.assertEqual(self.task.items[0].title, "01") + self.assertEqual(self.task.items[1].title, "02") + + def test_title_only(self): + """Test filenames including only title.""" + + self.item1.path = "/music/files/Song One.m4a" + self.item2.path = "/music/files/Song Two.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 0) + self.assertEqual(self.task.items[1].track, 0) + self.assertEqual(self.task.items[0].artist, "") + self.assertEqual(self.task.items[1].artist, "") + self.assertEqual(self.task.items[0].title, "Song One") + self.assertEqual(self.task.items[1].title, "Song Two") + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") From c30f9603eb175924f07d666eaea3f069cc9b7096 Mon Sep 17 00:00:00 2001 From: Vrihub <Vrihub@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:31:42 +0200 Subject: [PATCH 05/11] Fix format and lint errors --- beetsplug/fromfilename.py | 8 +-- test/plugins/test_fromfilename.py | 83 +++++++++++++++---------------- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index 42544e655..5b8bafc44 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -25,8 +25,10 @@ from beets.util import displayable_path # Filename field extraction patterns. PATTERNS = [ # Useful patterns. - (r"^(?P<track>\d+)\.?\s*-\s*(?P<artist>.+?)\s*-\s*(?P<title>.+?)" - r"(\s*-\s*(?P<tag>.*))?$"), + ( + r"^(?P<track>\d+)\.?\s*-\s*(?P<artist>.+?)\s*-\s*(?P<title>.+?)" + r"(\s*-\s*(?P<tag>.*))?$" + ), r"^(?P<artist>.+?)\s*-\s*(?P<title>.+?)(\s*-\s*(?P<tag>.*))?$", r"^(?P<track>\d+)\.?[\s\-_]+(?P<title>.+)$", r"^(?P<title>.+) by (?P<artist>.+)$", @@ -160,7 +162,7 @@ class FromFilenamePlugin(plugins.BeetsPlugin): # Look for useful information in the filenames. for pattern in PATTERNS: - self._log.debug("Trying pattern: {}".format(pattern)) + self._log.debug(f"Trying pattern: {pattern}") d = all_matches(names, pattern) if d: apply_matches(d, self._log) diff --git a/test/plugins/test_fromfilename.py b/test/plugins/test_fromfilename.py index 3dc600ced..511f63d38 100644 --- a/test/plugins/test_fromfilename.py +++ b/test/plugins/test_fromfilename.py @@ -12,27 +12,26 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Tests for the fromfilename plugin. -""" +"""Tests for the fromfilename plugin.""" import unittest from unittest.mock import Mock + from beetsplug import fromfilename class FromfilenamePluginTest(unittest.TestCase): - def setUp(self): """Create mock objects for import session and task.""" self.session = Mock() - item1config = {'path': '', 'track': 0, 'artist': '', 'title': ''} + item1config = {"path": "", "track": 0, "artist": "", "title": ""} self.item1 = Mock(**item1config) - item2config = {'path': '', 'track': 0, 'artist': '', 'title': ''} + item2config = {"path": "", "track": 0, "artist": "", "title": ""} self.item2 = Mock(**item2config) - taskconfig = {'is_album': True, 'items': [self.item1, self.item2]} + taskconfig = {"is_album": True, "items": [self.item1, self.item2]} self.task = Mock(**taskconfig) def tearDown(self): @@ -47,12 +46,12 @@ class FromfilenamePluginTest(unittest.TestCase): f = fromfilename.FromFilenamePlugin() f.filename_task(self.task, self.session) - self.assertEqual(self.task.items[0].track, 1) - self.assertEqual(self.task.items[1].track, 2) - self.assertEqual(self.task.items[0].artist, "Artist Name") - self.assertEqual(self.task.items[1].artist, "Artist Name") - self.assertEqual(self.task.items[0].title, "Song One") - self.assertEqual(self.task.items[1].title, "Song Two") + assert self.task.items[0].track == 1 + assert self.task.items[1].track == 2 + assert self.task.items[0].artist == "Artist Name" + assert self.task.items[1].artist == "Artist Name" + assert self.task.items[0].title == "Song One" + assert self.task.items[1].title == "Song Two" def test_sep_dash(self): """Test filenames that use "-" as separator.""" @@ -63,12 +62,12 @@ class FromfilenamePluginTest(unittest.TestCase): f = fromfilename.FromFilenamePlugin() f.filename_task(self.task, self.session) - self.assertEqual(self.task.items[0].track, 1) - self.assertEqual(self.task.items[1].track, 2) - self.assertEqual(self.task.items[0].artist, "Artist_Name") - self.assertEqual(self.task.items[1].artist, "Artist_Name") - self.assertEqual(self.task.items[0].title, "Song_One") - self.assertEqual(self.task.items[1].title, "Song_Two") + assert self.task.items[0].track == 1 + assert self.task.items[1].track == 2 + assert self.task.items[0].artist == "Artist_Name" + assert self.task.items[1].artist == "Artist_Name" + assert self.task.items[0].title == "Song_One" + assert self.task.items[1].title == "Song_Two" def test_track_title(self): """Test filenames including track and title.""" @@ -79,12 +78,12 @@ class FromfilenamePluginTest(unittest.TestCase): f = fromfilename.FromFilenamePlugin() f.filename_task(self.task, self.session) - self.assertEqual(self.task.items[0].track, 1) - self.assertEqual(self.task.items[1].track, 2) - self.assertEqual(self.task.items[0].artist, "") - self.assertEqual(self.task.items[1].artist, "") - self.assertEqual(self.task.items[0].title, "Song_One") - self.assertEqual(self.task.items[1].title, "Song_Two") + assert self.task.items[0].track == 1 + assert self.task.items[1].track == 2 + assert self.task.items[0].artist == "" + assert self.task.items[1].artist == "" + assert self.task.items[0].title == "Song_One" + assert self.task.items[1].title == "Song_Two" def test_title_by_artist(self): """Test filenames including title by artist.""" @@ -95,12 +94,12 @@ class FromfilenamePluginTest(unittest.TestCase): f = fromfilename.FromFilenamePlugin() f.filename_task(self.task, self.session) - self.assertEqual(self.task.items[0].track, 0) - self.assertEqual(self.task.items[1].track, 0) - self.assertEqual(self.task.items[0].artist, "The Artist") - self.assertEqual(self.task.items[1].artist, "The Artist") - self.assertEqual(self.task.items[0].title, "Song One") - self.assertEqual(self.task.items[1].title, "Song Two") + assert self.task.items[0].track == 0 + assert self.task.items[1].track == 0 + assert self.task.items[0].artist == "The Artist" + assert self.task.items[1].artist == "The Artist" + assert self.task.items[0].title == "Song One" + assert self.task.items[1].title == "Song Two" def test_track_only(self): """Test filenames including only track.""" @@ -111,12 +110,12 @@ class FromfilenamePluginTest(unittest.TestCase): f = fromfilename.FromFilenamePlugin() f.filename_task(self.task, self.session) - self.assertEqual(self.task.items[0].track, 1) - self.assertEqual(self.task.items[1].track, 2) - self.assertEqual(self.task.items[0].artist, "") - self.assertEqual(self.task.items[1].artist, "") - self.assertEqual(self.task.items[0].title, "01") - self.assertEqual(self.task.items[1].title, "02") + assert self.task.items[0].track == 1 + assert self.task.items[1].track == 2 + assert self.task.items[0].artist == "" + assert self.task.items[1].artist == "" + assert self.task.items[0].title == "01" + assert self.task.items[1].title == "02" def test_title_only(self): """Test filenames including only title.""" @@ -127,12 +126,12 @@ class FromfilenamePluginTest(unittest.TestCase): f = fromfilename.FromFilenamePlugin() f.filename_task(self.task, self.session) - self.assertEqual(self.task.items[0].track, 0) - self.assertEqual(self.task.items[1].track, 0) - self.assertEqual(self.task.items[0].artist, "") - self.assertEqual(self.task.items[1].artist, "") - self.assertEqual(self.task.items[0].title, "Song One") - self.assertEqual(self.task.items[1].title, "Song Two") + assert self.task.items[0].track == 0 + assert self.task.items[1].track == 0 + assert self.task.items[0].artist == "" + assert self.task.items[1].artist == "" + assert self.task.items[0].title == "Song One" + assert self.task.items[1].title == "Song Two" def suite(): From da08978eca23cea19948295c12ffe63f0e62ad61 Mon Sep 17 00:00:00 2001 From: Vrihub <Vrihub@users.noreply.github.com> Date: Tue, 16 Sep 2025 20:20:43 +0200 Subject: [PATCH 06/11] Add entry to changelog.rst --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9a514f9f0..8d7f8c5a9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,9 @@ New features: Bug fixes: +- :doc:`/plugins/fromfilename`: Fix :bug:`5218` and improve the code + (refactor regexps, allow for more cases, add some logging) + For packagers: Other changes: From ae9489cb928f635a20dbde5c350c56139bfae5e0 Mon Sep 17 00:00:00 2001 From: Vrihub <Vrihub@users.noreply.github.com> Date: Tue, 16 Sep 2025 20:48:40 +0200 Subject: [PATCH 07/11] Fix formatting to make poe format-docs happy --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8d7f8c5a9..bedcac9d7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,8 +11,8 @@ New features: Bug fixes: -- :doc:`/plugins/fromfilename`: Fix :bug:`5218` and improve the code - (refactor regexps, allow for more cases, add some logging) +- :doc:`/plugins/fromfilename`: Fix :bug:`5218` and improve the code (refactor + regexps, allow for more cases, add some logging) For packagers: From 80ffa4879decc683829162efd238af8d15151f37 Mon Sep 17 00:00:00 2001 From: Vrihub <Vrihub@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:46:26 +0200 Subject: [PATCH 08/11] Improve regexp and module docstring --- beetsplug/fromfilename.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index 5b8bafc44..c3fb4bc6b 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -12,8 +12,8 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""If the title is empty, try to extract track and title from the -filename. +"""If the title is empty, try to extract it from the filename +(possibly also extract track and artist) """ import os @@ -30,7 +30,7 @@ PATTERNS = [ r"(\s*-\s*(?P<tag>.*))?$" ), r"^(?P<artist>.+?)\s*-\s*(?P<title>.+?)(\s*-\s*(?P<tag>.*))?$", - r"^(?P<track>\d+)\.?[\s\-_]+(?P<title>.+)$", + r"^(?P<track>\d+)\.?[\s_-]+(?P<title>.+)$", r"^(?P<title>.+) by (?P<artist>.+)$", r"^(?P<track>\d+).*$", r"^(?P<title>.+)$", From 638afc3d2cef3cdc337364f5fd6c4ae553f4f6ef Mon Sep 17 00:00:00 2001 From: Vrihub <Vrihub@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:54:33 +0200 Subject: [PATCH 09/11] Refactor tests using pytest --- test/plugins/test_fromfilename.py | 195 ++++++++++++------------------ 1 file changed, 76 insertions(+), 119 deletions(-) diff --git a/test/plugins/test_fromfilename.py b/test/plugins/test_fromfilename.py index 511f63d38..f13e88aea 100644 --- a/test/plugins/test_fromfilename.py +++ b/test/plugins/test_fromfilename.py @@ -1,5 +1,4 @@ # This file is part of beets. -# Copyright 2016, Jan-Erik Dahlin. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -14,129 +13,87 @@ """Tests for the fromfilename plugin.""" -import unittest -from unittest.mock import Mock +import pytest from beetsplug import fromfilename -class FromfilenamePluginTest(unittest.TestCase): - def setUp(self): - """Create mock objects for import session and task.""" - self.session = Mock() - - item1config = {"path": "", "track": 0, "artist": "", "title": ""} - self.item1 = Mock(**item1config) - - item2config = {"path": "", "track": 0, "artist": "", "title": ""} - self.item2 = Mock(**item2config) - - taskconfig = {"is_album": True, "items": [self.item1, self.item2]} - self.task = Mock(**taskconfig) - - def tearDown(self): - del self.session, self.task, self.item1, self.item2 - - def test_sep_sds(self): - """Test filenames that use " - " as separator.""" - - self.item1.path = "/music/files/01 - Artist Name - Song One.m4a" - self.item2.path = "/music/files/02. - Artist Name - Song Two.m4a" - - f = fromfilename.FromFilenamePlugin() - f.filename_task(self.task, self.session) - - assert self.task.items[0].track == 1 - assert self.task.items[1].track == 2 - assert self.task.items[0].artist == "Artist Name" - assert self.task.items[1].artist == "Artist Name" - assert self.task.items[0].title == "Song One" - assert self.task.items[1].title == "Song Two" - - def test_sep_dash(self): - """Test filenames that use "-" as separator.""" - - self.item1.path = "/music/files/01-Artist_Name-Song_One.m4a" - self.item2.path = "/music/files/02.-Artist_Name-Song_Two.m4a" - - f = fromfilename.FromFilenamePlugin() - f.filename_task(self.task, self.session) - - assert self.task.items[0].track == 1 - assert self.task.items[1].track == 2 - assert self.task.items[0].artist == "Artist_Name" - assert self.task.items[1].artist == "Artist_Name" - assert self.task.items[0].title == "Song_One" - assert self.task.items[1].title == "Song_Two" - - def test_track_title(self): - """Test filenames including track and title.""" - - self.item1.path = "/music/files/01 - Song_One.m4a" - self.item2.path = "/music/files/02. Song_Two.m4a" - - f = fromfilename.FromFilenamePlugin() - f.filename_task(self.task, self.session) - - assert self.task.items[0].track == 1 - assert self.task.items[1].track == 2 - assert self.task.items[0].artist == "" - assert self.task.items[1].artist == "" - assert self.task.items[0].title == "Song_One" - assert self.task.items[1].title == "Song_Two" - - def test_title_by_artist(self): - """Test filenames including title by artist.""" - - self.item1.path = "/music/files/Song One by The Artist.m4a" - self.item2.path = "/music/files/Song Two by The Artist.m4a" - - f = fromfilename.FromFilenamePlugin() - f.filename_task(self.task, self.session) - - assert self.task.items[0].track == 0 - assert self.task.items[1].track == 0 - assert self.task.items[0].artist == "The Artist" - assert self.task.items[1].artist == "The Artist" - assert self.task.items[0].title == "Song One" - assert self.task.items[1].title == "Song Two" - - def test_track_only(self): - """Test filenames including only track.""" - - self.item1.path = "/music/files/01.m4a" - self.item2.path = "/music/files/02.m4a" - - f = fromfilename.FromFilenamePlugin() - f.filename_task(self.task, self.session) - - assert self.task.items[0].track == 1 - assert self.task.items[1].track == 2 - assert self.task.items[0].artist == "" - assert self.task.items[1].artist == "" - assert self.task.items[0].title == "01" - assert self.task.items[1].title == "02" - - def test_title_only(self): - """Test filenames including only title.""" - - self.item1.path = "/music/files/Song One.m4a" - self.item2.path = "/music/files/Song Two.m4a" - - f = fromfilename.FromFilenamePlugin() - f.filename_task(self.task, self.session) - - assert self.task.items[0].track == 0 - assert self.task.items[1].track == 0 - assert self.task.items[0].artist == "" - assert self.task.items[1].artist == "" - assert self.task.items[0].title == "Song One" - assert self.task.items[1].title == "Song Two" +class Session: + pass -def suite(): - return unittest.TestLoader().loadTestsFromName(__name__) +class Item: + def __init__(self, path): + self.path = path + self.track = 0 + self.artist = "" + self.title = "" -if __name__ == "__main__": - unittest.main(defaultTest="suite") +class Task: + def __init__(self, items): + self.items = items + self.is_album = True + + +@pytest.mark.parametrize( + "song1, song2", + [ + ( + ( + "/tmp/01 - The Artist - Song One.m4a", + 1, + "The Artist", + "Song One", + ), + ( + "/tmp/02. - The Artist - Song Two.m4a", + 2, + "The Artist", + "Song Two", + ), + ), + ( + ("/tmp/01-The_Artist-Song_One.m4a", 1, "The_Artist", "Song_One"), + ("/tmp/02.-The_Artist-Song_Two.m4a", 2, "The_Artist", "Song_Two"), + ), + ( + ("/tmp/01 - Song_One.m4a", 1, "", "Song_One"), + ("/tmp/02. - Song_Two.m4a", 2, "", "Song_Two"), + ), + ( + ("/tmp/Song One by The Artist.m4a", 0, "The Artist", "Song One"), + ("/tmp/Song Two by The Artist.m4a", 0, "The Artist", "Song Two"), + ), + (("/tmp/01.m4a", 1, "", "01"), ("/tmp/02.m4a", 2, "", "02")), + ( + ("/tmp/Song One.m4a", 0, "", "Song One"), + ("/tmp/Song Two.m4a", 0, "", "Song Two"), + ), + ], +) +def test_fromfilename(song1, song2): + """ + Each "song" is a tuple of path, expected track number, expected artist, + expected title. + + We use two songs for each test for two reasons: + - The plugin needs more than one item to look for uniform strings in paths + in order to guess if the string describes an artist or a title. + - Sometimes we allow for an optional "." after the track number in paths. + """ + + session = Session() + item1 = Item(song1[0]) + item2 = Item(song2[0]) + task = Task([item1, item2]) + + f = fromfilename.FromFilenamePlugin() + f.filename_task(task, session) + + assert task.items[0].track == song1[1] + assert task.items[0].artist == song1[2] + assert task.items[0].title == song1[3] + assert task.items[1].track == song2[1] + assert task.items[1].artist == song2[2] + assert task.items[1].title == song2[3] From b8ae222dc4b5b3fea97e9c7512f2017294ff3aec Mon Sep 17 00:00:00 2001 From: Vrihub <Vrihub@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:04:59 +0200 Subject: [PATCH 10/11] Mention tests in changelog entry. --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index af1edaa80..d01148286 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,8 +11,8 @@ New features: Bug fixes: -- :doc:`/plugins/fromfilename`: Fix :bug:`5218` and improve the code (refactor - regexps, allow for more cases, add some logging) +- :doc:`/plugins/fromfilename`: Fix :bug:`5218`, improve the code (refactor + regexps, allow for more cases, add some logging), add tests. For packagers: From 152cafbf69de036253898a11349b8d59ecb6618b Mon Sep 17 00:00:00 2001 From: J0J0 Todos <jojo@peek-a-boo.at> Date: Mon, 6 Oct 2025 11:35:49 +0200 Subject: [PATCH 11/11] fromfilename: Fix tiny changelog formatting issue --- docs/changelog.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9c2912e0d..1bd843688 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -26,7 +26,6 @@ New features: Bug fixes: - - :doc:`plugins/spotify` Ensure ``spotifysync`` keeps popularity, ISRC, and related fields current even when audio features requests fail. :bug:`6061` - :doc:`plugins/spotify` Fixed an issue where track matching and lookups could