cleaner code, type annotations, and it even runs (slightly) faster! =u=

This commit is contained in:
Lynne Megido 2020-10-17 16:23:55 +10:00
parent 2f62c53a9f
commit 801b7369c4
Signed by: lynnesbian
GPG key ID: F0A184B5213D9F90

116
bcao.py
View file

@ -9,10 +9,13 @@ import argparse
import base64 import base64
import os import os
import re import re
import subprocess import sys
import tempfile import tempfile
import shutil import shutil
from zipfile import ZipFile, BadZipFile from os import path
from zipfile import ZipFile
from pathlib import Path
from typing import Optional, List, Dict
import mutagen import mutagen
from mutagen.flac import Picture from mutagen.flac import Picture
@ -26,24 +29,18 @@ def log(message: str, importance: int = 0):
def die(message: str, code: int = 1): def die(message: str, code: int = 1):
print(message) print(message)
exit(code) sys.exit(code)
def get_tag(m: mutagen.FileType, tag: str): def get_tag(mut_song: mutagen.FileType, tag: str) -> str:
if tag == "title": if tag == "track":
return sanitise(m['title'][0]) tag = "tracknumber"
elif tag == "track": tag = tag.replace("_", "")
return int(m['tracknumber'][0]) return sanitise(mut_song[tag][0] if type(mut_song[tag]) is list else mut_song[tag])
elif tag == "album":
return sanitise(m['album'][0])
else:
# may as well try
return sanitise(m[tag])
def sanitise(input: str): def sanitise(in_str: str) -> str:
if args.sanitise: if args.sanitise:
return re.sub(r"[?\\/:|*\"<>]", "_", input) return re.sub(r"[?\\/:|*\"<>]", "_", in_str)
else: return in_str
return input
parser = argparse.ArgumentParser(description="Extracts the given zip file downloaded from Bandcamp and organises it.") parser = argparse.ArgumentParser(description="Extracts the given zip file downloaded from Bandcamp and organises it.")
parser.add_argument('zip', help='The zip file to use') parser.add_argument('zip', help='The zip file to use')
@ -57,15 +54,17 @@ parser.add_argument('-t', '--threshold', dest='threshold', nargs=1, default=300,
help="Maximum acceptable cover art file size in kilobytes. Default: 300") help="Maximum acceptable cover art file size in kilobytes. Default: 300")
args = parser.parse_args() args = parser.parse_args()
# convert args.treshold to bytes
args.threshold *= 1024
if not os.path.exists(args.zip): if not path.exists(args.zip):
die(f"Couldn't find {args.zip}.", 2) die(f"Couldn't find {args.zip}.", 2)
log("Extracting...") log("Extracting...")
tmpd = tempfile.TemporaryDirectory() tmp_dir: tempfile.TemporaryDirectory = tempfile.TemporaryDirectory()
tmp = tmpd.name tmp: str = tmp_dir.name
cover = None cover: Optional[str] = None
song_names = [] song_names: List[str] = []
with ZipFile(args.zip, 'r') as zip_file: with ZipFile(args.zip, 'r') as zip_file:
for file in zip_file.namelist(): for file in zip_file.namelist():
@ -83,12 +82,11 @@ with ZipFile(args.zip, 'r') as zip_file:
# save the format of the songs (ogg, mp3, etc) # save the format of the songs (ogg, mp3, etc)
# we'll need this to know what metadata format we should write # we'll need this to know what metadata format we should write
song_format = os.path.splitext(song_names[0])[1][1:] song_format: str = path.splitext(song_names[0])[1][1:]
log("Resizing album art to embed in songs...") log("Resizing album art to embed in songs...")
with Image.open(str(Path(tmp, cover))) as image:
with Image.open(os.path.join(tmp, cover)) as image: temp_cover: Path = Path(tmp, "cover-lq.jpg")
temp_cover = os.path.join(tmp, "cover-lq.jpg")
if image.mode in ["RGBA", "P"]: if image.mode in ["RGBA", "P"]:
# remove alpha channel # remove alpha channel
@ -97,11 +95,18 @@ with Image.open(os.path.join(tmp, cover)) as image:
image.save(temp_cover, quality=85, optimize=True) image.save(temp_cover, quality=85, optimize=True)
image_smol = image image_smol = image
# keep shrinking the image by 90% until it's less than {args.threshold} kilobytes while path.getsize(temp_cover) > args.threshold:
while os.path.getsize(temp_cover) / 1024 > args.threshold: # keep shrinking the image by 90% until it's less than {args.threshold} kilobytes
image_smol = image_smol.resize((round(image_smol.size[0] * 0.9), round(image_smol.size[1] * 0.9))) ratio = 0.9
if path.getsize(temp_cover) > args.threshold * 2:
# if the file size of the cover is more than double the threshold, resize the cover image size by 80% instead
ratio = 0.8
image_smol = image_smol.resize([round(n * ratio) for n in image_smol.size])
image_smol.save(temp_cover, quality=85, optimize=True) image_smol.save(temp_cover, quality=85, optimize=True)
if image_smol.size[0] == 10: if image_smol.size[0] == 10:
# something very bad has happened here
die("Failed to resize image") die("Failed to resize image")
@ -123,50 +128,63 @@ with Image.open(temp_cover) as image:
else: else:
log(f"Format {song_format} is not fully supported - cover images will not be modified", 1) log(f"Format {song_format} is not fully supported - cover images will not be modified", 1)
artists = [] artists: List[str] = []
album = None album: Optional[str] = None
songs = {} songs: Dict[str, str] = {}
zeroes = min(len(song_names), 2) zeroes = min(len(song_names), 2)
first_loop: bool = True
for song in song_names: for song in song_names:
ext = os.path.splitext(song)[1:] m = mutagen.File(Path(tmp, song))
m = mutagen.File(os.path.join(tmp, song)) if first_loop:
# add the song's artist to the list, if it hasn't been seen yet # the first item in the artists list should be the album artist
[artists.append(sanitise(artist)) for artist in m['artist'] if artist not in artists] artists.append(get_tag(m, "album_artist"))
album = get_tag(m, "album")
first_loop = False
# add the song's artist(s) to the list
map(lambda x: artists.append(sanitise(x)), m['artist'])
songs[song] = f"{str(get_tag(m, 'track')).zfill(zeroes)} {get_tag(m, 'title')}.{song_format}" songs[song] = f"{str(get_tag(m, 'track')).zfill(zeroes)} {get_tag(m, 'title')}.{song_format}"
album = get_tag(m, "album")
# embed cover art # embed cover art
if song_format == "ogg": if song_format == "ogg":
m["metadata_block_picture"] = [embed_cover] m["metadata_block_picture"] = [embed_cover]
m.save() m.save()
# remove duplicate artists
artists = list(dict.fromkeys(artists))
if len(artists) > 1 and "Various Artists" not in artists: if len(artists) > 1 and "Various Artists" not in artists:
artists.append("Various Artists") artists.append("Various Artists")
artist = None artist: Optional[str] = None
while artist is None: while artist is None:
log("Artist directory:") log("Artist directory:")
for i in range(len(artists)): for i, artist_name in enumerate(artists):
log(f"{i+1}) {artists[i]}") log(f"{i+1}) {artist_name}")
log(f"{len(artists) + 1}) Custom...") log(f"{len(artists) + 1}) Custom...")
choice = "1" if args.quiet else input("> ") choice = "1" if args.quiet else input("> ")
if choice.isdecimal(): if choice.isdecimal():
if int(choice) == len(artists) + 1: choice = int(choice)
log("Enter the name to use.") if choice == len(artists) + 1:
log("Enter the name to use:")
artist = input("> ")
else: else:
try: try:
artist = artists[int(choice) - 1] artist = artists[choice - 1]
except KeyError: except KeyError:
log(f"Please choose a number between 1 and {len(artists) + 1}.") log(f"Please choose a number between 1 and {len(artists) + 1}.")
else: else:
log(f"Please choose a number between 1 and {len(artists) + 1}") log(f"Please choose a number between 1 and {len(artists) + 1}")
destination = os.path.join(args.destination, artist, album) destination: Path = Path(args.destination, artist, album)
log(f"Moving files to {destination}...") log(f"Moving files to \"{destination}\"...")
os.makedirs(destination, exist_ok=True) os.makedirs(destination, exist_ok=True)
for source_name, dest_name in songs.items(): for source_name, dest_name in songs.items():
shutil.move(os.path.join(tmp, source_name), os.path.join(destination, dest_name)) shutil.move(Path(tmp, source_name), Path(destination, dest_name))
shutil.move(os.path.join(tmp, cover), os.path.join(destination, cover)) shutil.move(Path(tmp, cover), Path(destination, cover))
tmpd.cleanup() tmp_dir.cleanup()
log("Done!") log("Done!")