#!/bin/env python """ this script assumes a layout like the following, in the pwd of your employees /Videos /shows /????p /ShowName /[Season ?? ]Episode ?? [ - TITLE].mkv /movies /????p /[A-Z0-9@] /MovieName.mkv """ from sys import argv, stderr, exit from glob import iglob from itertools import chain import shlex import re import readline from dataclasses import dataclass from pathlib import Path from typing import Optional from os import system import socket RESOLUTIONS=[(1280, 720), (1366, 768), (1920, 1080)] #RESOLUTIONS=[(710, 480), (1136, 768)] argv0 = 'reenc.py' def usage(): global argv0 print(f'usage: {argv0} ', file=stderr) exit(-1) class JobError(Exception): pass def send_cmd(reader, writer, cmd): lines = [] writer.write(cmd.encode()+b'\n') writer.flush() while True: line = reader.readline() if line == '?\n': raise JobError(cmd) elif line == 'OKAY\n': return lines else: lines.append(line.strip()) @dataclass class Episode: file: Path subs: Optional[str] = None title: Optional[str] = None season: int = 0 number: int = 0 def outname(self): name = f'Episode {self.number:02}' if self.title is not None: name = f'{name} - {self.title}' if self.season != 0: name = f'Season {self.season:02} {name}' return f'{name}.mkv' def outpath(self, res, show): wid, hei = res return Path('Videos/shows') / f'{hei}p' / show / self.outname() def generate_script(self, show, res): wid, hei = res outpath = self.outpath(res, show) inparg = shlex.quote(str(self.file)) outarg = shlex.quote(str(outpath)) dirarg = shlex.quote(str(outpath.parent)) return '#!/bin/sh\n' f'mkdir -p {dirarg}\n' f'ffmpeg -loglevel error -i {inparg} -map 0 -c:a copy -c:s ass -c:v h264 -s {wid}x{hei} {outarg}\n' def slug(self): if self.season == 0: return f'E{self.number:02}' else: return f'S{self.season:02}E{self.number:02}' def find_common_ends(files): files = map(lambda x: re.sub('\d', '#', x), files) needle = next(files) pfxlen = len(needle) sfxlen = len(needle) for file in files: while needle[:pfxlen] != file[:pfxlen]: pfxlen -= 1 while needle[-sfxlen:] != file[-sfxlen:]: sfxlen -= 1 return needle[:pfxlen], needle[-sfxlen:] def find_sXXeXX(name): m = re.search('S([0-9]+)E([0-9]+)', name, re.I) if m is not None: season, episode = m.groups() return int(season), int(episode) season, episode = 0, 0 if (m := re.search('S([0-9]+)', name, re.I)) is not None: season = int(m.group(1)) name = name[:m.start()] + name[m.end():] elif (m := re.search('Season ([0-9]+)', name, re.I)) is not None: season = int(m.group(1)) name = name[:m.start()] + name[m.end():] if (m := re.search('E([0-9]+)', name, re.I)) is not None: episode = int(m.group(1)) elif (m := re.search('Episode ([0-9]+)', name, re.I)) is not None: episode = int(m.group(1)) elif (m := re.search('([0-9]+)', name, re.I)) is not None: episode = int(m.group(1)) return season, episode def dump_metafile(path, name, files): with open(path, 'wt') as fp: print(f'showname: {name}',file=fp) print(file=fp) pfx, sfx = find_common_ends(map(lambda x: x.name, files)) for file in files: season, episode = find_sXXeXX(file.name) title = file.name[len(pfx):-len(sfx)] print(f'file: {str(file)}', file=fp) print(f'season: {season}', file=fp) print(f'number: {episode}', file=fp) print(f'title: {title}', file=fp) print(file=fp) def load_metafile(path): with open(path, 'rt') as fp: lines = map(str.strip, filter(lambda x: not x.startswith('#'), fp.readlines())) showname = None for line in lines: if line == '': break if ':' not in line: continue k,v = line.split(':', 1) if k.strip() == 'showname': showname = v.strip() episodes = [] data = {} for line in lines: if line == '': if len(data) > 0: episodes.append(Episode(**data)) data = {} if ':' not in line: continue k,v = line.split(':', 1) try: v = int(v.strip()) except: v = v.strip() if v == '': continue data[k.strip()] = v if len(data) != 0: episodes.append(Episode(**data)) return showname, episodes def check_overlap(episodes): overmap = {} ret = False for episode in episodes: name = episode.outname() if name in overmap: overmap[name].append(episode) ret = True else: overmap[name] = [episode] for path, eps in overmap.items(): if len(eps) > 1: print(f'multiple files would be saved at {path}') for ep in eps: print(f'\t{ep.file}') return ret def do_connect(host, port): sok = socket.create_connection((host, port)) return sok.makefile('r'), sok.makefile('wb') def queue_job(reader, writer, show, res, episode): jobname = f'{show} {episode.slug()} @ {res}' script = episode.generate_script(show, res).encode() writer.write(f'NEW JOB {len(script)}\n'.encode()) writer.write(script) writer.flush() jid = reader.readline().strip() if jid == '?\n': print(f'couldn\'t queue {jobname}') return else: jid = int(jid) reader.readline() send_cmd(reader, writer, f'META name IS {jobname}') send_cmd(reader, writer, 'MARK AS available') print(f'{jobname} queued as {jid}') if __name__ == '__main__': if len(argv) > 0: argv0 = argv[0] argv = argv[1:] if len(argv) != 3: usage() host, port, dir = argv print(f'reencoding {dir}...') dir = Path(dir) metapath = Path(dir) / 'reenc.meta' if metapath.exists(): showname, episodes = load_metafile(metapath) if check_overlap(episodes): print("exiting.") exit(-2) reader, writer = do_connect(host, port) for episode in episodes: for res in RESOLUTIONS: if episode.outpath(res, showname).exists(): print(f'{episode.slug()} @ {res} already exists, skipping') queue_job(reader, writer, showname, res, episode) writer.write(b'QUIT\n') writer.flush() else: print(f'no metafile found, generating {metapath}...') mkglob = lambda x: iglob(f'**/*.{x}', root_dir=dir, recursive=True) globs = chain(mkglob('mkv'), mkglob('mp4'), mkglob('mov'), mkglob('avi')) files = [*map(lambda p: dir / p, globs)] files.sort() showname = input('enter the name of this show\n% ') dump_metafile(metapath, showname, files) system(f'xdg-open {shlex.quote(str(metapath))}')