#!/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 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, 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, 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 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 # noqa SmallTag._file_extension_mapping[(".mp1", ".mp2", ".mp3")] = _ID3