From 2a9972922ff79de7acdf38cb21e2ac66df6c7ea7 Mon Sep 17 00:00:00 2001 From: Aleteoryx Date: Tue, 23 Dec 2025 22:05:14 -0500 Subject: [PATCH] queuers --- queuers/employ.sh | 18 ++++ queuers/reenc.py | 259 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100755 queuers/employ.sh create mode 100644 queuers/reenc.py diff --git a/queuers/employ.sh b/queuers/employ.sh new file mode 100755 index 0000000000000000000000000000000000000000..7d00581789514b56871e20dee4b315ba191f838f --- /dev/null +++ b/queuers/employ.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +if [ $# \!= 3 ]; then + 1>&2 echo 'usage: employ.sh ' + exit 1 +fi + +host="$1" +port="$2" +file="$3" + +{ + echo -n 'NEW JOB ' + stat -c %s "$file" + cat "$file" + echo 'MARK AS available' + echo QUIT +} | nc "$host" "$port" diff --git a/queuers/reenc.py b/queuers/reenc.py new file mode 100644 index 0000000000000000000000000000000000000000..b630bfd02019ae22b8c9d60b351a24461d4b7958 --- /dev/null +++ b/queuers/reenc.py @@ -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} ', 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))}') +