#!/usr/bin/python
""" Generate shell scripts to fix "Unknown Album" problems.
ASSUMPTIONS:
* pork.py is in /opt/pork
* tracks.sh.template is in /opr/pork/templates
* id3lib and PyYAML are installed
* jinja2 is installed
This script walks all subdirectories under the supplied one, or a default:
/storage/music/mp3/CDs/U/Unknown Artist/
searching for those containing a file called 'tracklist.txt'. If present,
this file is parsed according to a simple format.
* the first line is the album title
* all subsequent lines are tracks
* track lines are 3 values, separated by tabs
* track number
* artist
* name
If the file parses according to that format, a shell script is written
to the same directory. The script is called 'tracks.sh', and contains
the commands to tag and rename the mp3s.
If this is all successful, 'tracklist.txt' is renamed 'tracklist-done.txt'.
This is so fix-unknowns.py does not operate on the same directory twice,
for example when scheduled to run via cron.
TODO: loads. Seriously, there's loads more that can happen here. Here's
some ideas.
* Submit tags up to freedb
* Rename the directory itself
* Move the directory into the correct place
* Take /etc/ripit/config into account regarding directory structure,
mp3 naming convention, etc
* Work on non-mp3s (like flac; ie, use a different template and
spit out different commands)
* Execute tracks.sh and remove it afterwards (ooh, that'd be nice).
* I'm sure there's loads more
"""
import os, sys
sys.path.insert(0,'/opt/pork')
from pork import Renderer
base = os.path.join('/storage','music','mp3','CDs','U','Unknown Artist')
config = { 'template': '/opt/pork/templates/tracks.sh.template',
'engine': 'jinja2' }
def parse_tracklist(dir):
""" Parse a tracklist.txt file.
Accepts a directory as argument.
If there's a tracklist.txt file inside the given directory, and it
successfully parses, then a dictionary is returned. If there is no
such file, or there is but it doesn't parse, then returns None.
"""
file = os.path.join(dir,'tracklist.txt')
if not os.path.exists(file):
return None
try:
handle = open(file)
except IOError, e:
print "### couldn't open tracklist.txt"
print "### error was [%s]" % e
return None
# we will track the unique artists. if there's more than one,
# then the album is officially by "Various Artists". Because I say so.
artists = []
# first line is the album title. end of.
album_title = handle.readline()
# this matches the YAML we want to spit out
album = { 'album': album_title.rstrip(),
'songs': [] }
songs = handle.readlines()
for line_num, song in enumerate(songs):
# 0-indexed
line_num = line_num + 1
try:
num, artist, name = song.rstrip().split('\t')
except ValueError:
# we bail at the very first error!
print "### error with track [%s]" % line_num
print "### [%s]" % song.rstrip()
return None
else:
album['songs'].append({ 'num': int(num),
'artist': str(artist),
'name': str(name) })
if artist not in artists:
artists.append(artist)
# hmm. I thought I was going to use this, but I haven't yet. Maybe we'll
# do something with the TPE2 tag, or use it to decide between
# Various Artists vs <Artist> when moving the directory, if/when I get
# round to writing that part. Who knows how useful it will be?
if len(artists) > 1:
album['compilation'] = True
album['total'] = len(album['songs'])
return album
def fix_unknown_album(arg, dirname, fnames):
""" Path-walking tracks.sh emitter.
This function is called from by os.path.walk. For each directory, if it
contains a file called 'tracklist.txt' it tries to parse it; if that works,
it uses the info in it to create a file called 'tracks.sh' in the same
directory. Finally, it renames 'tracklist.txt' to 'tracklist-done.txt'.
tracks.sh contains the commands to tag and rename the mp3s according to
the tracklist.
"""
# ignore the top level directory
if dirname == base:
return
if 'tracklist.txt' in fnames:
print "### found tracklist.txt in [%s]" % dirname
tracklist = parse_tracklist(dirname)
if tracklist:
path = os.path.join(base, dirname)
# EPIC PORK WIN
# ahem
# what I mean by that is, the pork.py Renderer does everything
# we want already. there's no need to create any yaml first,
# all pork.py does is turn that back into the dictionaries
# we've already created.
target = os.path.join(path, 'tracks.sh')
config.update({'target': target})
Renderer(config, tracklist).spit()
os.rename(os.path.join(path, 'tracklist.txt'),
os.path.join(path, 'tracklist-done.txt'))
else:
print "### didn't parse, ignoring"
else:
print "### no tracklist.txt in [%s], ignoring" % dirname
def main(dir=base):
os.path.walk(dir, fix_unknown_album, None)
if __name__ == '__main__':
# accept an argument, otherwise just go to the default place
if len(sys.argv) > 1:
main(sys.argv[1])
else:
main()