diff options
| author | mikeos <mike.osipov@gmail.com> | 2013-07-13 12:25:31 +0400 |
|---|---|---|
| committer | mikeos <mike.osipov@gmail.com> | 2013-07-13 12:25:31 +0400 |
| commit | 6e83dad6187123ea8251b11ed3b524a71e3b1e9c (patch) | |
| tree | e7efc3598cac39ec8c79f1e3f576d0b8e6a8003c | |
| parent | 961637d2c6921b4aec45d46014808abe7b1ac02f (diff) | |
able to read cue
| -rw-r--r-- | cue.py | 251 | ||||
| -rw-r--r-- | cueread.py | 61 |
2 files changed, 231 insertions, 81 deletions
@@ -1,7 +1,7 @@ +from cchardet import detect as encoding_detect +import codecs import sys - -def printf(fmt, *args): - sys.stdout.write(fmt % args) +import re class Track: def __init__(self, number, datatype): @@ -11,13 +11,13 @@ class Track: raise InvalidCommand("invalid number \"%s\"" % number) self.datatype = datatype - self.indexes = [] - - self.performer = "" - self.title = "" + self.indexes = {} + self.attrs = {} - def get_number(): - return self.number + def get(self, attr): + return self.attrs.get(attr, + None if attr in ("pregap", "postgap") else "" + ) class File: def __init__(self, name, filetype): @@ -28,20 +28,22 @@ class File: def __repr__(self): return self.name - def get_type(self): + def type(self): return self.filetype class Cue: def __init__(self): - self.performer = "" - self.title = "" + self.attrs = {} self.tracks = [] self.files = [] - def get_tracks(self): + def tracks(self): return self.tracks + def get(self, attr): + return self.attrs.get(attr, "") + class CueParserError(Exception): pass @@ -54,36 +56,75 @@ class InvalidCommand(CueParserError): class InvalidContext(CueParserError): pass -def check_argc(count): +class Context: + ( + GENERAL, + TRACK, + FILE + ) = range(3) + +def check(count = None, context = None): def deco(func): def method(cls, *lst): - n = len(lst) - if n != count: - raise InvalidCommand("%d args expected, got %d" % (count, n)) + 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: - ( - GENERAL, - TRACK - FILE, - ) = range(3) + re_timestamp = re.compile("^[\d]{1,3}:[\d]{1,2}:[\d]{1,2}$") + rem_commands = ('genre', 'data', '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 - def __init__(self, msg): self.cue = Cue() - self.context = self.GENERAL + self.context = Context.GENERAL + self.track = None + self.file = None + self.commands = { - "file": self.parse_file, - "track": self.parse_track, - "index": self.parse_index, - "title": self.parse_title, - "performer": self.parse_performer, - "flags": self.parse_flags - } + "file": self.parse_file, + "flags": self.parse_flags, + "index": self.parse_index, + "pregap": self.parse_pregap, + "rem": self.parse_rem, + "track": self.parse_track, - self.msg = msg + "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): @@ -116,75 +157,124 @@ class CueParser: 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(): - return cue + def get_cue(self): + return self.cue - @check_argc(2) + @check(2) def parse_file(self, *args): - self.last_file = File(*args) - self.cue.files.append(self.last_file) - self.context = self.FILE + self.file = File(*args) + self.cue.files.append(self.file) + self.context = Context.FILE - @check_argc(2) + @check(2, (Context.FILE, Context.TRACK)) def parse_track(self, *args): - if self.context != self.FILE: - raise InvalidContext + self.track = Track(*args) + self.cue.tracks.append(self.track) + self.file.tracks.append(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.last_track = Track(*args) - self.cue.tracks.append(self.last_track) - self.last_file.tracks.append(self.last_track) - self.context = self.TRACK + self.track.indexes[number] = self.parse_timestamp(time) - def set_prop(self, attr, value, opt): - obj = opt.get(self.context) - if obj is None: - raise InvalidContext + @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 getattr(obj, attr): + if attr in obj.attrs: raise InvalidCommand("duplicate") - setattr(obj, attr, value) - - @check_argc(1): - def parse_title(self, title): - self.set_prop("title", title, { - self.GENERAL: self.cue, - self.TRACK: self.last_track - }) + obj.attrs[attr] = value - @check_argc(1) - def parse_performer(self, performer): - self.set_prop("performer", performer, { - self.GENERAL: self.cue, - self.TRACK: self.last_track - }) - + @check(context = Context.TRACK) def parse_flags(self, *flags): - if self.context != self.TRACK: - raise InvalidContext - if self.last_track.indexes: + 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_default(self, args): + def parse_skip(self, *args): + pass + + def parse_default(self, *args): raise UnknownCommand def parse(self, cmd, arg): - self.commands.get(cmd.lower(), parse_default)(*self.split_args(arg)) + self.commands.get(cmd.lower(), self.parse_default)(*self.split_args(arg)) + +def __read_file(filename): + f = open(filename, "rb") + data = f.read() + f.close() + + 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) + + return encoded -def read_cuesheet(filename): - def msg(fmt, args): - printf("read_cuesheet %s:%d: ", filename, nline) - printf(fmt, args) +def read_cue(filename, 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 - if not fmt.endswith("\n"): - printf("\n") + cuefile = __read_file(filename) + parser = CueParser() - fp = open(filename) - parser = CueParser(msg) nline = 0 - for line in fp.readlines(): + for line in cuefile.split("\n"): nline = nline + 1 s = line.strip() if not len(s): @@ -192,7 +282,7 @@ def read_cuesheet(filename): data = s.split(None, 1) if len(data) is 1: - msg("arg missed") + msg("invalid command \"%s\": arg missed", data[0]) continue try: @@ -206,5 +296,4 @@ def read_cuesheet(filename): except CueParserError as err: msg("%s", err) - fp.close() return parser.get_cue() diff --git a/cueread.py b/cueread.py new file mode 100644 index 0000000..53d52d1 --- /dev/null +++ b/cueread.py @@ -0,0 +1,61 @@ +from os.path import basename +import sys + +from cue import read_cue + +if sys.version_info.major == 2: + class Encoded: + def __init__(self, stream): + self.stream = stream + + def write(self, msg): + self.stream.write(msg.encode("utf-8")) + + def __getattr__(self, attr): + return getattr(self.stream, attr) + + sys.stdout = Encoded(sys.stdout) + +def printf(fmt, *args): + sys.stdout.write(fmt % args) + +def msf(ts): + m = ts / (60 * 75) + s = ts / 75 % 60 + f = ts % 75 + + return "%d:%d:%d" % (m, s, f) + +progname = basename(sys.argv[0]) +if len(sys.argv) != 2: + printf("Usage: %s cuefile\n", progname) + sys.exit(1) + +try: + cue = read_cue(sys.argv[1], on_error = lambda err:\ + sys.stderr.write("** %s:%d: %s\n" % (progname, err.line, err)) + ) +except Exception as err: + printf("%s: read_cue failed: %s: %s\n", progname, err.__class__.__name__, err) + sys.exit(1) + +printf("Cue attributes:\n") +for key in sorted(cue.attrs.keys()): + printf("\t%s = %s\n", key, cue.attrs[key]) + +for file in cue.files: + printf("File \"%s\" %s\n", file, file.type()) + for track in file.tracks: + printf("\tTrack %d\n", track.number) + pregap = track.get("pregap") + postgap = track.get("postgap") + for key in sorted(track.attrs.keys()): + if key not in ("pregap", "postgap"): + printf("\t\t%s = %s\n", key, track.attrs[key]) + if pregap is not None: + printf("\t\tPregap %s\n", msf(pregap)) + for key in sorted(track.indexes.keys()): + printf("\t\tIndex %d %s\n", key, msf(track.indexes[key])) + if postgap is not None: + printf("\t\tPostgap %s\n", msf(postgap)) +sys.exit(0) |
