Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 210 lines (182 sloc) 7.61 KB
#!/usr/bin/env python2.7
__author__ = ['[Brandon Amos](http://bamos.github.io)']
__date__ = '2014.04.19'
"""
This script (music-organizer.py) organizes my music collection for
iTunes and [mpv](http://mpv.io) using tag information.
The directory structure is `<artist>/<track>`, where `<artist>` and `<track>`
are lower case strings separated by dashes.
See my blog post
[Using Python to organize a music directory](http://bamos.github.io/2014/07/05/music-organizer/)
for a more detailed overview of this script.
"""
import argparse
import glob
import os
import re
import shutil
import sys
from mutagen.easyid3 import EasyID3
parser = argparse.ArgumentParser(
description='''Organizes a music collection using tag information.
The directory format is that the music collection consists of
artist subdirectories, and there are 2 modes to operate on
the entire collection or a single artist.
All names are made lowercase and separated by dashes for easier
navigation in a Linux filesystem.'''
)
parser.add_argument('--delete-conflicts', action='store_true',
dest='delete_conflicts',
help='''If an artist has duplicate tracks with the same name,
delete them. Note this might always be best in case an
artist has multiple versions. To keep multiple versions,
fix the tag information.''')
parser.add_argument('--ignore-multiple-artists', action='store_true',
dest='ignore_multiple_artists',
help='''This script will prompt for confirmation if an artist
directory has songs with more than 2 different tags.
This flag disables the confirmation and won't perform
this check.''')
parser.add_argument('--collection', action='store_true',
help='''Operate in 'collection' mode and run 'artist' mode
on every subdirectory.''')
parser.add_argument('--artist', action='store_true',
help='''Operate in 'artist' mode and copy all songs to the
root of the directory and cleanly format the names to
be easily typed and navigated in a shell.''')
parser.add_argument('--delete-unrecognized-extensions', action='store_true',
dest='delete_unrecognized')
args = parser.parse_args()
if args.collection and args.artist:
print("Error: Only provide 1 of '--collection' or '--artist'.")
sys.exit(-1)
elif not (args.collection or args.artist):
print("Error: Mode '--collection' or '--artist' not provided.")
sys.exit(-1)
# Maps a string such as 'The Beatles' to 'the-beatles'.
def toNeat(s):
s = s.lower().replace("&", "and")
# Put spaces between and remove blank characters.
blankCharsPad = r"()\[\],.\\\?\#/\!\$\:\;"
blankCharsNoPad = r"'\""
s = re.sub(r"([" + blankCharsPad + r"])([^ ])", "\\1 \\2", s)
s = re.sub("[" + blankCharsPad + blankCharsNoPad + "]", "", s)
# Replace spaces with a single dash.
s = re.sub(r"[ \*\_]+", "-", s)
s = re.sub("-+", "-", s)
s = re.sub("^-*", "", s)
s = re.sub("-*$", "", s)
# Ensure the string is only alphanumeric with '-', '+', and '='.
search = re.search("[^0-9a-z\-\+\=]", s)
if search:
print("Error: Unrecognized character in '" + s + "'")
sys.exit(-42)
return s
def artist(artistDir):
print("Organizing artist '" + artistDir + "'.")
if not args.ignore_multiple_artists:
artists = set()
for dirname, dirnames, filenames in os.walk(artistDir):
# Make sure there aren't a lot of different artists
# in case this was called from the wrong directory.
for filename in filenames:
try:
audio = EasyID3(os.path.join(dirname, filename))
artist = audio['artist'][0].decode()
artists.add(artist)
except:
pass
if len(artists) > 2:
while True:
print("Warning: More than 2 artists found in '{}'.".format(
artistDir))
print("This will move all songs to the '{}' directory.".format(
artistDir))
print("Continue? yes/no")
choice = raw_input().lower()
valid = {"yes": True, "y": True, "no": False, "n": False}
if choice in valid:
if valid[choice]:
break
else:
print("Exiting.")
sys.exit(-1)
delete_dirs = []
for dirname, dirnames, filenames in os.walk(artistDir):
# Move all the files to the root directory.
for filename in filenames:
ext = os.path.splitext(filename)[1]
if ext == ".mp3":
fullPath = os.path.join(dirname, filename)
print("file: " + str(fullPath))
try:
audio = EasyID3(fullPath)
title = audio['title'][0].encode('ascii', 'ignore')
print(" title: " + title)
except:
title = None
if not title:
print("Error: title not found for '" + filename + "'")
sys.exit(-42)
neatTitle = toNeat(title)
print(" neatTitle: " + neatTitle)
newFullPath = os.path.join(artistDir, neatTitle + ext)
print(" newFullPath: " + newFullPath)
if newFullPath != fullPath:
if os.path.isfile(newFullPath):
if args.delete_conflicts:
os.remove(fullPath)
print("File exists: '" + newFullPath + "'")
print("Deleted: '" + fullPath + "'")
else:
print("Error: File exists: '" + newFullPath + "'")
sys.exit(-42)
else:
os.rename(fullPath, newFullPath)
elif ext == ".pdf":
pass
else:
if not args.delete_unrecognized:
print("Error: Unrecognized file extension in '{}'.".format(
filename))
sys.exit(-42)
# Delete all subdirectories.
for subdirname in dirnames:
delete_dirs.append(subdirname)
for d in delete_dirs:
shutil.rmtree(os.path.join(artistDir, d), ignore_errors=True)
def song(filename):
if filename[0] == '.':
print("Ignoring dotfile: '{}'".format(filename))
return
print("Organizing song '" + filename + "'.")
ext = os.path.splitext(filename)[1]
try:
audio = EasyID3(filename)
artist = audio['artist'][0].encode('ascii', 'ignore')
title = audio['title'][0].encode('ascii', 'ignore')
print(" artist: " + artist)
print(" title: " + title)
except:
artist = None
title = None
neatArtist = toNeat(artist)
neatTitle = toNeat(title)
print(" neatArtist: " + neatArtist)
print(" neatTitle: " + neatTitle)
if not os.path.isdir(neatArtist):
os.mkdir(neatArtist)
newFullPath = os.path.join(neatArtist, neatTitle + ext)
os.rename(filename, newFullPath)
def collection():
for f in glob.glob('*'):
if os.path.isdir(f):
if f != 'iTunes' and f != 'playlists':
artist(f)
elif os.path.isfile(f):
song(f)
if args.artist:
artist('.')
else:
collection()
print("\nComplete!")