From afb599dcf439579d7175560b4151453b00a002cc Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Sun, 2 Apr 2023 18:55:45 -0400 Subject: [PATCH] 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 --- emmental/audio/tagger.py | 30 ++++++++++++++++--- emmental/db/tagger.py | 2 +- tests/audio/test_tagger.py | 60 +++++++++++++++++++++++++++++++++----- tests/db/test_tagger.py | 2 +- 4 files changed, 80 insertions(+), 14 deletions(-) diff --git a/emmental/audio/tagger.py b/emmental/audio/tagger.py index 6a81687..243bdf3 100644 --- a/emmental/audio/tagger.py +++ b/emmental/audio/tagger.py @@ -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) diff --git a/emmental/db/tagger.py b/emmental/db/tagger.py index 81212f5..418cc5a 100644 --- a/emmental/db/tagger.py +++ b/emmental/db/tagger.py @@ -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) diff --git a/tests/audio/test_tagger.py b/tests/audio/test_tagger.py index 532db06..de78b72 100644 --- a/tests/audio/test_tagger.py +++ b/tests/audio/test_tagger.py @@ -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) diff --git a/tests/db/test_tagger.py b/tests/db/test_tagger.py index dd5e68d..85c1c34 100644 --- a/tests/db/test_tagger.py +++ b/tests/db/test_tagger.py @@ -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"))