from getopt import gnu_getopt as getopt, GetoptError from math import floor from os.path import basename, dirname, join, splitext from re import compile, Match, search from sys import argv, exit from typing import List, Optional try: from _rssbotlib import AVDict, version, VideoInfo have_rssbotlib = True except ImportError: AVDict = None have_rssbotlib = False RSSBOTLIB_NOTFOUND = '''rssbotlib not found. The source code is available at https://github.com/lifegpc/ffmpeg-study/tree/master/rssbotlib''' # noqa: E501 DUR_REG = compile(r'^(?P[\+-])?(((?P\d+):)?((?P\d+):))?(?P\d+)(\.(?P\d+))?$') # noqa: E501 LRCDUR_REG = compile(r'\[(((?P\d+):)?((?P\d+):))?(?P\d+)(\.(?P\d+))?\]') # noqa: E501 def convert_dur(r: Match) -> float: rd = r.groupdict() t = int(rd['sec']) if rd['ms']: t += int(rd['ms']) / (10 ** len(rd['ms'])) if rd['min']: t += int(rd['min']) * 60 if rd['h']: t += int(rd['h']) * 3600 if 'sign' in rd and rd['sign'] == '-': t = -t return t def prase_duration(s: str) -> float: r = search(DUR_REG, s) if r is None: raise ValueError(f'Can not parse duration "{s}"') return convert_dur(r) def generate_good_filename(meta: AVDict) -> Optional[str]: m = meta.to_dict() if 'title' in m and 'artist' in m: return f"{m['artist']} - {m['title']}.lrc" elif 'title' in m: return f"{m['title']}.lrc" class Lyric: def __init__(self) -> None: self._l = [] self._meta = [] self._bom = False self._has_dur = True def apply_offset(self): offset = None for m in self._meta: if m.startswith('offset:'): offset = int(m.lstrip('offset:')) self._meta.remove(m) break if offset is None: return for i in self._l: i['time'] = max(0, round((i['time'] * 1000 + offset) / 1000, 2)) def convert_translate_type(self, dur: Optional[float]): self._l.sort(key=lambda d: d['time']) re = [] ltmp = 0 tmp = None for i in self._l: if tmp is not None: re.append({'time': max(ltmp, round(i['time'] - 0.01, 2)), 'data': tmp}) tmp = None s: str = i['data'] if s.find(' / ') > 0: li = s.split(' / ', 1) ltmp = i['time'] tmp = li[1] re.append({'time': i['time'], 'data': li[0]}) else: re.append(i.copy()) if tmp is not None: if dur is not None: re.append({'time': max(round(dur, 2), round(ltmp + 0.01, 2)), 'data': tmp}) self._l = re def parse(self, fn: str): li = [] me = [] with open(fn, 'r', encoding='UTF-8') as f: t = f.read(1) if t == '\ufeff': self._bom = True else: f.seek(0, 0) ll = f.readlines(1) while len(ll) > 0: for i in ll: i = i.rstrip('\n') if self._has_dur: tre = LRCDUR_REG.finditer(i) re: List[Match] = [] for tmp in tre: re.append(tmp) if len(re) == 0: if i.startswith('[') and i.endswith(']'): me.append(i[1:-1]) else: if len(li) == 0: self._has_dur = False li.append(i) else: print(f'Ignored "{i}"') else: d = i[re[-1].end():] for r in re: li.append({'time': round(convert_dur(r), 2), 'data': d}) else: if i.startswith('[') and i.endswith(']'): me.append(i[1:-1]) else: li.append(i) ll = f.readlines(1) self._l = li self._meta = me def save(self, fn: str): with open(fn, 'w', encoding='UTF-8') as f: if self._bom: f.write('\ufeff') for m in self._meta: f.write(f"[{m}]\n") for i in self._l: if isinstance(i, str): f.write(f"{i}\n") else: t = round(i['time'] * 100) m = floor(t / 6000) s = floor((t % 6000) / 100) ms = t % 100 f.write(f"[{m:02}:{s:02}.{ms:02}]{i['data']}\n") class Cml: def __init__(self, arg: List[str]) -> None: self.output = None self.file = None self.verbose = False self.duration = None self.dir = None self.offset = False if len(arg) == 0: self.print_help() exit(0) try: r = getopt(arg, '-hVvo:f:t:d:', ['help', 'version', 'verbose', 'output=', 'file=', 'duration=', 'dir=', 'offset']) for i in r[0]: if i[0] == '-h' or i[0] == '--help': self.print_help() exit(0) elif i[0] == '-V' or i[0] == '--version': self.print_version() exit(0) elif i[0] == '-v' or i[0] == '--verbose': self.verbose = True elif i[0] == '-o' or i[0] == '--output': self.output = i[1] elif i[0] == '-f' or i[0] == '--file': self.file = i[1] elif i[0] == '-t' or i[0] == '--duration': self.duration = prase_duration(i[1]) elif i[0] == '-d' or i[0] == '--dir': self.dir = i[1] elif i[0] == '--offset': self.offset = True if len(r[1]) == 0: raise GetoptError('Input lyric file is needed.') if len(r[1]) > 1: raise GetoptError('Too much input lyric file.') self.input = r[1][0] except GetoptError as e: print(e.msg) exit(1) def print_help(self): print('''convert_lrc.py [options] Convert translated lryics. Options: -h, --help Print this help message. -V, --version Print version. -v, --verbose Enable verbose logging. -o, --output Specify output path. -f, --file Specify music file, will read duration and other information from file. (rssbotlib is needed.) -t, --duration