コード例 #1
0
class Song(object):
    def __init__(self, filename=None, ignore_steps=False):
        parsers = {
            "Meta": self.parse_meta,
            "Song": self.parse_song,
            "Timing": self.parse_timing,
            "Formats": self.parse_formats,
            "Styles": self.parse_styles,
            "Variants": self.parse_variants,
            "Lyrics": self.parse_lyrics,
        }
        self.ignore_steps = ignore_steps
        self.pathbase = os.path.dirname(filename) if filename else None
        section = None
        lines = []
        self.meta = None
        self.song = None
        self.timing = None
        self.formats = None
        self.styles = None
        self.variants = None
        self.compounds = None

        self.fake_time = 0
        self.line = 0
        self.section_line = 0

        if not filename:
            self.meta = OrderedDict()
            self.song = OrderedDict()
            self.timing = BeatCounter()
            self.formats = OrderedDict()
            self.styles = OrderedDict()
            self.variants = OrderedDict()
            self.compounds = []
            return

        for line in codecs.open(filename, encoding='utf-8', mode='r'):
            self.line += 1
            line = line.replace("\n","").replace("\r","")
            if line.startswith("#"):
                continue
            if section is None:
                if not line:
                    continue
                if line[0] != "[" or line[-1] != "]":
                    raise ParseError("Expected section header")
                section = line[1:-1]
                self.section_line = self.line
                continue
            if line and line[0] == "[" and line[-1] == "]":
                if section not in parsers:
                    raise ParseError("Unknown section %s" % section)
                parsers[section](lines)
                lines = []
                section = line[1:-1]
                self.section_line = self.line
            else:
                lines.append(line)
        if section is not None:
            parsers[section](lines)

        if self.variants and self.styles:
            for variant in self.variants.values():
                variant.load_tags(self.styles)

    def keyvalues(self, lines):
        for line in lines:
            if not line:
                continue
            if "=" not in line:
                raise ParseError("Expected key=value format (%s)" % line)
            key, val = line.split("=", 1)
            yield key, val

    def parse_meta(self, lines):
        self.meta = OrderedDict()
        for key, val in self.keyvalues(lines):
            lang = None
            m = re.match(r"(.*)\[(.*)\]", key)
            if m:
                key = m.group(1)
                lang = m.group(2)
            if key not in self.meta:
                self.meta[key] = MultiString()
            self.meta[key][lang] = val

    def parse_song(self, lines):
        self.song = OrderedDict()
        for key, val in self.keyvalues(lines):
            self.song[key] = val

    def parse_timing(self, lines):
        self.timing = BeatCounter()
        for time, beat in self.keyvalues(lines):
            if time[0] != "@":
                raise ParseError("Expected @time")
            self.timing.add(float(time[1:]), int(beat))

    def parse_formats(self, lines):
        self.formats = OrderedDict()
        for tag, fmt in self.keyvalues(lines):
            if fmt not in FORMATS:
                raise ParseError("Unknown format %s" % fmt)
            self.formats[tag] = FORMATS[fmt]

    def parse_twolevel(self, lines):
        inner = None
        for line in lines:
            if not line:
                continue
            if line[0] == "{":
                if line[-1] != "}":
                    raise ParseError("Expected {...} (%s)" % line)
                if inner is not None:
                    yield name, inner
                name = line[1:-1]
                inner = OrderedDict()
                continue
            if inner is None:
                raise ParseError("Expected {...} (%s)" % line)
            if "=" not in line:
                raise ParseError("Expected key=value format (%s)" % line)
            key, val = line.split("=", 1)
            inner[key] = val
        if inner is not None:
            yield name, inner

    def parse_styles(self, lines):
        self.styles = OrderedDict()
        for name, data in self.parse_twolevel(lines):
            self.styles[name] = Style(data)

    def parse_variants(self, lines):
        self.variants = OrderedDict()
        for name, data in self.parse_twolevel(lines):
            self.variants[name] = Variant(data)

    def parse_lyrics(self, lines):
        self.compounds = []
        compounds = None
        compound = None
        lineno = self.section_line
        for s in (lines + [""]):
            lineno += 1
            if not s and compound:
                first = compound[compound.keys()[0]]
                for key,val in compound.items():
                    if first.steps != val.steps:
                        raise ParseError("%d: Duration mismatch: %d!=%d (%s) (%s)" % (lineno, first.steps, val.steps, unicode(first), unicode(val)))
                if compound.timing is not None:
                    if compound.steps != len(compound.timing):
                        raise ParseError("%d: Timing line length mismatch: %d!=%d" % (lineno, len(compound.timing), compound.steps))
                else:
                    compound.start = self.fake_time
                    self.fake_time += compound.steps
                compound = None

            if not s:
                continue
            if compound is None:
                compound = Compound(self.timing)
                self.compounds.append(compound)

            if ":" not in s:
                raise ParseError("Expected 'X: value': %r" % s)

            tag, text = s.split(":", 1)
            tag = tag.strip()
            text = text.strip()

            if tag == "@":
                if self.ignore_steps:
                    continue
                timing = map(parse_time, text.split())
                compound.start = timing[0]
                compound.timing = timing[1:]
                if compound:
                    first = compound[compound.keys()[0]]
                    if first.steps != len(compound.timing):
                        raise ParseError("%d: Timing line length mismatch: %d!=%d" % (lineno, len(compound.timing), compound.steps))
                continue

            if tag not in self.formats:
                raise ParseError("Undefined format %r" % tag)

            compound[tag] = self.formats[tag](text)

    def dump(self):
        s = ""
        if self.meta is not None:
            s += "[Meta]\n"
            for tag, value in self.meta.items():
                for lang, text in value.items():
                    if lang is None:
                        s += u"%s=%s\n" % (tag, text)
                    else:
                        s += u"%s[%s]=%s\n" % (tag, lang, text)
            s += "\n"
        if self.song is not None:
            s += "[Song]\n"
            for tag, value in self.song.items():
                s += "%s=%s\n" % (tag, value)
            s += "\n"
        if self.timing is not None:
            s += "[Timing]\n"
            for time, beat in self.timing.beats:
                s += "@%f=%d\n" % (time, beat)
            s += "\n"
        if self.formats is not None:
            s += "[Formats]\n"
            for tag, fmt in self.formats.items():
                s += "%s=%s\n" % (tag, I_FORMATS[fmt])
            s += "\n"
        if self.styles is not None:
            s += "[Styles]\n"
            for name, style in self.styles.items():
                s += "{%s}\n" % name
                for key, value in style.data.items():
                    s += "%s=%s\n" % (key, value)
                s += "\n"
        if self.variants is not None:
            s += "[Variants]\n"
            for name, variant in self.variants.items():
                s += "{%s}\n" % name
                for key, value in variant.data.items():
                    s += u"%s=%s\n" % (key, value)
                s += "\n"
        if self.compounds is not None:
            s += "[Lyrics]\n\n"
            for compound in self.compounds:
                for tag, molecule in compound.items():
                    s += u"%s: %s\n" % (tag, molecule.source)
                if compound.timing is not None:
                    s += u"@: %s  %s\n" % (str(compound.start), ' '.join(map(str,compound.timing)))
                s += "\n"
                if any(i.break_before or i.break_after for i in compound.values()):
                    s += "\n"
        return s

    def save(self, filename):
        fd = open(filename, "w")
        fd.write(self.dump().encode("utf-8"))
        fd.close()

    @property
    def audiofile(self):
        return os.path.join(self.pathbase, self.song["audio"])

    @property
    def videofile(self):
        if "video" not in self.song:
            return None
        else:
            return os.path.join(self.pathbase, self.song["video"])

    @property
    def coverfile(self):
        if "cover" not in self.song:
            return None
        else:
            return os.path.join(self.pathbase, self.song["cover"])

    @property
    def aspect(self):
        if "aspect" not in self.song:
            return None
        else:
            return fractions.Fraction(self.song["aspect"])

    def get_lyric_snippet(self, variant_id, length=100):
        variant = self.variants[variant_id]
        tags = set(i for i in variant.tag_list if variant.tags[i].edge == TagInfo.BOTTOM)
        lyrics = ""
        broke = False
        for compound in self.compounds:
            if len(lyrics) >= length:
                break
            for tag, molecule in compound.items():
                if tag not in tags:
                    continue
                if molecule.break_before and not broke:
                    lyrics += u"/" + molecule.SPACE
                broke = False
                lyrics += molecule.text + molecule.SPACE
                if molecule.break_after:
                    lyrics += u"/" + molecule.SPACE
                    broke = True

        return lyrics

    def get_font_path(self, font):
        song_font = os.path.join(self.pathbase, font)
        if os.path.exists(song_font):
            return song_font
        cwd_font = font
        if os.path.exists(cwd_font):
            return cwd_font
        raise IOError("Font %s not found" % font)