summaryrefslogtreecommitdiff
path: root/cue.py
diff options
context:
space:
mode:
Diffstat (limited to 'cue.py')
-rw-r--r--cue.py359
1 files changed, 0 insertions, 359 deletions
diff --git a/cue.py b/cue.py
deleted file mode 100644
index d2fb958..0000000
--- a/cue.py
+++ /dev/null
@@ -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()