SmallTag/smalltag/smalltag.py

123 lines
4.4 KiB
Python

#!/usr/bin/python3
from __future__ import annotations
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 BinaryIO
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"): None,
(".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,
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')
@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, advanced: dict[str, str | float | list[str]] | None=None, file_obj: BinaryIO = None):
"""
Write to the file given at creation of the tag or to the file object given to this function.
:param advanced: Dict containing fields to write.
:param file_obj: File object to write to. (Has to be in "rb+" mode.)
"""
raise NotImplementedError
def compose_tag(self, advanced: dict[str, str | float | list[str]] | None=None) -> bytes:
"""
Only compose a tag without writing.
:param advanced: Dict containing fields to write.
:return: Composed tag as bytes.
"""
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 None:
self._filehandler = open(self.filename, "rb+")
else:
self._filehandler = file_obj
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, _Ogg # noqa
SmallTag._file_extension_mapping[(".mp1", ".mp2", ".mp3")] = _ID3
SmallTag._file_extension_mapping[('.oga', '.ogg', '.opus', '.spx')] = _Ogg