tags2playlist is a Python script for automatic playlist generation.
Creating playlists manually can be a real pain, especially if you have music from lots of different artists. If you just want to listen to a specific genre, it takes a lot of time to create genre playlists and to update them on adding or removal of new artists. Also, renaming files or directories will lead to corrupt playlists.
tags2playlist will do this automatically if you have a tidy music collection, that is a directory for each artist where the directory name matches the artist's name. This script will look for all subdirectories (=artists) in a given directory containing a file named .tags and add all files in the corresponding subdirectories to all tag playlists described in the .tags file (see example below). As the creation of those .tags files may also take some time, a little helper script is included to retrieve tags automatically from last.fm.
Imagine your music is in /path/to/mymusic/ (there is also support for multiple music directories) with the following structure:
/path/to/mymusic/artist one/great song.mp3 /path/to/mymusic/artist one/great album/even greater song.mp3 /path/to/mymusic/artist two ...
You just have to create a file .tags in each of the artist directories containing a list of tags (one per line), e.g.
/path/to/mymusic/artist one/.tags /path/to/mymusic/artist two/.tags ...
where the content of /path/to/mymusic/artist one/.tags may look like:
hard rock heavy metal
Now you just have to run the script as tags2playlist /path/to/mymusic/ and it will produce playlists for each of the tags containing music files of the corresponding directory. For the given example, the playlist hard rock will include the songs /path/to/mymusic/artist one/great song.mp3 and /path/to/mymusic/artist one/great album/even greater song.mp3.
If you don't want to create all those .tags files by hand, you can use the included helper script called tagginghelper. This script will automatically generate .tags files by retrieving the top tags for all your artists using last.fm.
Here is the (shortened) output of a real world music collection. First, the .tags files are generated for all music in the directories /mnt/sda6/mp3/ and /mnt/sda6/music/ with the helper script tagginghelper:
$ tagginghelper.py /mnt/sda6/mp3/ /mnt/sda6/music/ traversing directory /mnt/sda6/mp3/ retrieving top 5 tags for ACDC from last.fm writing 5 tags (hard rock, classic rock, heavy metal, rock, rock and roll) for ACDC to /mnt/sda6/mp3/ACDC/.tags retrieving top 5 tags for Across The Border from last.fm writing 5 tags (folk punk, punk, folk rock, irish folk, german) for Across The Border to /mnt/sda6/mp3/Across The Border/.tags retrieving top 5 tags for After Forever from last.fm writing 5 tags (gothic metal, symphonic metal, female fronted metal, metal, gothic) for After Forever to /mnt/sda6/mp3/After Forever/.tags retrieving top 5 tags for Ahead to the Sea from last.fm writing 5 tags (folk punk, folk, german, folk rock, folk-punk) for Ahead to the Sea to /mnt/sda6/mp3/Ahead to the Sea/.tags retrieving top 5 tags for Al Andaluz Project from last.fm writing 5 tags (medieval, mediterranean, ethereal, neomedieval, world) for Al Andaluz Project to /mnt/sda6/mp3/Al Andaluz Project/.tags retrieving top 5 tags for Al Qaynah from last.fm writing 5 tags (arabic metal, oriental metal, folk metal, eastern metal, oriental) for Al Qaynah to /mnt/sda6/mp3/Al Qaynah/.tags retrieving top 5 tags for Alchemy VII from last.fm writing 5 tags (pagan, neo-pagan, pagan music, neopagan, under 2000 listeners) for Alchemy VII to /mnt/sda6/mp3/Alchemy VII/.tags retrieving top 5 tags for Alcian Blue from last.fm writing 5 tags (shoegaze, post-punk, ambient, indie, atmospheric) for Alcian Blue to /mnt/sda6/mp3/Alcian Blue/.tags retrieving top 5 tags for Alestorm from last.fm writing 5 tags (pirate metal, folk metal, power metal, true scottish pirate metal, scottish) for Alestorm to /mnt/sda6/mp3/Alestorm/.tags [...]
Then, all .tags files are processed inside /mnt/sda6/mp3/ and /mnt/sda6/music/ by tags2playlist to generate the corresponding playlists:
$ tags2playlist.py /mnt/sda6/mp3/ /mnt/sda6/music/ searching for .tags in /mnt/sda6/mp3/ parsing /mnt/sda6/mp3/ACDC/.tags parsing /mnt/sda6/mp3/Across The Border/.tags parsing /mnt/sda6/mp3/After Forever/.tags [...] parsing /mnt/sda6/mp3/Omega Massif/.tags parsing /mnt/sda6/mp3/Soul Whirling Somewhere/.tags searching for .tags in /mnt/sda6/music/ parsing /mnt/sda6/music/Subtonix/.tags parsing /mnt/sda6/music/Swallow/.tags parsing /mnt/sda6/music/The Sky Drops/.tags parsing /mnt/sda6/music/Die Art/.tags [...] writing 286 playlists writing playlist for tag 4ad to /mnt/sda6/mp3/playlists/4ad.m3u (93 entries) writing playlist for tag 60s to /mnt/sda6/mp3/playlists/60s.m3u (252 entries) writing playlist for tag 70s to /mnt/sda6/mp3/playlists/70s.m3u (698 entries) writing playlist for tag 77 to /mnt/sda6/mp3/playlists/77.m3u (28 entries) writing playlist for tag 77 style punk to /mnt/sda6/mp3/playlists/77 style punk.m3u (28 entries) writing playlist for tag 80s to /mnt/sda6/mp3/playlists/80s.m3u (1412 entries) writing playlist for tag 90s to /mnt/sda6/mp3/playlists/90s.m3u (7 entries) writing playlist for tag a cappella to /mnt/sda6/mp3/playlists/a cappella.m3u (32 entries) writing playlist for tag a cappella metal to /mnt/sda6/mp3/playlists/a cappella metal.m3u (32 entries) writing playlist for tag acoustic to /mnt/sda6/mp3/playlists/acoustic.m3u (14 entries) writing playlist for tag alternative to /mnt/sda6/mp3/playlists/alternative.m3u (1199 entries) writing playlist for tag alternative rock to /mnt/sda6/mp3/playlists/alternative rock.m3u (170 entries) writing playlist for tag ambient to /mnt/sda6/mp3/playlists/ambient.m3u (594 entries) [...]
The format of the .tags file is rather simple. Each line contains a single tag applying to all files in the corresponding directory and all subdirectories. Additionally, the tag may be prepended by a colon and a relative path to a single music file, applying only to this file. Example:
hard rock heavy metal greatest song of all times: catfrighteners - sitting on the catstone.mp3
This will add all music files to the playlists hard rock and heavy metal and additionally add the song catfrighteners - sitting on the catstone.mp3 to the playlist named greatest song of all times. There is no limit on the number of tags per .tags file.
First, you should adapt the variable plistDir to the directory where you want to put the generated playlists in (the directory has to exist). Then, just pass one or more directories as arguments to the script, like tags2playlist /path/one /path/two /path/three […].
Just pass one or more directories which the script should traverse to generate the .tags files, like tagginghelper /path/one /path/two /path/three […]. Additionally, you may want to pass the parameter -a to just generate the .tags file for a single artist, like tagginghelper -a /path/one/great artist to just generate the file /path/one/great artist/.tags.
To retrieve more than the default 5 top tags, just edit the numTags variable in the script.
#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (C) 2010 Alexander Heinlein <alexander.heinlein@web.de> # # tags2playlist: automatic playlist generator # This script traverses a given directory in the search for .tags files # in the following format: # # tag1 # tag2 # tag3: file1 # tag3: file2 # tag4: file3 # tag5 # # Each .tags file contains a list of tags separated by newlines. Lines # without a colon simply specify the tag for files in the current directory # and all subdirectories. Lines with a colon specify tags for single files. # After gathering all tags with all corresponding files, a playlist is # generated for each tag. # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import os import sys from collections import defaultdict # files to include in the playlist mediaFiles = ["mp3", "ogg", "aac", "flac", "wma", "wav", "aiff", "mpc", "ape", "m4a"] # directory to write playlists to plistDir = "/mnt/sda6/mp3/playlists" playlists = defaultdict(list) # colored output red, yellow, reset = range(3) colors = { red : "\033[1;31m", yellow : "\033[1;33m", reset : "\033[1;m" } def printCol(text, color): print colors[color] + text + colors[reset] # removes non-media files (pretty slow implementation) def cleanFiles(files): clean = list() for file in files: dotPos = file.rfind(".") extension = file[dotPos + 1:] if extension in mediaFiles: clean.append(file) return clean # returns a list of media files in the current directory and all subdirectories def getSubFiles(dir): subFiles = list() for root, dirs, files in os.walk(dir): for file in cleanFiles(files): subFiles.append(root + "/" + file) return subFiles # parse .tags file def parseTags(dir): tagFile = dir + "/.tags" print "parsing", tagFile for line in open(tagFile, 'r'): tag = line.strip("\n") if not ":" in tag: # tag belongs to all files subFiles = getSubFiles(dir) playlists[tag].extend(subFiles) else: # tag belongs to just one file colPos = tag.find(":") file = dir + "/" + tag[colPos + 1:].lstrip(" ") tag = tag[:colPos] if not os.path.isfile(file): printCol("warning: file " + file + " referenced by " + tagFile + " does not exist or is not a file", yellow) playlists[tag].append(file) # look for file .tags in current directory and all subdirectories def walkDirectory(dir): if not os.path.isdir(dir): printCol("error: " + dir + " does not exist or is not a directory", red) return found = False for root, dirs, files in os.walk(dir): if '.tags' in files: found = True parseTags(root) if found == False: printCol("warning: couldn't find any .tags file in " + dir, yellow) # write all playlists def writePlaylists(): tags = playlists.keys() tags.sort() for tag in tags: plistOut = plistDir + "/" + tag + ".m3u" files = playlists[tag] print "writing playlist for tag " + tag + " to " + plistOut + " (" + str(len(files)) + " entries)" files.sort() plist = open(plistOut, "w") for file in files: plist.write(file + "\n") plist.close() if __name__ == '__main__': if len(sys.argv) < 2: print "usage:", sys.argv[0], "directory(ies)" sys.exit(0) for dir in sys.argv[1:]: print "searching for .tags in", dir walkDirectory(dir) print print "writing", len(playlists.keys()), "playlists" writePlaylists()
#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (C) 2010 Alexander Heinlein <alexander.heinlein@web.de> # # tagginghelper: little helper script for tags2playlist # This script traverses a given directory and retrieves for each directory # the top tags for the artist of the same name from last.fm. Then it writes # .tags files parsable by tags2playlist which will use them to generate tag # playlists. # # Note: the lastfm module requires the decorator and elementtree modules # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import os import sys import lastfm # max. number of tags per artist numTags = 5 api_key = 'db6646a55074b09f8be725bcda9088e8' api = lastfm.Api(api_key) # colored output red, yellow, reset = range(3) colors = { red : "\033[1;31m", yellow : "\033[1;33m", reset : "\033[1;m" } def printCol(text, color): print colors[color] + text + colors[reset] # ask last.fm for top tags def getLastfmTags(artist): info = api.get_artist(artist.decode('utf-8')) topTags = info.top_tags tags = list() for tag in topTags[:numTags]: tags.append(tag.name.lower()) return tags # generate .tags for given artist inside given directory def tagArtist(artist, dir): print "retrieving top", numTags, "tags for", artist, "from last.fm" try: tags = getLastfmTags(artist) except Exception as e: printCol("error for " + artist + ": " + str(e), red) return tagListOut = dir + "/" + artist + "/.tags" print "writing " + str(len(tags)) + " tags (" + ', '.join(tags) + ") for " + artist + " to " + tagListOut if len(tags) == 0: printCol("warning: no tags for " + artist, yellow) return tagList = open(tagListOut, "w") for tag in tags: tagList.write(tag + "\n") tagList.close() # process each directory name as an artist def walkDirectory(dir): artists = os.listdir(dir) artists.sort() for artist in artists: if not os.path.isdir(dir + "/" + artist): continue else: tagArtist(artist, dir) # print usage information def printUsage(name): print "usage:", name, "[-a] directory(ies)" print "little helper for tags2playlist, creates .tags files with the help of last.fm" print "-a just processes the given directory(ies) as an artist, useful for updating" print " a single .tags file or to process a newly added directory" if __name__ == '__main__': if len(sys.argv) < 2: printUsage(sys.argv[0]) sys.exit(0) # tag only given directory as one artist if(sys.argv[1] == "-a"): if len(sys.argv) < 3: printUsage(sys.argv[0]) sys.exit(0) else: for dir in sys.argv[2:]: # get artist (last directory in path) and parent dir dir = dir.rstrip("/") artistPos = dir.rfind("/") artist = dir[artistPos + 1:] parentDir = dir[:artistPos] print "processing only directory", dir, "with artist", artist tagArtist(artist, parentDir) # process all directories else: for dir in sys.argv[1:]: print "traversing directory", dir walkDirectory(dir)