m4a support

This commit is contained in:
Lynne Megido 2020-10-17 21:02:52 +10:00
parent 7cb6545096
commit f78c8d7c78
Signed by: lynnesbian
GPG Key ID: F0A184B5213D9F90

64
bcao.py
View File

@ -17,10 +17,15 @@ from zipfile import ZipFile
from pathlib import Path from pathlib import Path
from typing import Optional, Union, List, Dict from typing import Optional, Union, List, Dict
# pycharm tells me some of these classes shouldn't be imported because they're not declared in __all__.
# however, the mutagen docs show example code where someone creates a mutagen.flac.Picture by referring to it as
# Picture(), implying that they had imported mutagen.flac.Picture, and therefore i'm right and the computer is WRONG
# https://mutagen.readthedocs.io/en/latest/api/flac.html#mutagen.Picture.data
import mutagen import mutagen
from mutagen.flac import Picture, FLAC from mutagen.flac import Picture, FLAC
from mutagen.oggvorbis import OggVorbis from mutagen.oggvorbis import OggVorbis
from mutagen.mp3 import MP3 from mutagen.mp3 import MP3
from mutagen.mp4 import MP4, MP4Cover
from mutagen.id3 import APIC, PictureType from mutagen.id3 import APIC, PictureType
from PIL import Image from PIL import Image
@ -32,7 +37,14 @@ vorbis_to_id3: Dict[str, str] = {
"album": "TALB", "album": "TALB",
"album_artist": "TPE2" "album_artist": "TPE2"
} }
fully_supported: List[str] = ["ogg", "flac", "mp3"] vorbis_to_itunes: Dict[str, str] = {
"track": 'trkn',
"artist": '\xa9ART',
"title": '\xa9nam',
"album": '\xa9alb',
"album_artist": 'aART'
}
fully_supported: List[str] = ["ogg", "flac", "mp3", "m4a"]
MutagenFile = Union[MP3, FLAC, OggVorbis, mutagen.FileType] MutagenFile = Union[MP3, FLAC, OggVorbis, mutagen.FileType]
def log(message: str, importance: int = 0): def log(message: str, importance: int = 0):
@ -43,16 +55,34 @@ def die(message: str, code: int = 1):
print(message) print(message)
sys.exit(code) sys.exit(code)
def get_tag(mut_song: MutagenFile, tag: str, allow_list: bool = False, allow_sanitising: bool = True) -> str: def get_tag(mut_song: MutagenFile, tag: str, allow_list: bool = False, allow_sanitising: bool = True)\
-> Union[str, List[str]]:
if isinstance(mut_song, MP3): if isinstance(mut_song, MP3):
tag = vorbis_to_id3[tag] tag = vorbis_to_id3[tag]
tag_list = [str(x) for x in mut_song.tags.getall(tag)] tag_list = mut_song.tags.getall(tag)
elif isinstance(mut_song, MP4):
# every tag in the MP4 file (from what i can tell) is a list
# this includes the track number tag, which is a list, containing a single tuple, containing two ints (track, total)
# unless we account for this, tag_list will be set to [(1, 5)], and then converted to a string, resulting in
# ['(1, 5)'], which (if not allow_list) will be returned as '(1, 5)', which is not exactly helpful.
tag = vorbis_to_itunes[tag]
if tag == 'trkn':
# mut_song[tag] == [(1, 5)]
# mut_song[tag][0] == (1, 5)
tag_list = mut_song[tag][0]
else:
tag_list = mut_song[tag]
else: else:
if tag == "track": if tag == "track":
tag = "tracknumber" tag = "tracknumber"
tag = tag.replace("_", "") tag = tag.replace("_", "")
tag_list = mut_song[tag] if isinstance(mut_song[tag], list) else [mut_song[tag]] tag_list = mut_song[tag] if isinstance(mut_song[tag], list) else [mut_song[tag]]
# convert the list of strings/ID3 frames/ints/whatevers to strings
tag_list = list(map(str, tag_list))
# sanitise everything # sanitise everything
if allow_sanitising: if allow_sanitising:
tag_list = [sanitise(tag) for tag in tag_list] tag_list = [sanitise(tag) for tag in tag_list]
@ -76,7 +106,10 @@ def has_cover(mut_song: MutagenFile):
return True return True
return False return False
return False if isinstance(mut_song, MP4):
return 'covr' in mut_song and len(mut_song['covr']) != 0
raise NotImplementedError("Song format not yet implemented.")
def sanitise(in_str: str) -> str: def sanitise(in_str: str) -> str:
if args.sanitise: if args.sanitise:
@ -88,7 +121,7 @@ parser = argparse.ArgumentParser(usage='%(prog)s zip [options]',
formatter_class=argparse.RawTextHelpFormatter, formatter_class=argparse.RawTextHelpFormatter,
description="Extracts the given zip file downloaded from Bandcamp and organises it.", description="Extracts the given zip file downloaded from Bandcamp and organises it.",
epilog=f"Cover art can only be embedded in files of the following types: {', '.join(fully_supported).upper()}.\nIf " epilog=f"Cover art can only be embedded in files of the following types: {', '.join(fully_supported).upper()}.\nIf "
"the music is in any other format, %(prog)s will behave as though you passed the flag '-c n'.") "the song is in any other format, %(prog)s will behave as though you passed '-c n', but will otherwise work normally.")
parser.add_argument('zip', help='The zip file to use.') parser.add_argument('zip', help='The zip file to use.')
parser.add_argument('-c', '--add-cover-images', dest='process_cover', default='w', choices=['n', 'a', 'w'], parser.add_argument('-c', '--add-cover-images', dest='process_cover', default='w', choices=['n', 'a', 'w'],
help="When to embed cover art into songs.\nOptions: [n]ever, [a]lways, [w]hen necessary.\nDefault: %(default)s") help="When to embed cover art into songs.\nOptions: [n]ever, [a]lways, [w]hen necessary.\nDefault: %(default)s")
@ -166,6 +199,18 @@ if args.process_cover != 'n':
data = cover_file.read() data = cover_file.read()
with Image.open(temp_cover) as image: with Image.open(temp_cover) as image:
# it's really strange that the more annoying the file's metadata is, the *less* annoying it is to create cover art
# for it in mutagen.
# vorbis: open standard, so easy to use that mutagen supplies a bunch of "easy" wrappers around other formats to
# make them work more like mutagen.
# cover-annoy-o-meter: high. mutagen requires you to specify the width, height, colour depth, etc etc
# id3: well documented, but rather cryptic (which is more understandable, "album_artist" or "TPE2").
# cover-annoy-o-meter: not bad at all - at least you get a constructor this time - although it is kinda annoying
# that you have to specify the file encoding, and how you need both a type and a desc.
# m4a: scarce documentation, closed format, half reverse engineered from whatever itunes is doing, exists pretty
# much exclusively in the realm of apple stuff.
# cover-annoy-o-meter: all you need is the file data and the format type.
if song_format in ["ogg", "flac"]: if song_format in ["ogg", "flac"]:
# i hate this # i hate this
embed_cover = Picture() embed_cover = Picture()
@ -179,7 +224,7 @@ if args.process_cover != 'n':
# apparently APIC files get compressed on save if they are "large": # apparently APIC files get compressed on save if they are "large":
# https://mutagen.readthedocs.io/en/latest/api/id3_frames.html#mutagen.id3.APIC # https://mutagen.readthedocs.io/en/latest/api/id3_frames.html#mutagen.id3.APIC
# i don't know what that means (lossless text compression? automatic JPEG conversion?) and i don't know if or how # i don't know what that means (lossless text compression? automatic JPEG conversion?) and i don't know if or how
# i can disable it, which kinda sucks... # i can disable it, which kinda sucks...
# if, for example, mutagen's threshold for "large" is 200KiB, then any file over that size would be reduced to # if, for example, mutagen's threshold for "large" is 200KiB, then any file over that size would be reduced to
# below it, either by resizing or JPEG quality reduction or whatever, making the -t flag useless for values above # below it, either by resizing or JPEG quality reduction or whatever, making the -t flag useless for values above
# 200 when saving MP3 files. # 200 when saving MP3 files.
@ -199,6 +244,11 @@ if args.process_cover != 'n':
desc='cover', desc='cover',
data=data data=data
) )
elif song_format == "m4a":
embed_cover = MP4Cover(
data=data,
imageformat=MP4Cover.FORMAT_JPEG
)
artists: List[str] = [] artists: List[str] = []
album: Optional[str] = None album: Optional[str] = None
@ -228,6 +278,8 @@ for song in song_names:
m.add_picture(embed_cover) m.add_picture(embed_cover)
elif song_format == "mp3": elif song_format == "mp3":
m.tags.add(embed_cover) m.tags.add(embed_cover)
elif song_format == "m4a":
m['covr'] = [embed_cover]
m.save() m.save()