mirror of
https://github.com/beetbox/beets.git
synced 2026-02-17 04:43:40 +01:00
655 lines
20 KiB
Python
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
|