@@ 0,0 1,259 @@
+#!/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)]
+
+argv0 = 'reenc.py'
+
+def usage():
+ global argv0
+ print(f'usage: {argv0} <host> <port> <srcdir>', file=stderr)
+ exit(-1)
+
+
+@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
+ inparg = shlex.quote(str(self.file))
+ outarg = shlex.quote(str(self.outpath(res, show)))
+ return '#!/bin/sh\n' f'mkdir -p `dirname {outarg}`\n' f'ffmpeg -loglevel quiet -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 = iter(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))
+ elif (m := re.search('Season ([0-9]+)', name, re.I)) is not None:
+ season = int(m.group(1))
+
+ 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()
+
+ 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()
+
+ writer.write(f'META name IS {jobname}\n'.encode())
+ writer.flush()
+ reader.readline()
+
+ writer.write(f'MARK AS available\n'.encode())
+ writer.flush()
+ reader.readline()
+
+ 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)]
+
+ showname = input('enter the name of this show\n% ')
+
+ dump_metafile(metapath, showname, files)
+ system(f'xdg-open {shlex.quote(str(metapath))}')
+