diff create.py @ 136:da4f7200665f default tip

buncha shit
author Paper <paper@tflc.us>
date Sat, 07 Mar 2026 18:04:10 -0500
parents 0d8eabdd12ab
children
line wrap: on
line diff
--- a/create.py	Sat Jan 24 15:10:05 2026 -0500
+++ b/create.py	Sat Mar 07 18:04:10 2026 -0500
@@ -6,6 +6,9 @@
 import argparse
 import json
 import subprocess
+import os
+import math #ceil
+import typing
 
 def build_vf(blur) -> list[str]:
 	if blur:
@@ -14,82 +17,84 @@
 	return ["-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"]
 
 # Build filter string
-def build_filter_string(length: int, cover: bool) -> str:
-    f = ""
-    i = 1 if cover else 0
-    for x in range(length):
-        f += ("[%d:0]" % (x + i))
-    f += ("concat=n=%d:v=0:a=1[out]" % length)
-    return f
+def build_filter_string(length: int, offset: int) -> str:
+	f = ""
+	i = offset
+	for x in range(length):
+		f += ("[%d:0]" % (x + i))
+	f += ("concat=n=%d:v=0:a=1[out]" % length)
+	return f
 
-def build_arguments_list(audio_files: list[str], cover, output, blur) -> list:
-    args = ["ffmpeg", "-y"]
-    if not cover is None:
-        args.extend(["-r", "1", "-loop", "1", "-i", cover])
-    # I want you to play that song again
-    for f in audio_files:
-        args.extend(["-i", f])
-    args.extend(["-filter_complex", build_filter_string(len(audio_files), False if cover is None else True), "-map", "[out]"])
-    if not cover is None:
-        # Letterbox to 1920x1080
-        args.extend(["-map", "0:v", "-tune", "stillimage", "-c:v", "libx264"])
-        args.extend(build_vf(blur))
-    args.extend(["-shortest"])
-    args.append(output + ".mkv")
-    return args
+def build_arguments_list(audio_files: list[str], cover: typing.Optional[str], output: str, blur: bool, duration: float) -> list:
+	args = ["ffmpeg", "-y"]
+	# I want you to play that song again
+	if not cover is None:
+		args.extend(["-r", "1", "-loop", "1", "-i", cover])
+	for f in audio_files:
+		args.extend(["-i", f])
+	args.extend(["-filter_complex", build_filter_string(len(audio_files), 1 if cover else 0), "-map", "[out]"])
+	if not cover is None:
+		# Letterbox to 1920x1080
+		args.extend(["-map", "0:v",
+		             "-tune", "stillimage",
+		             "-c:v", "libx264",
+		             # Add a second to account for inconsistencies in durations
+		             "-t", "%f" % (duration + 1.0)])
+		args.extend(build_vf(blur))
+	args.append(output + ".mkv")
+	return args
 
 
 # Returns a Popen object that should be waited on
-def run_ffmpeg(audio_files: list[str], cover, output, blur) -> subprocess.Popen:
-    args = build_arguments_list(audio_files, cover, output, blur)
-    print(args)
-    return subprocess.Popen(args)
+def run_ffmpeg(audio_files: list[str], cover: typing.Optional[str], output: str, blur: bool, duration: float) -> subprocess.Popen:
+	args = build_arguments_list(audio_files, cover, output, blur, duration)
+	print(args)
+	return subprocess.Popen(args)
 
 
 # Returns a Popen object that should be waited on.
 # stdout is also piped...
 def run_ffprobe(audio_file: str) -> subprocess.Popen:
-    return subprocess.Popen(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", audio_file], stdout=subprocess.PIPE, text=True)
+	return subprocess.Popen(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", audio_file], stdout=subprocess.PIPE, text=True)
 
 
-def get_title_from_tags(j, default: str) -> str:
-    # Is there an easier way to do this...?
-    for tag in j:
-        if tag.lower() == "title":
-            return j[tag]
+def get_title_from_tags(j: list, default: str) -> str:
+	# Is there an easier way to do this...?
+	for tag in j:
+		if tag.lower() == "title":
+			return j[tag]
 
-    # Ugh
-    return default
+	# Ugh
+	return default
 
 def doit(audio_files: list[str], cover, output, blur) -> int:
-    ffprobes = [run_ffprobe(x) for x in audio_files]
-    ffmpeg = run_ffmpeg(audio_files, cover, output, blur)
+	dur = 0.0
 
-    # Wait for all ffprobe processes  to finish their jobs
-    for f in ffprobes:
-        f.wait()
+	ffprobes = [run_ffprobe(x) for x in audio_files]
 
-    # Iterate the list again, and add up all the times.
-    # This also accounts for milliseconds etc. to be
-    # more accurate
-    with open(output + ".txt", "w", encoding="utf-8") as fw:
-        dur = 0.0
-        for f in ffprobes:
-            j = json.load(f.stdout)
-            fw.write("%d:%02d:%02d %s\n" % (int(dur) // 3600, int(dur) % 3600 // 60, int(dur) % 60, get_title_from_tags(j["format"]["tags"], "OOPS....")))
-            dur += float(j["format"]["duration"])
+	# Create output txt file with timestamps; we also use this duration
+	# to force ffmpeg to do the right thing instead of elongating our
+	# video
+	with open(output + ".txt", "w", encoding="utf-8") as fw:
+		for f in ffprobes:
+			f.wait()
+			j = json.load(f.stdout)
+			fw.write("%d:%02d:%02d %s\n" % (int(dur) // 3600, int(dur) % 3600 // 60, int(dur) % 60, get_title_from_tags(j["format"]["tags"], "OOPS....")))
+			dur += float(j["format"]["duration"])
 
-    # Finally, wait on the big ffmpeg process
-    ffmpeg.wait()
+	ffmpeg = run_ffmpeg(audio_files, cover, output, blur, dur)
+
+	# Finally, wait on the big ffmpeg process
+	ffmpeg.wait()
 
 def main() -> int:
-    parser = argparse.ArgumentParser(prog='create', description='Creates album videos', epilog='(c) paper 2025 all rights reversed')
-    parser.add_argument('-c', '--cover', required=True)
-    parser.add_argument('-o', '--output', required=True)
-    parser.add_argument('-b', '--blur', action='store_true')
-    parser.add_argument('audio', action='extend', nargs='+', type=str)
-    args = parser.parse_args()
-    doit(args.audio, args.cover, args.output, args.blur)
+	parser = argparse.ArgumentParser(prog='create', description='Creates album videos', epilog='(c) paper 2025 all rights reversed')
+	parser.add_argument('-c', '--cover', required=True)
+	parser.add_argument('-o', '--output', required=True)
+	parser.add_argument('-b', '--blur', action='store_true')
+	parser.add_argument('audio', action='extend', nargs='+', type=str)
+	args = parser.parse_args()
+	doit(args.audio, args.cover, args.output, args.blur)
 
 if __name__ == "__main__":
-    exit(main())
+	exit(main())