Compare commits
2 Commits
2013d2e61d
...
b01bc0145b
| Author | SHA1 | Date | |
|---|---|---|---|
| b01bc0145b | |||
| ef7965d274 |
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python Debugger: Current File with Arguments",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${file}",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"args": "${command:pickArgs}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Executable → Regular
+73
-54
@@ -46,70 +46,79 @@ def get_duration(file_path: Path):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def read_progress(ostream_line, dur, pbar):
|
def set_progress(line: str, dur, pbar):
|
||||||
if not ostream_line.startswith("out_time_ms="):
|
if not line.startswith("out_time_ms="):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
us = int(ostream_line.split('=')[1])
|
us = int(line.split('=')[1])
|
||||||
if us < 0:
|
if us < 0:
|
||||||
return
|
return
|
||||||
secs = us / 1_000_000
|
current_sec = us / 1_000_000
|
||||||
if secs > dur * 1.1: # Allow 10% overshoot
|
pbar.n = min(current_sec, dur)
|
||||||
pbar.n = dur
|
|
||||||
else:
|
|
||||||
pbar.n = secs
|
|
||||||
pbar.refresh()
|
pbar.refresh()
|
||||||
except (ValueError, OverflowError):
|
except (ValueError, IndexError, OverflowError):
|
||||||
pass
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def start_ffmpeg(infile: Path, outfile: Path):
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y", "-i", str(infile),
|
||||||
|
"-progress", "pipe:1", "-nostats", "-loglevel", "error",
|
||||||
|
*COMPRESS_OPTIONS, str(outfile)
|
||||||
|
]
|
||||||
|
return subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
bufsize=1,
|
||||||
|
text=True,
|
||||||
|
encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def clean_on_failure(proc: subprocess.Popen, outfile: Path):
|
||||||
|
if proc.poll() is None:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
if outfile.exists():
|
||||||
|
try:
|
||||||
|
outfile.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def hevc_encode(infile: Path, outfile: Path, progress: bool = True):
|
def hevc_encode(infile: Path, outfile: Path, progress: bool = True):
|
||||||
# too heavy bro.
|
|
||||||
dur = get_duration(infile)
|
dur = get_duration(infile)
|
||||||
cmd = [
|
|
||||||
"ffmpeg", "-y", "-i", str(infile),
|
|
||||||
"-progress", "pipe:1", "-nostats",
|
|
||||||
*COMPRESS_OPTIONS, str(outfile)
|
|
||||||
]
|
|
||||||
logger.debug(f"Duration: {dur:.2f} seconds")
|
logger.debug(f"Duration: {dur:.2f} seconds")
|
||||||
logger.debug(f"Command: {' '.join(cmd)}")
|
proc = start_ffmpeg(infile, outfile)
|
||||||
proc = subprocess.Popen(
|
logger.debug(f"Command: {proc.args}")
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
||||||
text=True, encoding='utf-8', bufsize=1)
|
pbar = get_progress_bar(
|
||||||
progress = progress and is_tty()
|
|
||||||
pbar = None if not progress else get_progress_bar(
|
|
||||||
total=dur, unit='s',
|
total=dur, unit='s',
|
||||||
bar_format='{l_bar}{bar}| {n:.2f}/{total:.2f}({unit})')
|
bar_format='{l_bar}{bar}| {n:.2f}/{total:.2f}({unit})'
|
||||||
|
) if progress and is_tty() else None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
for line in iter(proc.stdout.readline, ''):
|
||||||
line = proc.stdout.readline()
|
set_progress(line.strip(), dur, pbar)
|
||||||
if not line and proc.poll() is not None:
|
if proc.poll() is not None:
|
||||||
break
|
break
|
||||||
if not pbar:
|
|
||||||
continue
|
|
||||||
read_progress(line.strip(), dur, pbar)
|
|
||||||
proc.wait()
|
proc.wait()
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
raise subprocess.CalledProcessError(proc.returncode, cmd)
|
raise subprocess.CalledProcessError(proc.returncode, "ffmpeg")
|
||||||
except (
|
except (
|
||||||
KeyboardInterrupt,
|
KeyboardInterrupt,
|
||||||
subprocess.CalledProcessError
|
subprocess.CalledProcessError
|
||||||
) as e:
|
) as e:
|
||||||
if proc.poll() is None:
|
|
||||||
proc.kill()
|
|
||||||
proc.wait()
|
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Encoding failed with code {proc.returncode}:"
|
f"Encoding failed with code {proc.returncode}:"
|
||||||
f" {e.__class__.__name__}")
|
f" {e.__class__.__name__}")
|
||||||
if outfile.exists():
|
clean_on_failure(proc, outfile)
|
||||||
try:
|
|
||||||
outfile.unlink()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
if pbar:
|
if pbar:
|
||||||
pbar.close()
|
pbar.close()
|
||||||
|
proc.stdout.close()
|
||||||
|
|
||||||
|
|
||||||
def build_file_list(srcdir, extensions):
|
def build_file_list(srcdir, extensions):
|
||||||
@@ -131,6 +140,23 @@ def make_output_path(infile: Path, outdir: Path = None, relroot: str = None):
|
|||||||
return outdir / rel_path
|
return outdir / rel_path
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_output(infile: Path, outfile: Path, rm_original: bool = False):
|
||||||
|
final_out = outfile.with_name(
|
||||||
|
outfile.name.replace(".hevc.mp4", ".mp4"))
|
||||||
|
is_path_conflict = final_out.resolve() == infile.resolve()
|
||||||
|
if is_path_conflict:
|
||||||
|
logger.warning(
|
||||||
|
f"Filename conflict!\n i: {infile}\n o: {final_out}")
|
||||||
|
if not rm_original:
|
||||||
|
logger.debug("Renaming original to avoid conflict.")
|
||||||
|
infile.rename(infile.with_stem(infile.stem + '.bak'))
|
||||||
|
logger.debug(f"Output file: {final_out}")
|
||||||
|
outfile.replace(final_out)
|
||||||
|
if rm_original and not is_path_conflict:
|
||||||
|
logger.debug(f"Remove original: {infile}")
|
||||||
|
infile.unlink()
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog=itsme,
|
prog=itsme,
|
||||||
@@ -176,7 +202,8 @@ def main():
|
|||||||
errmsg = 'No input files provided'
|
errmsg = 'No input files provided'
|
||||||
if not files:
|
if not files:
|
||||||
raise SystemExit(errmsg)
|
raise SystemExit(errmsg)
|
||||||
files.sort(key=lambda x: x.stat().st_size)
|
if len(files) > 1:
|
||||||
|
files.sort(key=lambda x: x.stat().st_size)
|
||||||
|
|
||||||
for idx, infile in enumerate(files, 1):
|
for idx, infile in enumerate(files, 1):
|
||||||
logger.info(f"[{idx}/{len(files)}] {infile.name}")
|
logger.info(f"[{idx}/{len(files)}] {infile.name}")
|
||||||
@@ -189,21 +216,13 @@ def main():
|
|||||||
except (OSError, PermissionError) as e:
|
except (OSError, PermissionError) as e:
|
||||||
logger.error(f"Job failed: {e}")
|
logger.error(f"Job failed: {e}")
|
||||||
raise
|
raise
|
||||||
final_out = outfile.with_name(
|
|
||||||
outfile.name.replace(".hevc.mp4", ".mp4"))
|
isize = infile.stat().st_size / (1024 * 1024)
|
||||||
if final_out.resolve() == infile.resolve():
|
osize = outfile.stat().st_size / (1024 * 1024)
|
||||||
logger.warning(
|
logger.info(f"Size deductions: {osize - isize:+.2f} MB")
|
||||||
f"Filename conflict!\n i: {infile}\n o: {final_out}")
|
logger.debug(f" i: {isize:.2f} MB -> o: {osize:.2f} MB")
|
||||||
if not args.remove_original:
|
|
||||||
logger.debug("renaming original to avoid conflict.")
|
resolve_output(infile, outfile, args.remove_original)
|
||||||
infile.rename(infile.with_stem(infile.stem + '.bak'))
|
|
||||||
outfile.replace(final_out)
|
|
||||||
else:
|
|
||||||
outfile.replace(final_out)
|
|
||||||
if args.remove_original:
|
|
||||||
logger.debug(f"Remove original: {infile}")
|
|
||||||
infile.unlink()
|
|
||||||
logger.debug(f"Output file: {final_out}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user