Basic functionality complete
This commit is contained in:
parent
2419663b34
commit
afc1e85d10
3 changed files with 138 additions and 5 deletions
24
README.md
24
README.md
|
@ -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
40
ansicodes.py
Normal 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
79
main.py
Normal 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)
|
Loading…
Reference in a new issue