Topic: m3u generating plugin (Picard)
Hello there,
Just started using Picard a week or two ago, and I love it. I checked out the plugins and while Lukáš Lalinský's CUE generator is great, it put way too much info in there for my needs. I wanted a playlist and I wanted relative paths.
So I hacked up his plugin. Basically just removing lines and editing a few. It now generates a m3u playlist, which if you don't know is just a plaintext list of files. I make a few assumptions in the implementation (because python is a stranger to me), but it works for me.
First off, it assumes all the files of the album are in the same directory, if they aren't, it will not work properly. I didn't want full static paths because I'd like to be able to move the music around, so I opted for same directory paths (just the filename).
It has the same version compatibilities as the CUE generating plugin, because it's almost exactly the same.
I made a few "enhancements" as well. Rather than whatever default directory the CUE file generator was sending me to before, I made it so that the save dialog pops up already in the same folder as the first track on the album, and the playlist is already named as "Artist - Album.m3u".
I kept Lukáš Lalinský's name on there because this plugin is still 99% his work, the only reason I put my name in there is so you don't blame him if it doesn't work properly ;p if you feel like making any improvements (I'm sure I left a ton of now unnecessary code in there) feel free to, I appreciate it.
Hope someone finds this useful and hope Lukáš doesn't mind me taking a machete to his plugin ;p
EDIT: Now it's more of an addon than a modification as per suggestion by voiceinsideyou. Can do both CUE and M3U.
# -*- coding: utf-8 -*-
PLUGIN_NAME = u"Generate cue/m3u"
PLUGIN_AUTHOR = u"Lukáš Lalinský (modified by Zwergner)"
PLUGIN_DESCRIPTION = "Generate cuesheet (.cue file) or m3u playlist (.m3u file) from an album."
PLUGIN_VERSION = "0.1"
PLUGIN_API_VERSIONS = ["0.9.0", "0.10"]
import os.path
import re
from PyQt4 import QtCore, QtGui
from picard.util import find_existing_path, encode_filename
from picard.ui.itemviews import BaseAction, register_album_action
_whitespace_re = re.compile('\s', re.UNICODE)
_split_re = re.compile('\s*("[^"]*"|[^ ]+)\s*', re.UNICODE)
def msfToMs(msf):
msf = msf.split(":")
return ((int(msf[0]) * 60 + int(msf[1])) * 75 + int(msf[2])) * 1000 / 75
class CuesheetTrack(list):
def __init__(self, cuesheet, index):
list.__init__(self)
self.cuesheet = cuesheet
self.index = index
def set(self, *args):
self.append(args)
def find(self, prefix):
return [i for i in self if tuple(i[:len(prefix)]) == tuple(prefix)]
def getTrackNumber(self):
return self.index
def getLength(self):
try:
nextTrack = self.cuesheet.tracks[self.index+1]
index0 = self.find((u"INDEX",u"01"))
index1 = nextTrack.find((u"INDEX",u"01"))
return msfToMs(index1[0][2]) - msfToMs(index0[0][2])
except IndexError:
return 0
def getField(self, prefix):
try:
return self.find(prefix)[0][len(prefix)]
except IndexError:
return u""
def getArtist(self):
return self.getField((u"PERFORMER",))
def getTitle(self):
return self.getField((u"TITLE",))
def setArtist(self, artist):
found = False
for item in self:
if item[0] == u"PERFORMER":
if not found:
item[1] = artist
found = True
else:
del item
if not found:
self.append((u"PERFORMER", artist))
artist = property(getArtist, setArtist)
class Cuesheet(object):
def __init__(self, filename):
self.filename = filename
self.tracks = []
def read(self):
f = open(encode_filename(self.filename))
self.parse(f.readlines())
f.close()
def unquote(self, string):
if string.startswith('"'):
if string.endswith('"'):
return string[1:-1]
else:
return string[1:]
return string
def quote(self, string):
if _whitespace_re.search(string):
return '"' + string.replace('"', '\'') + '"'
return string
def parse(self, lines):
track = CuesheetTrack(self, 0)
self.tracks = [track]
isUnicode = False
for line in lines:
# remove BOM
if line.startswith('\xfe\xff'):
isUnicode = True
line = line[1:]
# decode to unicode string
line = line.strip()
if isUnicode:
line = line.decode('UTF-8', 'replace')
else:
line = line.decode('ISO-8859-1', 'replace')
# parse the line
split = [self.unquote(s) for s in _split_re.findall(line)]
keyword = split[0].upper()
if keyword == 'TRACK':
trackNum = int(split[1])
track = CuesheetTrack(self, trackNum)
self.tracks.append(track)
track.append(split)
def write(self):
lines = []
for track in self.tracks:
num = track.index
for line in track:
indent = 0
if num > 0:
if line[0] == "TRACK":
indent = 2
elif line[0] != "FILE":
indent = 4
line2 = u" ".join([self.quote(s) for s in line])
lines.append(" " * indent + line2.encode("UTF-8") + "\n")
f = open(encode_filename(self.filename), "wt")
f.writelines(lines)
f.close()
class Playlist(object):
def __init__(self, filename):
self.filename = filename
self.tracks = []
def read(self):
f = open(encode_filename(self.filename))
self.parse(f.readlines())
f.close()
def unquote(self, string):
if string.startswith('"'):
if string.endswith('"'):
return string[1:-1]
else:
return string[1:]
return string
def parse(self, lines):
track = CuesheetTrack(self, 0)
self.tracks = [track]
isUnicode = False
for line in lines:
# remove BOM
if line.startswith('\xfe\xff'):
isUnicode = True
line = line[1:]
# decode to unicode string
line = line.strip()
if isUnicode:
line = line.decode('UTF-8', 'replace')
else:
line = line.decode('ISO-8859-1', 'replace')
# parse the line
split = [self.unquote(s) for s in _split_re.findall(line)]
keyword = split[0].upper()
if keyword == 'TRACK':
trackNum = int(split[1])
track = CuesheetTrack(self, trackNum)
self.tracks.append(track)
track.append(split)
def write(self):
lines = []
for track in self.tracks:
num = track.index
for line in track:
indent = 0
if num > 0:
if line[0] == "TRACK":
indent = 2
elif line[0] != "FILE":
indent = 4
line2 = u" ".join(line)
lines.append(line2.encode("UTF-8") + "\n")
f = open(encode_filename(self.filename), "wt")
f.writelines(lines)
f.close()
class GenerateM3u(BaseAction):
NAME = "Generate &m3u Playlist..."
def callback(self, objs):
album = objs[0]
trackpath = album.tracks[0].linked_files[0]
current_directory = album.tracks[0].linked_files[0].filename or self.config.persist["current_directory"] or QtCore.QDir.homePath()
current_directory = find_existing_path(unicode(current_directory))
current_directory += "\\" + album.metadata["albumartist"] + " - " + album.metadata["album"]
selected_format = QtCore.QString()
filename = QtGui.QFileDialog.getSaveFileName(None, "", current_directory, "M3U Playlist (*.m3u)", selected_format)
if filename:
filename = unicode(filename)
playlist = Playlist(filename)
while len(playlist.tracks) <= len(album.tracks):
track = CuesheetTrack(playlist, len(playlist.tracks))
playlist.tracks.append(track)
t = playlist.tracks[0]
index = 0.0
for i, track in enumerate(album.tracks):
t = playlist.tracks[i + 1]
for file in track.linked_files:
audio_filename = file.filename
audio_filename = os.path.basename(audio_filename)
audio_filename = audio_filename
t.set(audio_filename)
playlist.write()
class GenerateCuesheet(BaseAction):
NAME = "Generate &Cuesheet..."
def callback(self, objs):
album = objs[0]
current_directory = album.tracks[0].linked_files[0].filename or self.config.persist["current_directory"] or QtCore.QDir.homePath()
current_directory = find_existing_path(unicode(current_directory))
current_directory += "\\" + album.metadata["albumartist"] + " - " + album.metadata["album"]
selected_format = QtCore.QString()
filename = QtGui.QFileDialog.getSaveFileName(None, "", current_directory, "Cuesheet (*.cue)", selected_format)
if filename:
filename = unicode(filename)
cuesheet = Cuesheet(filename)
#try: cuesheet.read()
#except IOError: pass
while len(cuesheet.tracks) <= len(album.tracks):
track = CuesheetTrack(cuesheet, len(cuesheet.tracks))
cuesheet.tracks.append(track)
#if len(cuesheet.tracks) > len(album.tracks) - 1:
# cuesheet.tracks = cuesheet.tracks[0:len(album.tracks)+1]
t = cuesheet.tracks[0]
t.set("PERFORMER", album.metadata["albumartist"])
t.set("TITLE", album.metadata["album"])
t.set("REM", "MUSICBRAINZ_ALBUM_ID", album.metadata["musicbrainz_albumid"])
t.set("REM", "MUSICBRAINZ_ALBUM_ARTIST_ID", album.metadata["musicbrainz_albumartistid"])
if "date" in album.metadata:
t.set("REM", "DATE", album.metadata["date"])
index = 0.0
for i, track in enumerate(album.tracks):
mm = index / 60.0
ss = (mm - int(mm)) * 60.0
ff = (ss - int(ss)) * 75.0
index += track.metadata.length / 1000.0
t = cuesheet.tracks[i + 1]
t.set("TRACK", "%02d" % (i + 1), "AUDIO")
t.set("PERFORMER", track.metadata["artist"])
t.set("TITLE", track.metadata["title"])
t.set("REM", "MUSICBRAINZ_TRACK_ID", track.metadata["musicbrainz_trackid"])
t.set("REM", "MUSICBRAINZ_ARTIST_ID", track.metadata["musicbrainz_artistid"])
t.set("INDEX", "01", "%02d:%02d:%02d" % (mm, ss, ff))
for file in track.linked_files:
audio_filename = file.filename
if os.path.dirname(filename) == os.path.dirname(audio_filename):
audio_filename = os.path.basename(audio_filename)
cuesheet.tracks[i].set("FILE", audio_filename, "MP3")
cuesheet.write()
register_album_action(GenerateCuesheet())
register_album_action(GenerateM3u())just make a text file named cuem3u.py and pop it into your plugin directory.