diff options
Diffstat (limited to 'cue.py')
| -rw-r--r-- | cue.py | 359 |
1 files changed, 0 insertions, 359 deletions
@@ -1,359 +0,0 @@ -from cchardet import detect as encoding_detect -import codecs -import sys -import re - -class Track: - def __init__(self, number, datatype): - try: - self.number = int(number) - except ValueError: - raise InvalidCommand("invalid number \"%s\"" % number) - - self.type = datatype - self._indexes = {} - self._attrs = {} - - def attrs(self): - return sorted(self._attrs.items()) - - def indexes(self): - return sorted(self._indexes.items()) - - def get(self, attr): - return self._attrs.get(attr, - None if attr in ("pregap", "postgap") else "" - ) - - def isaudio(self): - return self.type == "AUDIO" and self.begin is not None - -class File: - def __init__(self, name, filetype): - self.name = name - self.type = filetype - self._tracks = [] - - def tracks(self, filter_audio = True): - return filter(Track.isaudio if filter_audio else None, self._tracks) - - def add_track(self, track): - self._tracks.append(track) - - def isaudio(self): - return self.type == "WAVE" - - def has_audio_tracks(self): - return len(list(self.tracks())) > 0 - - def split_points(self, info): - rate = info.sample_rate * info.bits_per_sample * info.channels // 8 - - for track in list(self.tracks())[1:]: - yield rate * track.begin // 75 - - def __repr__(self): - return self.name - -class Cue: - def __init__(self): - self._attrs = {} - self._files = [] - - def attrs(self): - return sorted(self._attrs.items()) - - def files(self, filter_audio = True): - return filter(File.isaudio if filter_audio else None, self._files) - - def get(self, attr): - return self._attrs.get(attr, "") - - def add_file(self, file): - self._files.append(file) - -class CueParserError(Exception): - pass - -class UnknownCommand(CueParserError): - pass - -class InvalidCommand(CueParserError): - pass - -class InvalidContext(CueParserError): - pass - -class Context: - ( - GENERAL, - TRACK, - FILE - ) = range(3) - -def check(count = None, context = None): - def deco(func): - def method(cls, *lst): - if count is not None: - n = len(lst) - if n != count: - raise InvalidCommand( - "%d arg%s expected, got %d" % - (count, "s" if count > 1 else "", n) - ) - if context is not None: - if type(context) in (list, tuple): - if cls.context not in context: - raise InvalidContext - elif cls.context != context: - raise InvalidContext - func(cls, *lst) - return method - return deco - -class CueParser: - re_timestamp = re.compile("^[\d]{1,3}:[\d]{1,2}:[\d]{1,2}$") - rem_commands = ('genre', 'date', 'comment') - - def __init__(self): - def do_set_attr(name, cue = False, track = False, convert = None): - def func(*args): - n = len(args) - if n != 1: - raise InvalidCommand("1 arg expected, got %d" % n) - opt = {} - if cue: - opt[Context.GENERAL] = self.cue - if track: - opt[Context.TRACK] = self.track - - arg = convert(args[0]) if convert else args[0] - self.set_attr(name, arg, opt) - return func - - self.cue = Cue() - self.context = Context.GENERAL - self.track = None - self.file = None - - self.commands = { - "file": self.parse_file, - "flags": self.parse_flags, - "index": self.parse_index, - "pregap": self.parse_pregap, - "rem": self.parse_rem, - "track": self.parse_track, - - "catalog": do_set_attr("catalog", cue = True), - "performer": do_set_attr("performer", cue = True, track = True), - "postgap": do_set_attr("postgap", track = True, convert = self.parse_timestamp), - "songwriter": do_set_attr("songwriter", cue = True, track = True), - "title": do_set_attr("title", cue = True, track = True), - - "cdtextfile": self.parse_skip, - "isrc": self.parse_skip, - } - - @staticmethod - def split_args(args): - lst = [] - quote = None - cur = [] - - def push(): - lst.append("".join(cur)) - cur[:] = [] - - for ch in args: - if quote: - if ch != quote: - cur.append(ch) - else: - quote = None - elif ch.isspace(): - if cur: - push() - elif ch in ("\"", "'"): - quote = ch - else: - cur.append(ch) - - if quote: - raise CueParserError("unclosed quote '%s'" % quote) - - if cur: - push() - - return lst - - @staticmethod - def parse_timestamp(time): - if not CueParser.re_timestamp.match(time): - raise InvalidCommand("invalid timestamp \"%s\"" % time) - - m, s, f = map(int, time.split(":")) - return (m * 60 + s) * 75 + f - - def get_cue(self): - return self.cue - - @check(2) - def parse_file(self, *args): - self.file = File(*args) - self.cue.add_file(self.file) - self.context = Context.FILE - - @check(2, (Context.FILE, Context.TRACK)) - def parse_track(self, *args): - self.track = Track(*args) - self.file.add_track(self.track) - self.context = Context.TRACK - - @check(2, Context.TRACK) - def parse_index(self, number, time): - if "postgap" in self.track._attrs: - raise InvalidCommand("after POSTGAP") - try: - number = int(number) - except ValueError: - raise InvalidCommand("invalid number \"%s\"" % number) - if number is 0 and "pregap" in self.track._attrs: - raise InvalidCommand("conflict with previous PREGAP") - if number in self.track._indexes: - raise InvalidCommand("duplicate index number %d" % number) - - self.track._indexes[number] = self.parse_timestamp(time) - - @check(1, Context.TRACK) - def parse_pregap(self, time): - if self.track._indexes: - raise InvalidCommand("must appear before any INDEX commands for the current track") - self.set_attr("pregap", self.parse_timestamp(time), obj = self.track) - - def set_attr(self, attr, value, opt = None, obj = None): - if opt is not None: - obj = opt.get(self.context) - if obj is None: - raise InvalidContext - elif obj is None: - raise CueParserError("CueParserError.set_attr: invalid usage") - - if attr in obj._attrs: - raise InvalidCommand("duplicate") - - obj._attrs[attr] = value - - @check(context = Context.TRACK) - def parse_flags(self, *flags): - if self.track._indexes: - raise InvalidCommand("must appear before any INDEX commands") - - def parse_rem(self, opt, value = None, *args): - cmd = opt.lower() - if value and cmd in self.rem_commands: - if len(args): - raise InvalidCommand("extra arguments for \"%s\"" % opt) - self.set_attr(cmd, value, obj = self.cue) - - def parse_skip(self, *args): - pass - - def parse_default(self, *args): - raise UnknownCommand - - def parse(self, cmd, arg): - self.commands.get(cmd.lower(), self.parse_default)(*self.split_args(arg)) - - def calc_offsets(self): - for file in self.cue._files: - previous = None - for track in file._tracks: - track.begin = None - track.end = None - - pregap = track.get("pregap") - if pregap is None and 0 in track._indexes: - pregap = track._indexes[0] - if pregap is not None and previous and previous.end is None: - previous.end = pregap - - try: - track.begin = min([v for k, v in track._indexes.items() if k != 0]) - except: - continue - - if previous and previous.end is None: - previous.end = track.begin if pregap is None else pregap - - postgap = track.get("postgap") - if postgap is not None: - track.end = postgap - - previous = track - -def __read_file(filename, coding = None): - f = open(filename, "rb") - data = f.read() - f.close() - - if coding: - return data.decode(coding) - - encoded = None - try: - encoded = data.decode("utf-8-sig") - except UnicodeDecodeError: - pass - - if encoded is None: - enc = encoding_detect(data) - if enc is None: - raise Exception("autodetect failed") - encoding = enc["encoding"] - try: - encoded = data.decode(encoding) - except UnicodeDecodeError: - raise Exception("autodetect failed: invalid encoding %s" % encoding) - except Exception as exc: - raise Exception("decoding failed: %s" % exc) - - return encoded - -def read(filename, coding = None, on_error = None): - if on_error: - def msg(fmt, *args): - err = CueParserError(fmt % args) - err.line = nline - on_error(err) - else: - msg = lambda *args: None - - cuefile = __read_file(filename, coding) - parser = CueParser() - - nline = 0 - - for line in cuefile.split("\n"): - nline = nline + 1 - s = line.strip() - if not len(s): - continue - - data = s.split(None, 1) - if len(data) is 1: - msg("invalid command \"%s\": arg missed", data[0]) - continue - - try: - parser.parse(*data) - except UnknownCommand: - msg("unknown command \"%s\"", data[0]) - except InvalidContext: - msg("invalid context for command \"%s\"", data[0]) - except InvalidCommand as err: - msg("invalid command \"%s\": %s", data[0], err) - except CueParserError as err: - msg("%s", err) - - parser.calc_offsets() - return parser.get_cue() |
