beets/test/plugins/test_fromfilename.py

655 lines
20 KiB
Python

# This file is part of beets.
#
# 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."""
from dataclasses import dataclass
import pytest
from beets.library import Item
from beets.test.helper import PluginMixin
from beetsplug.fromfilename import FromFilenamePlugin
class Session:
pass
def mock_item(**kwargs):
defaults = dict(
title="",
artist="",
albumartist="",
album="",
disc=0,
track=0,
catalognum="",
media="",
mtime=12345,
)
return Item(**{**defaults, **kwargs})
@dataclass
class Task:
items: list[Item]
is_album: bool = True
@pytest.mark.parametrize(
"text,matchgroup",
[
("3", {"disc": None, "track": "3", "artist": None, "title": "3"}),
("04", {"disc": None, "track": "04", "artist": None, "title": "04"}),
("6.", {"disc": None, "track": "6", "artist": None, "title": "6"}),
("3.5", {"disc": "3", "track": "5", "artist": None, "title": None}),
("1-02", {"disc": "1", "track": "02", "artist": None, "title": None}),
("100-4", {"disc": "100", "track": "4", "artist": None, "title": None}),
(
"04.Title",
{"disc": None, "track": "04", "artist": None, "title": "Title"},
),
(
"5_-_Title",
{"disc": None, "track": "5", "artist": None, "title": "Title"},
),
(
"1-02 Title",
{"disc": "1", "track": "02", "artist": None, "title": "Title"},
),
(
"3.5 - Title",
{"disc": "3", "track": "5", "artist": None, "title": "Title"},
),
(
"5_-_Artist_-_Title",
{"disc": None, "track": "5", "artist": "Artist", "title": "Title"},
),
(
"3-8- Artist-Title",
{"disc": "3", "track": "8", "artist": "Artist", "title": "Title"},
),
(
"4-3 - Artist Name - Title",
{
"disc": "4",
"track": "3",
"artist": "Artist Name",
"title": "Title",
},
),
(
"4-3_-_Artist_Name_-_Title",
{
"disc": "4",
"track": "3",
"artist": "Artist_Name",
"title": "Title",
},
),
(
"6 Title by Artist",
{"disc": None, "track": "6", "artist": "Artist", "title": "Title"},
),
(
"Title",
{"disc": None, "track": None, "artist": None, "title": "Title"},
),
],
)
def test_parse_track_info(text, matchgroup):
f = FromFilenamePlugin()
m = f._parse_track_info(text)
assert matchgroup == m
@pytest.mark.parametrize(
"text,matchgroup",
[
(
# highly unlikely
"",
{
"albumartist": None,
"album": None,
"year": None,
"catalognum": None,
"media": None,
},
),
(
"1970",
{
"albumartist": None,
"album": None,
"year": "1970",
"catalognum": None,
"media": None,
},
),
(
"Album Title",
{
"albumartist": None,
"album": "Album Title",
"year": None,
"catalognum": None,
"media": None,
},
),
(
"Artist - Album Title",
{
"albumartist": "Artist",
"album": "Album Title",
"year": None,
"catalognum": None,
"media": None,
},
),
(
"Artist - Album Title (2024)",
{
"albumartist": "Artist",
"album": "Album Title",
"year": "2024",
"catalognum": None,
"media": None,
},
),
(
"Artist - 2024 - Album Title [flac]",
{
"albumartist": "Artist",
"album": "Album Title",
"year": "2024",
"catalognum": None,
"media": None,
},
),
(
"(2024) Album Title [CATALOGNUM] WEB",
# sometimes things are just going to be unparsable
{
"albumartist": "Album Title",
"album": "WEB",
"year": "2024",
"catalognum": "CATALOGNUM",
"media": None,
},
),
(
"{2024} Album Artist - Album Title [INFO-WAV]",
{
"albumartist": "Album Artist",
"album": "Album Title",
"year": "2024",
"catalognum": None,
"media": None,
},
),
(
"VA - Album Title [2025] [CD-FLAC]",
{
"albumartist": "Various Artists",
"album": "Album Title",
"year": "2025",
"catalognum": None,
"media": "CD",
},
),
(
"Artist - Album Title 3000 (1998) [FLAC] {CATALOGNUM}",
{
"albumartist": "Artist",
"album": "Album Title 3000",
"year": "1998",
"catalognum": "CATALOGNUM",
"media": None,
},
),
(
"various - cd album (2023) [catalognum 123] {vinyl mp3}",
{
"albumartist": "Various Artists",
"album": "cd album",
"year": "2023",
"catalognum": "catalognum 123",
"media": "Vinyl",
},
),
(
"[CATALOG567] Album - Various (2020) [WEB-FLAC]",
{
"albumartist": "Various Artists",
"album": "Album",
"year": "2020",
"catalognum": "CATALOG567",
"media": "Digital Media",
},
),
(
"Album 3000 {web}",
{
"albumartist": None,
"album": "Album 3000",
"year": None,
"catalognum": None,
"media": "Digital Media",
},
),
],
)
def test_parse_album_info(text, matchgroup):
f = FromFilenamePlugin()
m = f._parse_album_info(text)
assert matchgroup == m
@pytest.mark.parametrize(
"patterns,expected",
[
(
[
r"""
(?P<disc>\d+(?=[\.\-_]\d))?
# a disc must be followed by punctuation and a digit
[\.\-]{,1}
# disc punctuation
(?P<track>\d+)?
# match the track number
[\.\-_\s]*
# artist separators
(?P<artist>.+?(?=[\s*_]?[\.\-by].+))?
# artist match depends on title existing
[\.\-_\s]*
(?P<by>by)?
# if 'by' is found, artist and title will need to be swapped
[\.\-_\s]*
# title separators
(?P<title>.+)?
# match the track title
""",
r"",
r"(?:<invalid)",
r"(.*)",
r"(?P<disc>asda}]",
],
1,
)
],
)
def test_to_regex(patterns, expected):
f = FromFilenamePlugin()
p = f._to_regex(patterns)
assert len(p) == expected
class TestFromFilename(PluginMixin):
plugin = "fromfilename"
@pytest.mark.parametrize(
"expected_item",
[
mock_item(
path="/tmp/01 - The Artist - Song One.m4a",
artist="The Artist",
track=1,
title="Song One",
),
mock_item(
path="/tmp/01 The Artist - Song One.m4a",
artist="The Artist",
track=1,
title="Song One",
),
mock_item(
path="/tmp/02 The Artist - Song Two.m4a",
artist="The Artist",
track=2,
title="Song Two",
),
mock_item(
path="/tmp/01-The_Artist-Song_One.m4a",
artist="The_Artist",
track=1,
title="Song_One",
),
mock_item(
path="/tmp/02.-The_Artist-Song_Two.m4a",
artist="The_Artist",
track=2,
title="Song_Two",
),
mock_item(
path="/tmp/01 - Song_One.m4a",
track=1,
title="Song_One",
),
mock_item(
path="/tmp/02. - Song_Two.m4a",
track=2,
title="Song_Two",
),
mock_item(
path="/tmp/Song One by The Artist.m4a",
artist="The Artist",
title="Song One",
),
mock_item(
path="/tmp/Song Two by The Artist.m4a",
artist="The Artist",
title="Song Two",
),
mock_item(
path="/tmp/01.m4a",
track=1,
title="01",
),
mock_item(
path="/tmp/02.m4a",
track=2,
title="02",
),
mock_item(
path="/tmp/Song One.m4a",
title="Song One",
),
mock_item(
path="/tmp/Song Two.m4a",
title="Song Two",
),
mock_item(
path=(
"/tmp/"
"[CATALOG567] Album - Various - [WEB-FLAC]"
"/2-10 - Artist - Song One.m4a"
),
album="Album",
artist="Artist",
track=10,
disc=2,
albumartist="Various Artists",
catalognum="CATALOG567",
title="Song One",
media="Digital Media",
),
mock_item(
path=(
"/tmp/"
"[CATALOG567] Album - Various - [WEB-FLAC]"
"/03-04 - Other Artist - Song Two.m4a"
),
album="Album",
artist="Other Artist",
disc=3,
track=4,
albumartist="Various Artists",
catalognum="CATALOG567",
title="Song Two",
media="Digital Media",
),
],
)
def test_fromfilename(self, expected_item):
"""
Take expected items, create a task with just the paths.
After parsing, compare to the original with the expected attributes defined.
"""
task = Task([mock_item(path=expected_item.path)])
f = FromFilenamePlugin()
f.filename_task(task, Session())
res = task.items[0]
exp = expected_item
assert res.path == exp.path
assert res.artist == exp.artist
assert res.albumartist == exp.albumartist
assert res.disc == exp.disc
assert res.catalognum == exp.catalognum
assert res.year == exp.year
assert res.title == exp.title
@pytest.mark.parametrize(
"expected_items",
[
[
mock_item(
path="/Artist - Album/01 - Track1 - Performer.flac",
track=1,
title="Track1",
album="Album",
albumartist="Artist",
artist="Performer",
),
mock_item(
path="/Artist - Album/02 - Track2 - Artist.flac",
track=2,
title="Track2",
album="Album",
albumartist="Artist",
artist="Artist",
),
],
[
mock_item(
path=(
"/DiY - 8 Definitions of Bounce/"
"01 - Essa - Definition of Bounce.flac"
),
track=1,
title="Definition of Bounce",
albumartist="DiY",
album="8 Definitions of Bounce",
artist="Essa",
),
mock_item(
path=(
"/DiY - 8 Definitions of Bounce/"
"02 - Digs - Definition of Bounce.flac"
),
track=2,
title="Definition of Bounce",
album="8 Definitions of Bounce",
albumartist="DiY",
artist="Digs",
),
],
[
mock_item(
path=("/Essa - Magneto Essa/1 - Essa - Magneto Essa.flac"),
track=1,
title="Magneto Essa",
album="Magneto Essa",
albumartist="Essa",
artist="Essa",
),
mock_item(
path=("/Essa - Magneto Essa/2 - Essa - The Immortals.flac"),
track=2,
title="The Immortals",
album="Magneto Essa",
albumartist="Essa",
artist="Essa",
),
],
[
mock_item(
path=("/Magneto Essa/1 - Magneto Essa - Essa.flac"),
track=1,
title="Magneto Essa",
album="Magneto Essa",
artist="Essa",
),
mock_item(
path=("/Magneto Essa/2 - The Immortals - Essa.flac"),
track=2,
title="The Immortals",
album="Magneto Essa",
artist="Essa",
),
],
[
# Even though it might be clear to human eyes,
# we can't guess since the various flag is thrown
mock_item(
path=(
"/Various - 303 Alliance 012/"
"1 - The End of Satellite - Benji303.flac"
),
track=1,
title="Benji303",
album="303 Alliance 012",
artist="The End of Satellite",
albumartist="Various Artists",
),
mock_item(
path=(
"/Various - 303 Alliance 012/"
"2 - Ruff Beats - Benji303.flac"
),
track=2,
title="Benji303",
album="303 Alliance 012",
artist="Ruff Beats",
albumartist="Various Artists",
),
],
[
# Even though it might be clear to human eyes,
# we can't guess since the various flag is thrown
mock_item(
path=(
"/303 Alliance 012/"
"1 - The End of Satellite - Benji303.flac"
),
track=1,
title="Benji303",
album="303 Alliance 012",
artist="The End of Satellite",
),
mock_item(
path=(
"/303 Alliance 012/"
"2 - Ruff Beats - Benji303 & Sam J.flac"
),
track=2,
title="Benji303 & Sam J",
album="303 Alliance 012",
artist="Ruff Beats",
),
],
],
)
def test_sanity_check(self, expected_items):
"""
Take a list of expected items, create a task with just the paths.
Goal is to ensure that sanity check
correctly adjusts the parsed artists and albums
After parsing, compare to the expected items.
"""
task = Task([mock_item(path=item.path) for item in expected_items])
f = FromFilenamePlugin()
f.filename_task(task, Session())
res = task.items
exp = expected_items
assert res[0].path == exp[0].path
assert res[0].artist == exp[0].artist
assert res[0].albumartist == exp[0].albumartist
assert res[0].disc == exp[0].disc
assert res[0].catalognum == exp[0].catalognum
assert res[0].year == exp[0].year
assert res[0].title == exp[0].title
assert res[1].path == exp[1].path
assert res[1].artist == exp[1].artist
assert res[1].albumartist == exp[1].albumartist
assert res[1].disc == exp[1].disc
assert res[1].catalognum == exp[1].catalognum
assert res[1].year == exp[1].year
assert res[1].title == exp[1].title
# TODO: Test with singleton import tasks
# TODO: Test with items that already have data, or other types of bad data.
# TODO: Test with items that have perfectly fine data for the most part
@pytest.mark.parametrize(
"fields,expected",
[
(
[
"albumartist",
"album",
"year",
"media",
"catalognum",
"artist",
"track",
"disc",
"title",
],
mock_item(
albumartist="Album Artist",
album="Album",
year="2025",
media="CD",
catalognum="CATALOGNUM",
disc=1,
track=2,
artist="Artist",
title="Track",
),
),
(
["album", "year", "media", "track", "disc", "title"],
mock_item(
album="Album",
year="2025",
media="CD",
disc=1,
title="Track",
),
),
],
)
def test_fields(self, fields, expected):
"""
With a set item and changing list of fields
After parsing, compare to the original with the expected attributes defined.
"""
path = (
"/Album Artist - Album (2025) [FLAC CD] {CATALOGNUM}/"
"1-2 Artist - Track.wav"
)
task = Task([mock_item(path=path)])
expected.path = path
with self.configure_plugin({"fields": fields}):
f = FromFilenamePlugin()
f.config
f.filename_task(task, Session())
res = task.items[0]
assert res.path == expected.path
assert res.artist == expected.artist
assert res.albumartist == expected.albumartist
assert res.disc == expected.disc
assert res.catalognum == expected.catalognum
assert res.year == expected.year
assert res.title == expected.title
def test_user_regex(self):
return