158 lines
5.2 KiB
Python
158 lines
5.2 KiB
Python
from . import *
|
|
|
|
import re
|
|
from os import path
|
|
from typing import Union, List, Dict
|
|
from pathlib import Path
|
|
from base64 import b64encode
|
|
|
|
import mutagen
|
|
# noinspection PyProtectedMember
|
|
from mutagen.flac import Picture
|
|
from mutagen.mp4 import MP4Cover
|
|
# noinspection PyProtectedMember
|
|
from mutagen.id3 import APIC, PictureType, Frame, TRCK, TPE1, TIT2, TALB, TPE2
|
|
|
|
class FallbackError(Exception):
|
|
pass
|
|
|
|
class SongInfo:
|
|
tag_lookup: Dict[str, Dict[str, str]] = {
|
|
"track": {"id3": "TRCK", "m4a": "trkn", "vorbis": "tracknumber"},
|
|
"artist": {"id3": "TPE1", "m4a": "©ART", "vorbis": "artist"},
|
|
"title": {"id3": "TIT2", "m4a": "©nam", "vorbis": "title"},
|
|
"album": {"id3": "TALB", "m4a": "©alb", "vorbis": "album"},
|
|
"album_artist": {"id3": "TPE2", "m4a": "aART", "vorbis": "albumartist"}
|
|
}
|
|
|
|
def __init__(self, file_name: Path):
|
|
self.m_file: MutagenFile = mutagen.File(file_name)
|
|
self.m_tags: MutagenTags = self.m_file.tags
|
|
|
|
self.file_name = str(file_name.name)
|
|
self.format = path.splitext(file_name)[1][1:]
|
|
self.fallback = False
|
|
|
|
if self.format not in format_lookup:
|
|
raise ValueError(f"Unsupported file type: {self.format}")
|
|
|
|
fallbacks = re.match(
|
|
r"^(?P<artist>.+) - (?P<album>.+) - (?P<track>\d{2,}) (?P<title>.+)\.(?:ogg|flac|aiff|wav|mp3|m4a)$",
|
|
self.file_name
|
|
)
|
|
|
|
if fallbacks is None:
|
|
raise FallbackError("Couldn't determine fallback tags!")
|
|
|
|
# set default values for the tags, in case the file is missing any (or all!) of them
|
|
self.tags: Dict[str, str] = {
|
|
"track": str(int(fallbacks.group("track"))), # convert to int and str again to turn e.g. "01" into "1"
|
|
"artist": fallbacks.group("artist"),
|
|
"title": fallbacks.group("title"),
|
|
"album": fallbacks.group("album"),
|
|
"album_artist": fallbacks.group("artist")
|
|
}
|
|
# set list_tags to the default tags in list form
|
|
# i.e. for every tag, set list_tags[x] = [tags[x]]
|
|
self.list_tags: Dict[str, List[str]] = dict((x[0], [x[1]]) for x in self.tags.items())
|
|
|
|
if self.m_tags is None:
|
|
# file has no tags
|
|
# generate empty tags
|
|
self.m_file.add_tags()
|
|
self.m_tags = self.m_file.tags
|
|
self.fallback = True
|
|
|
|
# write fallback tags to file
|
|
for standard_name, tag_set in self.tag_lookup.items():
|
|
tag = tag_set[format_lookup[self.format]]
|
|
self.m_tags[tag] = self.new_id3_tag(standard_name, self.tags[standard_name])
|
|
|
|
self.m_file.save()
|
|
|
|
else:
|
|
for standard_name, tag_set in self.tag_lookup.items():
|
|
tag = tag_set[format_lookup[self.format]]
|
|
|
|
if tag not in self.m_tags:
|
|
print(f"{tag} not in self.m_tags")
|
|
self.fallback = True
|
|
continue
|
|
|
|
value_list = self.m_tags[tag]
|
|
if self.format == "m4a" and standard_name == "track":
|
|
# every tag in the MP4 file (from what i can tell) is a list
|
|
# this includes the track number tag, which is a tuple of ints in a list.
|
|
# because every other format is either a non-list, or a list of non-lists, we need to account for this case
|
|
# (a list of lists of non-lists) specially, by turning it into a list of non-lists.
|
|
value_list = value_list[0]
|
|
|
|
if not isinstance(value_list, (list, tuple)):
|
|
value_list = [value_list]
|
|
|
|
# convert the list of strings/ID3 frames/ints/whatevers to sanitised strings
|
|
value_list = [re.sub(sanitisation_regex, "_", str(val)) for val in value_list]
|
|
|
|
self.tags[standard_name] = value_list[0]
|
|
self.list_tags[standard_name] = value_list
|
|
|
|
@staticmethod
|
|
def new_id3_tag(tag: str, value: str) -> Frame:
|
|
if tag == "track":
|
|
return TRCK(encoding=3, text=value)
|
|
|
|
elif tag == "artist":
|
|
return TPE1(encoding=3, text=value)
|
|
|
|
elif tag == "title":
|
|
return TIT2(encoding=3, text=value)
|
|
|
|
elif tag == "album":
|
|
return TALB(encoding=3, text=value)
|
|
|
|
elif tag == "album_artist":
|
|
return TPE2(encoding=3, text=value)
|
|
|
|
else:
|
|
raise ValueError(f"Unknown tag type {tag}!")
|
|
|
|
def get_target_name(self, zeroes: int) -> str:
|
|
return f"{self.tags['track'].zfill(zeroes)} {self.tags['title']}.{self.format}"
|
|
|
|
def has_cover(self) -> bool:
|
|
if self.format == "flac":
|
|
# needs to be handled separately from ogg, as it doesn't use the vorbis tags for cover art for whatever reason
|
|
return len(self.m_file.pictures) != 0
|
|
|
|
if format_lookup[self.format] == "vorbis":
|
|
return "metadata_block_picture" in self.m_tags and len(self.m_tags["metadata_block_picture"]) != 0
|
|
|
|
if format_lookup[self.format] == "id3":
|
|
apics: List[APIC] = self.m_tags.getall("APIC")
|
|
for apic in apics:
|
|
if apic.type == PictureType.COVER_FRONT:
|
|
return True
|
|
return False
|
|
|
|
if format_lookup[self.format] == "m4a":
|
|
return 'covr' in self.m_tags and len(self.m_tags['covr']) != 0
|
|
|
|
raise NotImplementedError("Song format not yet implemented.")
|
|
|
|
def set_cover(self, to_embed: Union[Picture, APIC, MP4Cover]) -> None:
|
|
# embed cover art
|
|
if self.format == "flac":
|
|
self.m_file.clear_pictures()
|
|
self.m_file.add_picture(to_embed)
|
|
elif format_lookup[self.format] == "vorbis":
|
|
self.m_tags["metadata_block_picture"] = [b64encode(to_embed.write()).decode("ascii")]
|
|
elif format_lookup[self.format] == "id3":
|
|
self.m_tags.add(to_embed)
|
|
elif format_lookup[self.format] == "m4a":
|
|
self.m_tags['covr'] = [to_embed]
|
|
|
|
self.m_file.save()
|
|
|
|
def __getitem__(self, item: str) -> str:
|
|
return self.tags[item]
|