258 lines
8.1 KiB
Python
Executable File
258 lines
8.1 KiB
Python
Executable File
#!/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)
|