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
|
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:
|
class _Tags:
|
||||||
"""Extract tags found in the Mutagen tag dictionary."""
|
"""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."""
|
"""Initialize the Tagger."""
|
||||||
self.file = file
|
self.file = file
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
|
@ -60,6 +73,13 @@ class _Tags:
|
||||||
tags.get("discsubtitle", [""])[0],
|
tags.get("discsubtitle", [""])[0],
|
||||||
tags.get("media", [""])[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.genres = sorted(self.list_genres())
|
||||||
self.year = self.get_year()
|
self.year = self.get_year()
|
||||||
|
|
||||||
|
@ -139,8 +159,10 @@ class _Tags:
|
||||||
return int(re.match(r"\d+", self.album.release).group(0))
|
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."""
|
"""Tag the requested file."""
|
||||||
if file.is_file():
|
if file.is_file():
|
||||||
if (tags := mutagen.File(file)) is not None:
|
file_mtime = file.stat().st_mtime
|
||||||
return _Tags(file, tags)
|
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:
|
if self._file is None:
|
||||||
break
|
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:
|
for artist in tags.artists:
|
||||||
self.__check_artist(artist)
|
self.__check_artist(artist)
|
||||||
|
|
||||||
|
|
|
@ -157,6 +157,28 @@ class TestAudioTagger(unittest.TestCase):
|
||||||
self.assertListEqual(tagger.genres, ["EP", "Genre 1", "Genre 2",
|
self.assertListEqual(tagger.genres, ["EP", "Genre 1", "Genre 2",
|
||||||
"Genre 3", "Genre 4", "Single"])
|
"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):
|
def test_year(self):
|
||||||
"""Test the year property."""
|
"""Test the year property."""
|
||||||
tagger = _Tags(self.file, {"date": ["1988-06-17"]})
|
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.is_file")
|
||||||
|
@unittest.mock.patch("pathlib.Path.stat")
|
||||||
class TestTagFile(unittest.TestCase):
|
class TestTagFile(unittest.TestCase):
|
||||||
"""Test case for the tag_file() function."""
|
"""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."""
|
"""Test calling tag_file() on something other than a file."""
|
||||||
path = pathlib.Path("/a/b/c")
|
path = pathlib.Path("/a/b/c")
|
||||||
mock_is_file.return_value = False
|
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()
|
mock_is_file.assert_called()
|
||||||
|
|
||||||
@unittest.mock.patch("mutagen.File")
|
@unittest.mock.patch("mutagen.File")
|
||||||
def test_no_tags(self, mock_mutagen_file: unittest.mock.Mock,
|
def test_no_tags(self, mock_mutagen_file: unittest.mock.Mock,
|
||||||
|
mock_stat: unittest.mock.Mock(),
|
||||||
mock_is_file: unittest.mock.Mock):
|
mock_is_file: unittest.mock.Mock):
|
||||||
"""Test calling tag_file() on a file that doesn't have tags."""
|
"""Test calling tag_file() on a file that doesn't have tags."""
|
||||||
path = pathlib.Path("/a/b/c/notags.txt")
|
path = pathlib.Path("/a/b/c/notags.txt")
|
||||||
mock_is_file.return_value = True
|
mock_is_file.return_value = True
|
||||||
mock_mutagen_file.return_value = None
|
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_is_file.assert_called()
|
||||||
mock_mutagen_file.assert_called_with(path)
|
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")
|
@unittest.mock.patch("mutagen.File")
|
||||||
def test_have_tags(self, mock_mutagen_file: unittest.mock.Mock,
|
def test_have_tags(self, mock_mutagen_file: unittest.mock.Mock,
|
||||||
|
mock_stat: unittest.mock.Mock,
|
||||||
mock_is_file: unittest.mock.Mock):
|
mock_is_file: unittest.mock.Mock):
|
||||||
"""Test calling tag_file() successfully."""
|
"""Test calling tag_file() successfully."""
|
||||||
path = pathlib.Path("/a/b/c/track.ogg")
|
path = pathlib.Path("/a/b/c/track.ogg")
|
||||||
mock_is_file.return_value = True
|
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),
|
for mtime in [None, 70.123]:
|
||||||
emmental.audio.tagger._Tags)
|
with self.subTest(mtime=mtime):
|
||||||
mock_is_file.assert_called()
|
tags = emmental.audio.tagger.tag_file(path, None)
|
||||||
mock_mutagen_file.assert_called_with(path)
|
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.tagger.ready.wait()
|
||||||
self.assertIsNone(self.tagger._tags)
|
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
|
mock_file.return_value = self.tags
|
||||||
self.tagger.tag_file(pathlib.Path("/a/b/c.ogg"))
|
self.tagger.tag_file(pathlib.Path("/a/b/c.ogg"))
|
||||||
|
|
Loading…
Reference in New Issue