#!/usr/bin/env python3 # -*- coding: utf-8 -*- # overly complicated flac reencoder originally written for the big touhou music torrent on nyaa.si # contains example of process calling and multithreading # not recommended to use (probably destructive) import os, re, time import subprocess import wave from queue import Queue from threading import Thread f = open("errlog", "w", encoding="UTF-8") def remove_if_exists(filename): if os.path.exists(filename): os.remove(filename) def opus_enc(queue, split_track_filename, track, quality=140.0): subprocess.call([ 'opusenc', '--vbr', '--bitrate', str(quality), #'--comp', 10, #default #'--framesize', '60', # default 20 '--artist', track.performer, '--comment', 'tracknumber={}'.format(track.index), '--title', track.title, '--date', track.cd_date, '--genre', track.cd_genre, '--album', track.cd_title, split_track_filename, '{}.opus'.format(os.path.splitext(split_track_filename)[0]), ]) queue.get() class Track(): def __init__(self, track_index, filename, parent): for member in ('cd_performer', 'cd_title', 'cd_date', 'cd_genre'): setattr(self, member, getattr(parent, member)) self.filename = filename self.filepath = filename[:filename.rfind('\\')+1] self.title = '' self.index = track_index self.performer = self.cd_performer self.time = { 1:0.0 } def __str__(self): return "{} - {} - {}".format(self.index, self.title, self.time) class CueSheet(): def __init__(self, filename): self.filename = filename self.filepath = filename[:filename.rfind('\\')+1] self.cd_performer = '' self.cd_title = '' self.cd_genre = '' self.cd_date = '' self.current_file = '' self.tracks = [] self.regex_lst = ( (re.compile(r'PERFORMER\s(.+)'), self.__performer), (re.compile(r'REM DATE\s(.+)'), self.__date), (re.compile(r'REM GENRE\s(.+)'), self.__genre), (re.compile(r'TITLE\s(.+)'), self.__title), (re.compile(r'FILE\s(.+)\sWAVE'), self.__file), (re.compile(r'TRACK\s(\d{2})\sAUDIO'), self.__track), (re.compile(r'INDEX\s(\d{2})\s(\d{1,3}:\d{2}:\d{2})'), self.__index), ) def __str__(self): value = "Title: {}\nPerformer: {}\nGenre: {}\nDate: {}\n".format(self.cd_title, self.cd_performer, self.cd_genre, self.cd_date) for track in self.tracks: value += ' ' + str(track) + '\n' return value def read(self): with open(self.filename, 'r', encoding='utf-8-sig') as f: for line in f: for regex, handler in self.regex_lst: mobj = regex.match(line.strip()) if mobj: handler(*self.unquote(mobj.groups())) def split(self): encoding_queue = multiprocessing.Queue(multiprocessing.cpu_count()) cds = set() tracks = set() for i, track in enumerate(self.tracks): # FATAL: sheet is not for .tta file if track.filename[-4:] != '.tta': f.write("\nFilename isn't .tta ({}):\n{}\n".format(track.filename, str(self))) return track_path = track.filepath + ' - '.join((track.index, track.title)).replace('?', '').replace('\\', '').replace('\\', '').replace(':', '') track_opus = track_path + '.opus' track_wav = track_path + '.wav' if os.path.exists(track_opus): f.write("File already exists, continuing... ({})".format(track_opus)) remove_if_exists(track_wav) continue cd_wav = track.filename[:-4] + '.wav' # decode .tta if needed if not os.path.exists(cd_wav): # FATAL: no file to decode if not os.path.exists(track.filename): f.write("\nFile doesn't exist ({}):\n{}\n".format(track.filename, str(self))) return result = subprocess.call([ 'tta', #'ttaenc', '-d', track.filename, #'-o', cd_wav ]) # FATAL: .tta decode failed if result != 0: f.write("Failed to decode .tta ({}):\n{}\n\n".format(track.index, str(self))) return # remove .tta remove_if_exists(track.filename) # split .wav into track if not os.path.exists(track_wav): wafi = wave.open(cd_wav, 'rb') param_names = ('nchannels', 'sampwidth', 'framerate', 'nframes', 'comptype', 'compname') params = wafi.getparams() param_dict = dict(zip(param_names, params)) start = int(param_dict['framerate'] * track.time[1]) stop = param_dict['nframes'] if len(sheet.tracks) > i+1 and sheet.tracks[i+1].filename == track.filename: stop = int(param_dict['framerate'] * sheet.tracks[i+1].time.get(0, sheet.tracks[i+1].time[1])) wafi_write = wave.open(track_wav, 'wb') newparams = list(params) newparams[3] = 0 wafi_write.setparams( tuple(newparams) ) wafi.setpos(start) wafi_write.writeframes(wafi.readframes(stop-start)) wafi_write.close() wafi.close() encoding_queue.put(track_wav) p = multiprocessing.Process( target=opus_enc, args=( encoding_queue, track_wav, track ) ) p.start() if cd_wav not in cds: cds.add(cd_wav) tracks.add(track_wav) while not encoding_queue.empty(): time.sleep(0.2) for cd in cds: remove_if_exists(cd) for track in tracks: remove_if_exists(track) remove_if_exists(self.filename) print(self.filename, "done!") def __performer(self, s): if not self.tracks: self.cd_performer = s else: self.tracks[-1].performer = s def __title(self, s): if not self.tracks: self.cd_title = s else: self.tracks[-1].title = s def __genre(self, s): self.cd_genre = s def __date(self, s): self.cd_date = s def __file(self, s): self.current_file = s def __track(self, s): self.tracks.append( Track(s, self.filepath + self.current_file, self) ) def __index(self, idx, s): idx = int(idx) self.tracks[-1].time[idx] = self.index_split(s) @staticmethod def index_split(s): t = s.split(':') return float(t[0])*60 + float(t[1]) + float(t[2]) / 75.0 @staticmethod def dqstrip(s): if s[0] == '"' and s[-1] == '"': return s[1:-1] return s @staticmethod def unquote(t): return tuple([CueSheet.dqstrip(s.strip()) for s in t]) class SplitterWorker(Thread): def __init__(self, queue, filename): Thread.__init__(self) self.queue = queue self.filename = filename def run(self): sheet = CueSheet(self.filename) sheet.read() sheet.split() if __name__ == '__main__': queue = Queue() for root, dirs, files in os.walk('.'): for name in files: if name[-4:].lower() == '.cue': worker = SplitterWorker(queue, root + '\\' + name) worker.daemon = True worker.start() if os.path.exists('./stop'): exit(1)