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:
parent
2c629c887c
commit
afb599dcf4
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"))
|
||||
|
|
Loading…
Reference in New Issue