Compare commits

..

No commits in common. "83458f9f30aa9f390c440959d0908c19dd1c9ee9" and "0ff526b5ca4c8ea92a12b004be938b9e36c1bc4c" have entirely different histories.

3 changed files with 146 additions and 152 deletions

4
.gitignore vendored
View file

@ -1,4 +0,0 @@
!bcao.py
!requirements.txt
!.gitignore
*

290
bcao.py
View file

@ -1,167 +1,167 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# BCAO - BandCamp Automatic Organiser #BCAO - BandCamp Automatic Organiser
# copyright 2018-2019 @LynnearSoftware@fedi.lynnesbian.space #copyright 2018-2019 @LynnearSoftware@fedi.lynnesbian.space
# Licensed under the GPLv3: https://www.gnu.org/licenses/gpl-3.0.html#content #Licensed under the GPLv3: https://www.gnu.org/licenses/gpl-3.0.html#content
# input: a .zip from bandcamp #input: a .zip from bandcamp
# output: it organises it, adds cover art, puts it in the right place... #output: it organises it, adds cover art, puts it in the right place...
import argparse import subprocess, argparse, sys, os, re, base64
import base64
import os
import re
import subprocess
import tempfile
import shutil
from zipfile import ZipFile, BadZipFile
import mutagen try:
from mutagen.flac import Picture from mutagen.oggvorbis import OggVorbis
from mutagen import id3 from mutagen.mp3 import MP3
from mutagen.flac import FLAC
from mutagen.flac import Picture
from mutagen.aac import AAC
except ImportError:
print("Please install python3-mutagen (pip install mutagen)")
sys.exit(1)
from PIL import Image try:
subprocess.check_output(["unzip", "--help"])
except:
print("Please install unzip (apt install unzip)")
sys.exit(1)
def log(message: str, importance: int = 0): try:
if not args.quiet or importance > 0: subprocess.check_output(["which", "convert"])
print(message) subprocess.check_output(["which", "identify"])
except:
print("Please install imagemagick, and ensure convert and identify are in your $PATH (apt install imagemagick")
sys.exit(1)
def die(message: str, code: int = 1): parser = argparse.ArgumentParser(description = "BandCamp Automatic Organiser. Extracts the given zip file downloaded from Bandcamp and organises it.")
print(message)
exit(code)
def get_tag(m: mutagen.FileType, tag: str):
if tag == "title":
return sanitise(m['title'][0])
elif tag == "track":
return int(m['tracknumber'][0])
elif tag == "album":
return sanitise(m['album'][0])
else:
# may as well try
return sanitise(m[tag])
def sanitise(input: str):
if args.sanitise:
return re.sub(r"[?\\/:|*\"<>]", "_", input)
else:
return input
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')
parser.add_argument('-d', '--destination', dest='destination', default='/home/lynne/Music/Music/', #KEEP THESE IN ALPHABETICAL ORDER!
help="The directory to organise the music into. Default: /home/lynne/Music/Music/") parser.add_argument('-q','--quiet', dest='quiet', action='store_true', help='Disable non-error output')
parser.add_argument('-q', '--quiet', dest='quiet', action='store_true', parser.add_argument('-t','--threshold', dest='threshold', nargs=1, default=300, help="Maximum acceptable cover art file size in kilobytes. Default: 300")
help='Disable non-error output and assume default artist name.')
parser.add_argument('-u', '--unsanitised', dest='sanitise', action='store_false',
help="Don't replace NTFS-unsafe characters with underscores. Not recommended.")
parser.add_argument('-t', '--threshold', dest='threshold', nargs=1, default=300,
help="Maximum acceptable cover art file size in kilobytes. Default: 300")
args = parser.parse_args() args=parser.parse_args()
if not os.path.exists(args.zip): if not os.path.exists(args.zip):
die(f"Couldn't find {args.zip}.", 2) print("heh.... nice try kid... {0} aint a real file... i've been aroud the block a few times, ya know... you'll have to do a lot better than that to trick me.... kid...".format(args.zip))
sys.exit(2)
log("Extracting...") print("Extracting...")
tmp = tempfile.TemporaryDirectory() zipname = os.path.splitext(os.path.basename(args.zip))[0]
cover = None tmp = "/tmp/bcao/{0}".format(zipname)
song_names = [] subprocess.check_output(["rm", "-rf", tmp])
subprocess.check_output(['mkdir', '-p', tmp])
subprocess.check_output(["unzip", args.zip, "-d", tmp])
files = []
songs = []
with ZipFile(args.zip, 'r') as zip_file: for root, dirs, filez in os.walk(tmp):
for file in zip_file.namelist(): for file in filez: #for every file
if re.match(r"^(.+ - ){2}\d{2,} .+\.(ogg|flac|alac|aiff|wav|mp3|opus|m4a|aac|oga)$", file): files.append(root + os.sep + file)
# bandcamp zips contains songs with names formatted like "Album - Artist - 01 Song.mp3"
# for example, "King Crimson - In the Wake of Poseidon - 02 Pictures of a City.ogg"
# this regex should match only on those, and cut out (hopefully) all of the bonus material stuff, which shouldn't
# be added to the music folder (since i sync that to my phone, and "making of" videos are cool, but i don't
# have the space for it).
song_names.append(file)
zip_file.extract(file, tmp)
elif cover is None and re.match(r"cover\.(jpe?g|png)", file):
cover = file
zip_file.extract(file, tmp)
# save the format of the songs (ogg, mp3, etc)
# we'll need this to know what metadata format we should write
song_format = os.path.splitext(song_names[0])[1][1:]
log("Resizing album art to embed in songs...")
with Image.open(os.path.join(tmp, cover)) as image:
temp_cover = os.path.join(tmp, "cover-lq.jpg")
image.save(temp_cover, quality=85, optimize=True)
image_smol = image
# keep shrinking the image by 90% until it's less than {args.threshold} kilobytes
while os.path.getsize(temp_cover) / 1024 > args.threshold:
image_smol = image_smol.resize((round(image_smol.size[0] * 0.9), round(image_smol.size[1] * 0.9)))
image_smol.save(temp_cover, quality=85, optimize=True)
if image_smol.size[0] == 10:
die("Failed to resize image")
# read the image file to get the file's raw data
with open(temp_cover, 'r+b') as cover_file:
data = cover_file.read()
with Image.open(temp_cover) as image:
if song_format == "ogg":
# i hate this
cover = Picture()
cover.data = data
cover.type = mutagen.id3.PictureType.COVER_FRONT
cover.mime = "image/jpeg"
cover.width = image.size[0]
cover.height = image.size[1]
cover.depth = image.bits
cover = base64.b64encode(cover.write()).decode("ascii")
else:
log(f"Format {song_format} is not fully supported - cover images will not be modified", 1)
cover = ""
artists = [] artists = []
album = None album = ""
songs = {} fileExtensionRegex = re.compile(r"[^\.]+$")
zeroes = min(len(song_names), 2) probablyASongRegex = re.compile(r"^.+ - .+ - \d{2,} .+\.[^\.]+$") #matches "artist - album - 01 track.xyz" but not "some weird bonus thing.mp3"
for song in song_names: musicExts = ["ogg", "flac", "alac", "aiff", "wav", "mp3", "opus", "m4a", "aac", "oga"]
ext = os.path.splitext(song)[1:] bannedCharacters = ["?", "\\", "/", ":", "|", "*", "\"", "<", ">"] #characters that kill wangblows. all of these are fine on lincucks/crapOS except "/"
m = mutagen.File(os.path.join(tmp, song)) trackCount = 0
# add the song's artist to the list, if it hasn't been seen yet
[artists.append(sanitise(artist)) for artist in m['artist'] if artist not in artists]
songs[song] = f"{str(get_tag(m, 'track')).zfill(zeroes)} {get_tag(m, 'title')}.{song_format}"
album = get_tag(m, "album")
# embed cover art
if song_format == "ogg":
m["metadata_block_picture"] = [cover]
m.save()
if len(artists) > 1 and "Various Artists" not in artists: print("Processing... please wait.")
#use "01 Song.ogg" instead of "1 Song.ogg". Also works for albums with more than 99 tracks if that ever happens
trackNumberLength = len(str(len(files)))
if trackNumberLength < 2:
trackNumberLength = 2
for file in files:
if os.path.basename(file).lower() in ["cover.png", "cover.jpg", "cover.gif"]:
#we've found our cover art
cover = file
ext = re.search(fileExtensionRegex, file.lower()).group(0)
if ext in musicExts:
if re.search(probablyASongRegex, file) != None:
#this is definitely a song and probably not a bonus sound file or whatever
if ext == "ogg":
f = OggVorbis(file)
name = "{0} {1}.{2}".format(f["TRACKNUMBER"][0].zfill(trackNumberLength), f["TITLE"][0], ext)
for bc in bannedCharacters:
if bc in name:
name = name.replace(bc, "-") #replace banned characters with dashes
songs.append(name)
if album == "":
album = f["ALBUM"][0]
if f["ARTIST"][0] not in artists:
artists.append(f["ARTIST"][0])
trackCount = trackCount + 1
subprocess.check_output(["mv", file, os.path.dirname(file) + os.sep + name])
else:
print("UNSUPPORTED FORMAT BECAUSE LYNNE IS A LAZY MORON")
if len(artists) > 1:
artists.append("Various Artists") artists.append("Various Artists")
artist = None if cover == "":
while artist is None: #TODO: HANDLE THIS PROPERLY
log("Artist directory:") print("couldn't find the cover art :)))))")
sys.exit(1)
while os.path.getsize(cover) / 1024 > args.threshold:
if os.path.basename(cover) != "cover-lq.jpg":
nucover = os.path.dirname(cover) + os.sep + "cover-lq.jpg"
subprocess.check_output(["convert", cover, "-quality", "85", "-strip", nucover]) #convert the file to a jpeg
cover = nucover
else:
subprocess.check_output(["convert", cover, "-resize", "90%", cover]) #shrink it slightly
with open(cover, "rb") as cvr:
data = cvr.read()
imginfo = subprocess.check_output(["identify", "-ping", "-format", r"%w,%h,%m,%[bit-depth]", cover]).decode("utf-8").split(",")
picture = Picture()
picture.data = data
picture.type = 3
picture.mime = "image/{0}".format(imginfo[2].lower())
picture.width = int(imginfo[0])
picture.height = int(imginfo[1])
picture.depth = int(imginfo[3])
picture_data = picture.write()
encoded_data = base64.b64encode(picture_data)
vcomment_value = encoded_data.decode("ascii")
for song in songs:
f = OggVorbis(tmp + os.sep + song)
f["metadata_block_picture"] = [vcomment_value]
f.save()
artist = artists[0]
artists.append("Custom...")
# print("Please choose the artist name to use when creating the folder.")
choice = 0
while True:
print("Artist directory:")
for i in range(len(artists)): for i in range(len(artists)):
log(f"{i+1}) {artists[i]}") print("{0}) {1}".format(i + 1, artists[i]))
log(f"{len(artists) + 1}) Custom...") choice = input("> ")
choice = "1" if args.quiet else input("> ")
if choice.isdecimal():
if int(choice) == len(artists) + 1:
log("Enter the name to use.")
else:
try: try:
artist = artists[int(choice) - 1] choice = artists[int(choice) - 1]
except KeyError: except:
log(f"Please choose a number between 1 and {len(artists) + 1}.") print()
else: continue
log(f"Please choose a number between 1 and {len(artists) + 1}") break
if choice == "Custom...":
print("Enter the name to use.")
choice = input("> ")
# print("Setting artist to {0}".format(choice))
artist = choice
destination = os.path.join(args.destination, artist, album) mPath = "/home/lynne/Music/Music/{}/{}".format(artist, album) #todo: don't hardcode this
log(f"Moving files to {destination}...") subprocess.check_output(["mkdir", "-p", mPath])
os.makedirs(destination, exist_ok = True)
for source_name, dest_name in songs.items():
shutil.move(os.path.join(tmp, source_name), os.path.join(destination, dest_name))
shutil.move(os.path.join(tmp, "cover.jpg"), os.path.join(destination, "cover.jpg"))
tmp.cleanup() for root, dirs, filez in os.walk(tmp):
log("Done!") for file in filez: #for every file
subprocess.check_output(["mv", root + os.sep + file, mPath + os.sep + file])
print("Deleting {}...".format(tmp))
subprocess.check_output(["rm", "-rf", tmp])
print("Done!")

View file

@ -1,2 +0,0 @@
mutagen~=1.45
Pillow~=8.0