audio: Add Track support to the tagger

I extract the artist, length, mbid, mtime, tracknumber, and title from
the tags to use when constructing Tracks.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2023-04-02 18:55:45 -04:00
parent 2c629c887c
commit afb599dcf4
4 changed files with 80 additions and 14 deletions

View File

@ -39,10 +39,23 @@ class _Medium:
type: str
@dataclasses.dataclass
class _Track:
"""Class for holding Track-related tags."""
artist: str
length: int
mbid: int
mtime: float
number: int
title: str
class _Tags:
"""Extract tags found in the Mutagen tag dictionary."""
def __init__(self, file: pathlib.Path, tags: dict):
def __init__(self, file: pathlib.Path, tags: dict,
length: int = 0, mtime: float = 0.0):
"""Initialize the Tagger."""
self.file = file
self.tags = tags
@ -60,6 +73,13 @@ class _Tags:
tags.get("discsubtitle", [""])[0],
tags.get("media", [""])[0])
self.track = _Track(tags.get("artist", [""])[0],
length,
tags.get("musicbrainz_releasetrackid", [""])[0],
mtime,
int(tags.get("tracknumber", [0])[0]),
tags.get("title", [""])[0])
self.genres = sorted(self.list_genres())
self.year = self.get_year()
@ -139,8 +159,10 @@ class _Tags:
return int(re.match(r"\d+", self.album.release).group(0))
def tag_file(file: pathlib.Path) -> _Tags | None:
def tag_file(file: pathlib.Path, mtime: float | None) -> _Tags | None:
"""Tag the requested file."""
if file.is_file():
if (tags := mutagen.File(file)) is not None:
return _Tags(file, tags)
file_mtime = file.stat().st_mtime
if mtime is None or file_mtime > mtime:
if (tags := mutagen.File(file)) is not None:
return _Tags(file, tags, tags.info.length, file_mtime)

View File

@ -160,7 +160,7 @@ class Thread(threading.Thread):
if self._file is None:
break
if tags := emmental.audio.tagger.tag_file(self._file):
if tags := emmental.audio.tagger.tag_file(self._file, None):
for artist in tags.artists:
self.__check_artist(artist)

View File

@ -157,6 +157,28 @@ class TestAudioTagger(unittest.TestCase):
self.assertListEqual(tagger.genres, ["EP", "Genre 1", "Genre 2",
"Genre 3", "Genre 4", "Single"])
def test_track(self):
"""Test that tracks can be tagged corretly."""
tagger = _Tags(self.file, {"artist": ["Test Artist"],
"title": ["Test Title"],
"tracknumber": ["2"],
"musicbrainz_releasetrackid": ["ab-cd-ef"]},
12345, 678.90)
self.assertEqual(tagger.track.artist, "Test Artist")
self.assertEqual(tagger.track.length, 12345)
self.assertEqual(tagger.track.mbid, "ab-cd-ef")
self.assertEqual(tagger.track.mtime, 678.90)
self.assertEqual(tagger.track.number, 2)
self.assertEqual(tagger.track.title, "Test Title")
tagger = _Tags(self.file, {})
self.assertEqual(tagger.track.artist, "")
self.assertEqual(tagger.track.length, 0)
self.assertEqual(tagger.track.mbid, "")
self.assertEqual(tagger.track.mtime, 0.0)
self.assertEqual(tagger.track.number, 0)
self.assertEqual(tagger.track.title, "")
def test_year(self):
"""Test the year property."""
tagger = _Tags(self.file, {"date": ["1988-06-17"]})
@ -164,38 +186,60 @@ class TestAudioTagger(unittest.TestCase):
@unittest.mock.patch("pathlib.Path.is_file")
@unittest.mock.patch("pathlib.Path.stat")
class TestTagFile(unittest.TestCase):
"""Test case for the tag_file() function."""
def test_not_file(self, mock_is_file: unittest.mock.Mock):
def test_not_file(self, mock_stat: unittest.mock.Mock(),
mock_is_file: unittest.mock.Mock):
"""Test calling tag_file() on something other than a file."""
path = pathlib.Path("/a/b/c")
mock_is_file.return_value = False
self.assertIsNone(emmental.audio.tagger.tag_file(path))
self.assertIsNone(emmental.audio.tagger.tag_file(path, None))
mock_is_file.assert_called()
@unittest.mock.patch("mutagen.File")
def test_no_tags(self, mock_mutagen_file: unittest.mock.Mock,
mock_stat: unittest.mock.Mock(),
mock_is_file: unittest.mock.Mock):
"""Test calling tag_file() on a file that doesn't have tags."""
path = pathlib.Path("/a/b/c/notags.txt")
mock_is_file.return_value = True
mock_mutagen_file.return_value = None
self.assertIsNone(emmental.audio.tagger.tag_file(path))
self.assertIsNone(emmental.audio.tagger.tag_file(path, None))
mock_is_file.assert_called()
mock_mutagen_file.assert_called_with(path)
def test_not_updated(self, mock_stat: unittest.mock.Mock,
mock_is_file: unittest.mock.Mock):
"""Test calling tag_file() with an mtime <= mtime reported by stat."""
path = pathlib.Path("/a/b/c/track.ogg")
mock_is_file.return_value = True
mock_stat.return_value.st_mtime = 123.45
self.assertIsNone(emmental.audio.tagger.tag_file(path, 123.45))
self.assertIsNone(emmental.audio.tagger.tag_file(path, 246.8))
@unittest.mock.patch("mutagen.File")
def test_have_tags(self, mock_mutagen_file: unittest.mock.Mock,
mock_stat: unittest.mock.Mock,
mock_is_file: unittest.mock.Mock):
"""Test calling tag_file() successfully."""
path = pathlib.Path("/a/b/c/track.ogg")
mock_is_file.return_value = True
mock_mutagen_file.return_Value = dict()
mock_stat.return_value.st_mtime = 67.890
mock_mutagen_file.return_value = unittest.mock.MagicMock()
mock_mutagen_file.return_value.info.length = 12345
self.assertIsInstance(emmental.audio.tagger.tag_file(path),
emmental.audio.tagger._Tags)
mock_is_file.assert_called()
mock_mutagen_file.assert_called_with(path)
for mtime in [None, 70.123]:
with self.subTest(mtime=mtime):
tags = emmental.audio.tagger.tag_file(path, None)
self.assertIsInstance(tags, emmental.audio.tagger._Tags)
self.assertEqual(tags.track.length, 12345)
self.assertEqual(tags.track.mtime, 67.890)
mock_is_file.assert_called()
mock_stat.assert_called()
mock_mutagen_file.assert_called_with(path)

View File

@ -208,7 +208,7 @@ class TestTaggerThread(tests.util.TestCase):
self.tagger.ready.wait()
self.assertIsNone(self.tagger._tags)
mock_file.assert_called_with(pathlib.Path("/a/b/c.ogg"))
mock_file.assert_called_with(pathlib.Path("/a/b/c.ogg"), None)
mock_file.return_value = self.tags
self.tagger.tag_file(pathlib.Path("/a/b/c.ogg"))