Barely working. (Somehow TinyTag removes all "b"s at the beginning of the string tag.)
This commit is contained in:
parent
94e350428b
commit
03ea689bff
6 changed files with 196 additions and 3 deletions
|
@ -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
|
||||
|
|
11
smalltag/_formats/__init__.py
Normal file
11
smalltag/_formats/__init__.py
Normal 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
95
smalltag/_formats/_id3.py
Normal 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
|
|
@ -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
17
tests/id3.py
Normal 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
BIN
tests/test.mp3
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue