Basic functionality complete

This commit is contained in:
will 2023-08-04 21:53:47 -06:00
parent 2419663b34
commit afc1e85d10
3 changed files with 138 additions and 5 deletions

View file

@ -1,17 +1,31 @@
# music-library-converter
A basic Python script that recursively scans a directory and replicates it in the target directory with all the audio files (read: anything ffmpeg can open) converted to the target format, preserving metadata, and optionally with other files also copied over.
A basic Python script that recursively scans a directory and replicates it in the target directory with all the audio files converted to the target format.
Untested outside of Linux. Your mileage may vary.
## Features
- Compatible with any input format supported by ffmpeg without needing it to be specified
- Replicates directory structure
- Can also copy other files
- Preserves metadata (not cover art yet. working on it) (or can strip it, if you want)
- Output bitrate can be controlled
- Ignores files that already exist in the target directory
## Motivation
I use Syncthing to copy my entire music library to my phone as an alternative to streaming services, and thousands of lossless FLACs take up more space on there than I'd like, so I wrote a Bash script to automatically convert them to MP3s. Being a Bash script written by me, it wasn't very good, so I decided to rewrite it in Python.
## TODO
- Preserve embedded cover art
- Don't ignore files when origin file is newer than target
- Alert user when source directory doesn't exist or target directory already has files
- Progress display
## Dependencies
- ffmpeg
- Python modules:
- pydub
- os
- shutil
- pydub
- mutagen

40
ansicodes.py Normal file
View file

@ -0,0 +1,40 @@
# I care too much about it being pretty. lol
class util:
clear = "\u001b[2J"
clearline = "\u001b[2K"
up = "\u001b[1A"
down = "\u001b[1B"
right = "\u001b[1C"
left = "\u001b[1D"
nextline = "\u001b[1E"
prevline = "\u001b[1F"
top = "\u001b[0;0H"
class style:
reset = "\u001b[0m"
bold = "\u001b[1m"
underline = "\u001b[4m"
reverse = "\u001b[7m"
class fg:
black = "\u001b[30m"
red = "\u001b[31m"
green = "\u001b[32m"
yellow = "\u001b[33m"
blue = "\u001b[34m"
magenta = "\u001b[35m"
cyan = "\u001b[36m"
white = "\u001b[37m"
class bg:
black = "\u001b[40m"
red = "\u001b[41m"
green = "\u001b[42m"
yellow = "\u001b[43m"
blue = "\u001b[44m"
magenta = "\u001b[45m"
cyan = "\u001b[46m"
white = "\u001b[47m"

79
main.py Normal file
View file

@ -0,0 +1,79 @@
from pydub import AudioSegment
from pydub.utils import mediainfo
from pydub.exceptions import CouldntDecodeError
import os
import shutil
import argparse
from ansicodes import style, fg, bg, util
try:
os.system("") # enables ansi escape characters in terminal
# Argument parser code go here
parser = argparse.ArgumentParser()
parser.add_argument("source", help="The directory containing the music library you want to convert")
parser.add_argument("target", help="The directory where the converted library should go. Will be created if it does not exist.")
parser.add_argument("-f", "--format", help="Target format. Defaults to mp3.")
parser.add_argument("-b", "--bitrate", help="Target bitrate. Not specified by default.")
parser.add_argument("-k", "--keepotherfiles", help="Also copy over files that aren't audio", action="store_true")
parser.add_argument("-s", "--stripmetadata", help="Strip metadata", action="store_true")
args = parser.parse_args()
targetFormat = "mp3"
if(args.format):
targetFormat = args.format
keepOtherFiles = args.keepotherfiles
preserveMetadata = not args.stripmetadata
startDir = args.source
outDir = args.target
filecount = 0
for path,dirs,files in os.walk(startDir):
for filename in files:
filecount += 1
print(style.bold+"Converting library of"+style.reset,filecount,style.bold+"files in"+style.reset,startDir,style.bold+"to"+style.reset,targetFormat,style.bold+"in"+style.reset,outDir)
print()
for path,dirs,files in os.walk(startDir):
for dirname in dirs:
outdirname = os.path.join(outDir,os.path.join(path,dirname)[len(startDir):])
if not os.path.exists(outdirname):
os.makedirs(outdirname)
for filename in files:
inFile = os.path.join(path,filename)[len(startDir):]
inPath = os.path.join(path,filename)
outPath = os.path.join(outDir,inFile)
convertedPath = outPath[:outPath.rfind(".")]+"."+targetFormat
print(style.bold+"Processing"+style.reset,inFile)
if(os.path.isfile(outPath) or os.path.isfile(convertedPath)):
print("\u001b[1F\u001b[K Exists",convertedPath[len(outDir):])
else:
try: # Here I check if a file is music or not by just attempting to open it as an audio file and handling the file like a non-audio file if it fails. The main benefit to this approach is I don't have to check against a hardcoded list of formats, which makes having at least bare minimum support for the maximum amount of formats possible much easier.
song = AudioSegment.from_file(inPath)
if(preserveMetadata): # Maybe you want to use this to strip the metadata from your audio. I don't know your life
if(args.bitrate):
song.export(convertedPath+".tmp", format=targetFormat, bitrate=args.bitrate, tags=mediainfo(inPath).get('TAG',None))
else:
song.export(convertedPath+".tmp", format=targetFormat, tags=mediainfo(inPath).get('TAG',None))
else:
if(args.bitrate):
song.export(convertedPath+".tmp", format=targetFormat, bitrate=args.bitrate)
else:
song.export(convertedPath+".tmp", format=targetFormat)
os.rename(convertedPath+".tmp", convertedPath)
print("\u001b[1F\u001b[K Converted",convertedPath[len(outDir):])
except (IndexError, CouldntDecodeError):
if(keepOtherFiles):
shutil.copy(inPath, outPath)
print("\u001b[1F\u001b[K Copied",inFile)
else:
print("\u001b[1F\u001b[K Ignored",inFile)
except KeyboardInterrupt:
print(util.clearline+fg.red+"KeyboardInterrupt"+style.reset)