diff --git a/smalltag/_formats/_id3.py b/smalltag/_formats/_id3.py index e38767b..831bf44 100644 --- a/smalltag/_formats/_id3.py +++ b/smalltag/_formats/_id3.py @@ -1,19 +1,18 @@ #!/usr/bin/python3 +from __future__ import annotations from struct import pack -from tinytag import TinyTag +from io import BytesIO +from tinytag import TinyTag, tinytag from .. import SmallTag -if True: +if False: from collections.abc import Callable, Iterator # noqa from typing import Any, BinaryIO, Dict, List -_tinytag_id3 = TinyTag._get_parser_for_filename(".mp3") # noqa - - -class _ID3(SmallTag, _tinytag_id3): +class _ID3(SmallTag, tinytag._ID3): @staticmethod def _synchsafe(unsynchsafe_int: int) -> tuple[int, ...]: ints = ( @@ -41,7 +40,6 @@ class _ID3(SmallTag, _tinytag_id3): new_header = self._compose_id3v2_header(size) new_tag = new_header + new_frames - print(new_tag) size, extended, major = self._parse_id3v2_header(self._filehandler) diff --git a/smalltag/smalltag.py b/smalltag/smalltag.py index 7ff0c04..0d9f846 100644 --- a/smalltag/smalltag.py +++ b/smalltag/smalltag.py @@ -1,15 +1,29 @@ #!/usr/bin/python3 from __future__ import annotations -from tinytag import TinyTag, UnsupportedFormatError +from tinytag import tinytag if False: # just stole this lazy import type hinting trick from tinytag from collections.abc import Callable, Iterator # noqa from typing import Any, BinaryIO, Dict, List +TinyTag = tinytag.TinyTag +UnsupportedFormatError = tinytag.UnsupportedFormatError + class SmallTag(TinyTag): + _file_extension_mapping: dict[tuple[str, ...], type[SmallTag]]= { + (".mp1", ".mp2", ".mp3"): None, + ('.oga', '.ogg', '.opus', '.spx'): tinytag._Ogg, + ('.wav',): tinytag._Wave, + ('.flac',): tinytag._Flac, + ('.wma',): tinytag._Wma, + ('.m4b', '.m4a', '.m4r', '.m4v', '.mp4', + '.aax', '.aaxc'): tinytag._MP4, + ('.aiff', '.aifc', '.aif', '.afc'): tinytag._Aiff, + } + @classmethod def _get_parser_class( cls, @@ -30,6 +44,37 @@ class SmallTag(TinyTag): raise UnsupportedFormatError( 'No tag reader found to support file type') + @classmethod + def _get_parser_for_file_handle( + cls, + filehandle: BinaryIO + ) -> type[SmallTag] | None: + # https://en.wikipedia.org/wiki/List_of_file_signatures + header = filehandle.read(35) + filehandle.seek(0) + if header.startswith(b'ID3') or header.startswith(b'\xff\xfb'): + return _ID3 + + super()._get_parser_for_file_handle(filehandle) + # if header.startswith(b'fLaC'): + # return _Flac + # if ((header[4:8] == b'ftyp' + # and header[8:11] in {b'M4A', b'M4B', b'aax'}) + # or b'\xff\xf1' in header): + # return _MP4 + # if (header.startswith(b'OggS') + # and (header[29:33] == b'FLAC' or header[29:35] == b'vorbis' + # or header[28:32] == b'Opus' or header[29:34] == b'Speex')): + # return _Ogg + # if header.startswith(b'RIFF') and header[8:12] == b'WAVE': + # return _Wave + # if header.startswith(b'\x30\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00' + # b'\xAA\x00\x62\xCE\x6C'): + # return _Wma + # if header.startswith(b'FORM') and header[8:12] in {b'AIFF', b'AIFC'}: + # return _Aiff + # return None + def write(self, file_obj: BinaryIO = None): raise NotImplementedError @@ -42,14 +87,14 @@ class SmallTag(TinyTag): "(Has to be from the same file as specified while creating this TinyTag instance.)" ) - if file_obj is not None and not self._filehandler.closed(): + if file_obj is not None and not self._filehandler.closed: self._filehandler.close() - if file_obj is not None: - self._filehandler = file_obj + if file_obj is None: + self._filehandler = open(self.filename, "rb+") else: - self._filehandler = open(self.filename, "rb+") + self._filehandler = file_obj return should_close_file @@ -58,13 +103,5 @@ class SmallTag(TinyTag): # (this solution is stupid, but I don't know anything better.) from ._formats import _ID3 # noqa -SmallTag._file_extension_mapping = { - ('.mp1', '.mp2', '.mp3'): _ID3, - # ('.oga', '.ogg', '.opus', '.spx'): _Ogg, (Not yet implemented.) - # ('.wav',): _Wave, - # ('.flac',): _Flac, - # ('.wma',): _Wma, - # ('.m4b', '.m4a', '.m4r', '.m4v', '.mp4', - # '.aax', '.aaxc'): _MP4, - # ('.aiff', '.aifc', '.aif', '.afc'): _Aiff, -} +SmallTag._file_extension_mapping[(".mp1", ".mp2", ".mp3")] = _ID3 + diff --git a/tests/formats/test.mp3 b/tests/formats/test.mp3 new file mode 100644 index 0000000..41fb2a9 Binary files /dev/null and b/tests/formats/test.mp3 differ diff --git a/tests/formats/test_id3.py b/tests/formats/test_id3.py new file mode 100644 index 0000000..eca0e33 --- /dev/null +++ b/tests/formats/test_id3.py @@ -0,0 +1,30 @@ +#!/usr/bin/python3 + +from smalltag import SmallTag + + +# chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_ " + +# no "b" because there is a bug in TinyTag 2.1.0 that removes "b"s if they are at the beginning of a string +chars = "acdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789- _" + + +def test_writing_text_frames(): + for char1 in chars: + for char2 in chars: + field_content = f"{char1}{char2}Test{char2}{char1}" + + tag = SmallTag.get("formats/test.mp3") + + tag.title = field_content + tag.artist = field_content + tag.album = field_content + + tag.write() + + tag = SmallTag.get("formats/test.mp3") + + assert tag.title == field_content + assert tag.artist == field_content + assert tag.album == field_content + diff --git a/tests/id3.py b/tests/id3.py deleted file mode 100644 index f025c2f..0000000 --- a/tests/id3.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/python3 - -from json import dumps -from smalltag import SmallTag - - -test_tag = SmallTag.get("test.mp3") -print(test_tag) - -print(test_tag.title) -test_tag.title = "bTest" - -test_tag.write() - -test_tag = SmallTag.get("test.mp3") - -print(dumps(test_tag.as_dict(), indent=" ")) diff --git a/tests/test.mp3 b/tests/test.mp3 deleted file mode 100644 index e92a390..0000000 Binary files a/tests/test.mp3 and /dev/null differ