install/.bin/renameMP3.py

363 lines
11 KiB
Python
Executable File

#!/usr/bin/python
#
# Desmond Cox
# April 10, 2008
"""Project Music
Renames audio files based on metadata
Usage: renameMP3.py [options]
Options:
-d ..., --directory=... Specify which directory to work in
(default is the current directory)
-f ..., --format=... Specify the naming format
-r, --recursive Work recursively on the specified
directory
-h, --help Display this help
Formatting:
The following information is available to be used in the file name:
album artist title track
To specify a file name format, enter the desired format enclosed in quotation
marks. The words album, artist, title, and track will be replaced by values
retrieved from the audio file's metadata.
For example, --format="artist - album [track] title" will rename music files
with the name format:
Sample Artist - Sample Album [1] Sample Title
The following characters are of special importance to the operating system
and cannot be used in the file name:
\ / : * ? " < > |
(=) is replaced by the directory path separator, so to move files into
artist and album subdirectories, the following format can be used:
"artist(=)album(=)track - title"
If no format is provided, the default format is the same as used in the above
example.
Examples:
renameMP3.py Renames music files in the current
directory
renameMP3.py -d /music/path/ Renames music files in /music/path/
renameMP3.py -f "title -- artist" Renames music files in the current
directory with the name format:
Sample Title -- Sample Artist.mp3
renameMP3.py -d . -r
pip install mutagen --user
pip install easyid3 --user
pip install soundscrape --user
"""
### Imports ###
import time
import re
import os
import getopt
import sys
import fnmatch
import mutagen.easyid3
import mutagen.oggvorbis
restrictedCharPattern = re.compile('[\\\:\/\*\?"<>\|]')
formatPattern = re.compile('artist|album|title|track')
### Exceptions ###
class FormatError(Exception):
"""
Exception raised due to improper formatting
"""
pass
class DirectoryError(Exception):
"""
Exception raised due to a non-existent directory
"""
pass
### Definitions ###
def create_directory_of_file(file):
path = os.path.dirname(file)
try:
os.stat(path)
except:
os.makedirs(path)
# get the size of the console:
def get_console_size():
rows, columns = os.popen('stty size', 'r').read().split()
return {
'x': int(columns),
'y': int(rows)
}
CONSOLE_SIZE = get_console_size()
def clear_line():
print("\r" + " "*CONSOLE_SIZE["x"] + "\r", end="")
def print_mangle(data):
out = ""
for elem in data:
try:
if elem in "AZERTYUIOPQSDFGHJKLMWXCVBNazertyuiopqsdfghjklmwxcvbn1234567890)_-()éèà@ù!/:.;,?*µ%$£}{[]><":
out += elem
except:
pass
return out
##
## @brief Get list of all Files in a specific path (with a regex)
## @param[in] path (string) Full path of the machine to search files (start with / or x:)
## @param[in] regex (string) Regular expression to search data
## @param[in] recursive (bool) List file with recursive search
## @param[in] remove_path (string) Data to remove in the path
## @return (list) return files requested
##
def get_list_of_file_in_path(path, filter="*", recursive = False, remove_path=""):
print(" ******** " + path)
out = []
if os.path.isdir(os.path.realpath(path)):
tmp_path = os.path.realpath(path)
else:
print("[E] path does not exist : '" + str(path) + "'")
last_x = 0
for root, dirnames, file_names in os.walk(tmp_path):
deltaRoot = root[len(tmp_path):]
while len(deltaRoot) > 0 \
and ( deltaRoot[0] == '/' \
or deltaRoot[0] == '\\' ):
deltaRoot = deltaRoot[1:]
clear_line()
print("[I] path: '" + print_mangle(str(deltaRoot)) + "'", end="")
#ilter some stupid path ... thumbnails=>perso @eaDir synology
if ".thumbnails" in deltaRoot \
or "@eaDir" in deltaRoot:
continue
if recursive == False \
and deltaRoot != "":
return out
tmpList = []
for elem in filter:
tmpppp = fnmatch.filter(file_names, elem)
for elemmm in tmpppp:
tmpList.append(elemmm)
# Import the module :
for cycleFile in tmpList:
#for cycleFile in file_names:
add_file = os.path.join(tmp_path, deltaRoot, cycleFile)
if len(remove_path) != 0:
if add_file[:len(remove_path)] != remove_path:
print("[E] Request remove start of a path that is not the same: '" + add_file[:len(remove_path)] + "' demand remove of '" + str(remove_path) + "'")
else:
add_file = add_file[len(remove_path)+1:]
out.append(add_file)
print(" len out " + str(len(out)))
return out;
list_of_artist = []
list_of_album = []
list_of_title = []
class AudioFile:
"""
A generic audio file
"""
def __init__(self, file_name):
self.file_name = file_name
self.move_folder = ""
self.file_ext = os.path.splitext(file_name)[1].lower()
self.file_path = os.path.split(file_name)[0] + os.path.sep
try:
self.data = getattr(self, "parse_%s" % self.file_ext[1:])()
except:
self.data = None
self.move_folder = "zzz_error/"
# call the appropriate method based on the file type
if self.data == None:
return
self.generate()
def generate(self):
def lookup(key, default):
return self.data[key][0] if ( key in self.data.keys() and
self.data[key][0] ) else default
self.artist = lookup("artist", "No Artist")
self.album = lookup("album", "No Album")
self.title = lookup("title", "No Title")
self.track = lookup("tracknumber", "0")
if self.track != "0":
self.track = self.track.split("/")[0].lstrip("0")
# In regards to track numbers, self.data["tracknumber"] returns numbers
# in several different formats: 1, 1/10, 01, or 01/10. Wanting a
# consistent format, the returned string is split at the "/" and leading
# zeros are stripped.
try:
if int(self.track) < 10:
self.track = "0" + self.track
except:
pass
if self.artist == "" or self.artist == "No Artist":
self.data = None
if self.title == "" or self.title == "No Title":
self.data = None
if self.album == "" or self.album == "No Album":
self.data = None
def parse_mp3(self):
data = mutagen.easyid3.EasyID3(self.file_name)
return data
def parse_ogg(self):
return mutagen.oggvorbis.Open(self.file_name)
def rename(self, newfile_name):
def unique_name(newfile_name, count=0):
"""
Returns a unique name if a file already exists with the supplied
name
"""
if count == 0:
c = ""
else:
c = "_(%s)" % str(count)
prefix = directory + os.path.sep
testfile_name = prefix + newfile_name + c + self.file_ext
if os.path.isfile(testfile_name) == True:
return unique_name(newfile_name, count + 1)
else:
return testfile_name
if self.file_name == newfile_name:
print("get same file : " + self.file_name)
return
new_name = unique_name(newfile_name)
create_directory_of_file(new_name)
os.renames(self.file_name, new_name)
# Note: this function is quite simple at the moment; it does not support
# multiple file extensions, such as "sample.txt.backup", which would
# only retain the ".backup" file extension.
def clean_file_name(self, format):
"""
Generate a clean file name based on metadata
"""
if self.data == None:
return (self.file_name, self.move_folder + self.file_name)
rawfile_name = format % {"artist": self.artist,
"album": self.album,
"title": self.title,
"track": self.track}
rawfile_name.encode("ascii", "replace")
# encode is used to override the default encode error-handing mode;
# which is to raise a UnicodeDecodeError
clean_file_name = re.sub(restrictedCharPattern, "+", rawfile_name)
# remove restricted file_name characters (\, :, *, ?, ", <, >, |) from
# the supplied string
if self.move_folder != "":
clean_file_name = self.move_folder + clean_file_name
print("******** move in ZZ " + clean_file_name)
return (self.file_name, clean_file_name.replace("(=)", os.path.sep))
def safety(message):
print("\n***Attention: %s***" % message)
#safety = raw_input("Enter 'ok' to continue (any other response will abort): ")
safety = "ok"
if safety.lower().strip() != "ok":
print("\n***Attention: aborting***")
sys.exit()
def work(directory, format, recursive):
#fileList = get_list_of_file_in_path(directory, [".mp3", ".ogg"], recursive)
fileList = get_list_of_file_in_path(directory, ["*.*"], recursive=recursive)
try:
count = 0
total = len(fileList)
safety("all audio files in %s will be renamed : %d " % (directory, total))
print("\n***Attention: starting***")
start = time.time()
for file in fileList:
count += 1
current = AudioFile(file)
src_file_name, new_tmp_file_name = current.clean_file_name(format)
#if src_file_name == new_tmp_file_name:
# continue
current.rename(new_tmp_file_name)
new_tmp_file_name = print_mangle(new_tmp_file_name)
print("Renamed %d/%d %d/100: %s" % (count, total, int((count*100)/total), new_tmp_file_name))
print("\n%d files renamed in %f seconds" % (len(fileList),
time.time() - start))
except:
print("\n***Error: %s***" % file)
raise
### Main ###
directory = os.getcwd()
import argparse
parser = argparse.ArgumentParser(description='Check comparison between 2 path.')
parser.add_argument('--format',
type=str,
action='store',
default="%(artist)s(=)%(album)s(=)%(track)s-%(title)s",
help='data formating of the output')
parser.add_argument('--directory',
type=str,
action='store',
default=directory,
help='Path to ordering all the data')
parser.add_argument('--recursive',
action='store_true',
default=False,
help='parse recursively all the directory')
args = parser.parse_args()
def verifyFormat(format):
"""
Verify the supplied file_name format
"""
if re.search(restrictedCharPattern, format):
raise(FormatError, "supplied format contains restricted characters")
if not re.search(formatPattern, format):
raise(FormatError, "supplied format does not contain any metadata keys")
# the supplied format must contain at least one of "artist",
# "album", "title", or "track", or all files will be named
# identically
format = format.replace("artist", "%(artist)s")
format = format.replace("album", "%(album)s")
format = format.replace("title", "%(title)s")
format = format.replace("track", "%(track)s")
return format
try:
format = verifyFormat(args.format)
except FormatError:
print("\n***Error: %s***" % error)
sys.exit(2)
except error:
print("\n***Error: %s***" % error)
sys.exit(2)
work(args.directory, args.format, args.recursive)