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