123 lines
4.4 KiB
Python
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
|
|
|