Barely working. (Somehow TinyTag removes all "b"s at the beginning of the string tag.)

This commit is contained in:
The Wobbler 2025-03-14 17:30:04 +01:00
parent 94e350428b
commit 03ea689bff
6 changed files with 196 additions and 3 deletions

View file

@ -1,3 +1,10 @@
#!/usr/bin/python3
from .smalltag import SmallTag
if False:
from .smalltag import SmallTag # noqa
def __getattr__(name):
if name == "SmallTag":
from .smalltag import SmallTag
return SmallTag

View file

@ -0,0 +1,11 @@
#!/usr/bin/python3
if False:
from ._id3 import _ID3 # noqa
def __getattr__(name):
match name:
case "_ID3":
from ._id3 import _ID3
return _ID3

95
smalltag/_formats/_id3.py Normal file
View file

@ -0,0 +1,95 @@
#!/usr/bin/python3
from struct import pack
from tinytag import TinyTag
from .. import SmallTag
if True:
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):
@staticmethod
def _synchsafe(unsynchsafe_int: int) -> tuple[int, ...]:
ints = (
(unsynchsafe_int >> 21) & 0x7F,
(unsynchsafe_int >> 14) & 0x7F,
(unsynchsafe_int >> 7) & 0x7F,
unsynchsafe_int & 0x7F
)
return ints
def write(self, file_obj: BinaryIO=None):
should_close_file = self._set_filehandler_for_writing(file_obj)
# change keys to values in the mapping dict
self._ID3_WRITE_MAPPING = {} # noqa
for frame_id, name in self._ID3_MAPPING.items():
if len(frame_id) == 4:
self._ID3_WRITE_MAPPING[name] = frame_id
new_frames = self._compose_id3v2_frames()
size = len(new_frames)
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)
file_content = self._filehandler.read()
audio = file_content[size:]
self._filehandler.seek(0)
self._filehandler.write(new_tag + audio)
self._filehandler.truncate()
if should_close_file:
self._filehandler.close()
def _compose_id3v2_header(self, size: int):
header = b"ID3\x04\x00\x00"
synchsafe_size = self._synchsafe(size)
header += pack("4B", *synchsafe_size)
return header
def _compose_id3v2_frames(self):
tag_dict = self.as_dict()
frames = b""
for field_name, field_value in tag_dict.items():
if field_name not in self._ID3_WRITE_MAPPING:
continue
frames += self._compose_id3v2_frame(field_name, field_value)
return frames
def _compose_id3v2_frame(self, field_name, field_value):
frame_id = bytes(self._ID3_WRITE_MAPPING[field_name], "ISO-8859-1")
field_value = field_value[0]
if isinstance(field_value, str):
frame_value = b"\x00" + bytes(field_value, "ISO-8859-1")
else:
print(f"Writing of {field_name} is not implemented.")
return b""
frame_size = pack("4B", *self._synchsafe(len(frame_value)))
frame_data = frame_id + frame_size + b"\x00\x00" + frame_value
return frame_data

View file

@ -1,7 +1,70 @@
#!/usr/bin/python3
from tinytag import TinyTag
from __future__ import annotations
from tinytag import TinyTag, UnsupportedFormatError
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
class SmallTag(TinyTag):
pass
@classmethod
def _get_parser_class(
cls,
filename: str | None = None,
filehandle: BinaryIO | None = None
) -> type[SmallTag]:
if cls != SmallTag:
return cls
if filename:
parser_class = cls._get_parser_for_filename(filename)
if parser_class is not None:
return parser_class
# try determining the file type by magic byte header
if filehandle:
parser_class = cls._get_parser_for_file_handle(filehandle)
if parser_class is not None:
return parser_class
raise UnsupportedFormatError(
'No tag reader found to support file type')
def write(self, file_obj: BinaryIO = None):
raise NotImplementedError
def _set_filehandler_for_writing(self, file_obj: BinaryIO) -> bool:
should_close_file = file_obj is None and not self._filehandler.writable()
if self.filename is None and should_close_file:
raise ValueError(
"You must specify a new file object that is in write mode. "
"(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():
self._filehandler.close()
if file_obj is not None:
self._filehandler = file_obj
else:
self._filehandler = open(self.filename, "rb+")
return should_close_file
# import formats after SmallTag definition to avoid circular imports
# (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,
}

17
tests/id3.py Normal file
View file

@ -0,0 +1,17 @@
#!/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=" "))

BIN
tests/test.mp3 Normal file

Binary file not shown.