1# -*- coding: utf-8 -*- 2 3PLUGIN_NAME = "Generate Cuesheet" 4PLUGIN_AUTHOR = "Lukáš Lalinský, Sambhav Kothari" 5PLUGIN_DESCRIPTION = "Generate cuesheet (.cue file) from an album." 6PLUGIN_VERSION = "1.2" 7PLUGIN_API_VERSIONS = ["2.0"] 8 9 10import os.path 11import re 12from PyQt5 import QtCore, QtWidgets 13from picard.util import find_existing_path, encode_filename 14from picard.ui.itemviews import BaseAction, register_album_action 15 16 17_whitespace_re = re.compile('\s', re.UNICODE) 18_split_re = re.compile('\s*("[^"]*"|[^ ]+)\s*', re.UNICODE) 19 20 21def msfToMs(msf): 22 msf = msf.split(":") 23 return ((int(msf[0]) * 60 + int(msf[1])) * 75 + int(msf[2])) * 1000 / 75 24 25 26class CuesheetTrack(list): 27 28 def __init__(self, cuesheet, index): 29 list.__init__(self) 30 self.cuesheet = cuesheet 31 self.index = index 32 33 def set(self, *args): 34 self.append(args) 35 36 def find(self, prefix): 37 return [i for i in self if tuple(i[:len(prefix)]) == tuple(prefix)] 38 39 def getTrackNumber(self): 40 return self.index 41 42 def getLength(self): 43 try: 44 nextTrack = self.cuesheet.tracks[self.index + 1] 45 index0 = self.find(("INDEX", "01")) 46 index1 = nextTrack.find(("INDEX", "01")) 47 return msfToMs(index1[0][2]) - msfToMs(index0[0][2]) 48 except IndexError: 49 return 0 50 51 def getField(self, prefix): 52 try: 53 return self.find(prefix)[0][len(prefix)] 54 except IndexError: 55 return "" 56 57 def getArtist(self): 58 return self.getField(("PERFORMER",)) 59 60 def getTitle(self): 61 return self.getField(("TITLE",)) 62 63 def setArtist(self, artist): 64 found = False 65 for item in self: 66 if item[0] == "PERFORMER": 67 if not found: 68 item[1] = artist 69 found = True 70 else: 71 del item 72 if not found: 73 self.append(("PERFORMER", artist)) 74 75 artist = property(getArtist, setArtist) 76 77 78class Cuesheet(object): 79 80 def __init__(self, filename): 81 self.filename = filename 82 self.tracks = [] 83 84 def read(self): 85 with open(encode_filename(self.filename)) as f: 86 self.parse(f.readlines()) 87 88 def unquote(self, string): 89 if string.startswith('"'): 90 if string.endswith('"'): 91 return string[1:-1] 92 else: 93 return string[1:] 94 return string 95 96 def quote(self, string): 97 if _whitespace_re.search(string): 98 return '"' + string.replace('"', '\'') + '"' 99 return string 100 101 def parse(self, lines): 102 track = CuesheetTrack(self, 0) 103 self.tracks = [track] 104 isUnicode = False 105 for line in lines: 106 # remove BOM 107 if line.startswith('\xfe\xff'): 108 isUnicode = True 109 line = line[1:] 110 # decode to unicode string 111 line = line.strip() 112 if isUnicode: 113 line = line.decode('UTF-8', 'replace') 114 else: 115 line = line.decode('ISO-8859-1', 'replace') 116 # parse the line 117 split = [self.unquote(s) for s in _split_re.findall(line)] 118 keyword = split[0].upper() 119 if keyword == 'TRACK': 120 trackNum = int(split[1]) 121 track = CuesheetTrack(self, trackNum) 122 self.tracks.append(track) 123 track.append(split) 124 125 def write(self): 126 lines = [] 127 for track in self.tracks: 128 num = track.index 129 for line in track: 130 indent = 0 131 if num > 0: 132 if line[0] == "TRACK": 133 indent = 2 134 elif line[0] != "FILE": 135 indent = 4 136 line2 = " ".join([self.quote(s) for s in line]) 137 line2 = " " * indent + line2 + "\n" 138 lines.append(line2.encode("UTF-8")) 139 with open(encode_filename(self.filename), "wb") as f: 140 f.writelines(lines) 141 142 143class GenerateCuesheet(BaseAction): 144 NAME = "Generate &Cuesheet..." 145 146 def callback(self, objs): 147 album = objs[0] 148 current_directory = self.config.persist["current_directory"] or QtCore.QDir.homePath() 149 current_directory = find_existing_path(string_(current_directory)) 150 filename, selected_format = QtWidgets.QFileDialog.getSaveFileName( 151 None, "", current_directory, "Cuesheet (*.cue)") 152 if filename: 153 filename = string_(filename) 154 cuesheet = Cuesheet(filename) 155 #try: cuesheet.read() 156 #except IOError: pass 157 while len(cuesheet.tracks) <= len(album.tracks): 158 track = CuesheetTrack(cuesheet, len(cuesheet.tracks)) 159 cuesheet.tracks.append(track) 160 #if len(cuesheet.tracks) > len(album.tracks) - 1: 161 # cuesheet.tracks = cuesheet.tracks[0:len(album.tracks)+1] 162 163 t = cuesheet.tracks[0] 164 t.set("PERFORMER", album.metadata["albumartist"]) 165 t.set("TITLE", album.metadata["album"]) 166 t.set("REM", "MUSICBRAINZ_ALBUM_ID", album.metadata["musicbrainz_albumid"]) 167 t.set("REM", "MUSICBRAINZ_ALBUM_ARTIST_ID", album.metadata["musicbrainz_albumartistid"]) 168 if "date" in album.metadata: 169 t.set("REM", "DATE", album.metadata["date"]) 170 index = 0.0 171 for i, track in enumerate(album.tracks): 172 mm = index / 60.0 173 ss = (mm - int(mm)) * 60.0 174 ff = (ss - int(ss)) * 75.0 175 index += track.metadata.length / 1000.0 176 t = cuesheet.tracks[i + 1] 177 t.set("TRACK", "%02d" % (i + 1), "AUDIO") 178 t.set("PERFORMER", track.metadata["artist"]) 179 t.set("TITLE", track.metadata["title"]) 180 t.set("REM", "MUSICBRAINZ_TRACK_ID", track.metadata["musicbrainz_trackid"]) 181 t.set("REM", "MUSICBRAINZ_ARTIST_ID", track.metadata["musicbrainz_artistid"]) 182 t.set("INDEX", "01", "%02d:%02d:%02d" % (mm, ss, ff)) 183 for file in track.linked_files: 184 audio_filename = file.filename 185 extension = audio_filename.split(".")[-1].lower() 186 if os.path.dirname(filename) == os.path.dirname(audio_filename): 187 audio_filename = os.path.basename(audio_filename) 188 if extension in ["mp3", "mp2", "m2a"]: 189 file_type = "MP3" 190 elif extension in ["aiff", "aif", "aifc"]: 191 file_type = "AIFF" 192 else: 193 file_type = "WAVE" 194 cuesheet.tracks[i].set("FILE", audio_filename, file_type) 195 196 cuesheet.write() 197 198 199action = GenerateCuesheet() 200register_album_action(action) 201