Mercurial > codedump
comparison create.py @ 133:0d8eabdd12ab default tip
create: write H:MM:SS timestamps, add option to fill with gaussian-blur instead of black
many albums are longer than one hour so writing H:MM:SS is a
necessity. if anything there will just be verbose info that
isn't important for my use-case.
however the gaussian-blur is simply broken. It works, and it plays
locally just fine, but YouTube in particular elongates the video
to fit the full width. I'm not entirely sure why it does this, but
it makes it useless and ugly.
| author | Paper <paper@tflc.us> |
|---|---|
| date | Sat, 03 Jan 2026 20:25:38 -0500 |
| parents | 71df0cf3aa05 |
| children |
comparison
equal
deleted
inserted
replaced
| 132:71df0cf3aa05 | 133:0d8eabdd12ab |
|---|---|
| 4 # track numbers etc. | 4 # track numbers etc. |
| 5 | 5 |
| 6 import argparse | 6 import argparse |
| 7 import json | 7 import json |
| 8 import subprocess | 8 import subprocess |
| 9 | |
| 10 def build_vf(blur) -> list[str]: | |
| 11 if blur: | |
| 12 return ["-filter_complex", "[0:v]split=2[bg][fg];[bg]scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,boxblur=luma_radius=10:chroma_radius=10[bg_final];[fg]scale=1920:1080:force_original_aspect_ratio=decrease[fg_final];[bg_final][fg_final]overlay=(W-w)/2:(H-h)/2"] | |
| 13 | |
| 14 return ["-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"] | |
| 9 | 15 |
| 10 # Build filter string | 16 # Build filter string |
| 11 def build_filter_string(length: int, cover: bool) -> str: | 17 def build_filter_string(length: int, cover: bool) -> str: |
| 12 f = "" | 18 f = "" |
| 13 i = 1 if cover else 0 | 19 i = 1 if cover else 0 |
| 14 for x in range(length): | 20 for x in range(length): |
| 15 f += ("[%d:0]" % (x + i)) | 21 f += ("[%d:0]" % (x + i)) |
| 16 f += ("concat=n=%d:v=0:a=1[out]" % length) | 22 f += ("concat=n=%d:v=0:a=1[out]" % length) |
| 17 return f | 23 return f |
| 18 | 24 |
| 19 def build_arguments_list(audio_files: list[str], cover, output) -> list: | 25 def build_arguments_list(audio_files: list[str], cover, output, blur) -> list: |
| 20 args = ["ffmpeg", "-y"] | 26 args = ["ffmpeg", "-y"] |
| 21 if not cover is None: | 27 if not cover is None: |
| 22 args.extend(["-r", "1", "-loop", "1", "-i", cover]) | 28 args.extend(["-r", "1", "-loop", "1", "-i", cover]) |
| 23 # I want you to play that song again | 29 # I want you to play that song again |
| 24 for f in audio_files: | 30 for f in audio_files: |
| 25 args.extend(["-i", f]) | 31 args.extend(["-i", f]) |
| 26 args.extend(["-filter_complex", build_filter_string(len(audio_files), False if cover is None else True), "-map", "[out]"]) | 32 args.extend(["-filter_complex", build_filter_string(len(audio_files), False if cover is None else True), "-map", "[out]"]) |
| 27 if not cover is None: | 33 if not cover is None: |
| 28 # Letterbox to 1920x1080 | 34 # Letterbox to 1920x1080 |
| 29 args.extend(["-map", "0:v", "-tune", "stillimage", "-c:v", "libx264", "-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"]) | 35 args.extend(["-map", "0:v", "-tune", "stillimage", "-c:v", "libx264"]) |
| 36 args.extend(build_vf(blur)) | |
| 30 args.extend(["-shortest"]) | 37 args.extend(["-shortest"]) |
| 31 args.append(output + ".mkv") | 38 args.append(output + ".mkv") |
| 32 return args | 39 return args |
| 33 | 40 |
| 34 | 41 |
| 35 # Returns a Popen object that should be waited on | 42 # Returns a Popen object that should be waited on |
| 36 def run_ffmpeg(audio_files: list[str], cover, output) -> subprocess.Popen: | 43 def run_ffmpeg(audio_files: list[str], cover, output, blur) -> subprocess.Popen: |
| 37 args = build_arguments_list(audio_files, cover, output) | 44 args = build_arguments_list(audio_files, cover, output, blur) |
| 38 print(args) | 45 print(args) |
| 39 return subprocess.Popen(args) | 46 return subprocess.Popen(args) |
| 40 | 47 |
| 41 | 48 |
| 42 # Returns a Popen object that should be waited on. | 49 # Returns a Popen object that should be waited on. |
| 52 return j[tag] | 59 return j[tag] |
| 53 | 60 |
| 54 # Ugh | 61 # Ugh |
| 55 return default | 62 return default |
| 56 | 63 |
| 57 def doit(audio_files: list[str], cover, output) -> int: | 64 def doit(audio_files: list[str], cover, output, blur) -> int: |
| 58 ffprobes = [run_ffprobe(x) for x in audio_files] | 65 ffprobes = [run_ffprobe(x) for x in audio_files] |
| 59 ffmpeg = run_ffmpeg(audio_files, cover, output) | 66 ffmpeg = run_ffmpeg(audio_files, cover, output, blur) |
| 60 | 67 |
| 61 # Wait for all ffprobe processes to finish their jobs | 68 # Wait for all ffprobe processes to finish their jobs |
| 62 for f in ffprobes: | 69 for f in ffprobes: |
| 63 f.wait() | 70 f.wait() |
| 64 | 71 |
| 67 # more accurate | 74 # more accurate |
| 68 with open(output + ".txt", "w", encoding="utf-8") as fw: | 75 with open(output + ".txt", "w", encoding="utf-8") as fw: |
| 69 dur = 0.0 | 76 dur = 0.0 |
| 70 for f in ffprobes: | 77 for f in ffprobes: |
| 71 j = json.load(f.stdout) | 78 j = json.load(f.stdout) |
| 72 fw.write("%d:%02d %s\n" % (int(dur) // 60, int(dur) % 60, get_title_from_tags(j["format"]["tags"], "OOPS...."))) | 79 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...."))) |
| 73 dur += float(j["format"]["duration"]) | 80 dur += float(j["format"]["duration"]) |
| 74 | 81 |
| 75 # Finally, wait on the big ffmpeg process | 82 # Finally, wait on the big ffmpeg process |
| 76 ffmpeg.wait() | 83 ffmpeg.wait() |
| 77 | 84 |
| 78 def main() -> int: | 85 def main() -> int: |
| 79 parser = argparse.ArgumentParser(prog='create', description='Creates album videos', epilog='(c) paper 2025 all rights reversed') | 86 parser = argparse.ArgumentParser(prog='create', description='Creates album videos', epilog='(c) paper 2025 all rights reversed') |
| 80 parser.add_argument('-c', '--cover', required=True) | 87 parser.add_argument('-c', '--cover', required=True) |
| 81 parser.add_argument('-o', '--output', required=True) | 88 parser.add_argument('-o', '--output', required=True) |
| 89 parser.add_argument('-b', '--blur', action='store_true') | |
| 82 parser.add_argument('audio', action='extend', nargs='+', type=str) | 90 parser.add_argument('audio', action='extend', nargs='+', type=str) |
| 83 args = parser.parse_args() | 91 args = parser.parse_args() |
| 84 doit(args.audio, args.cover, args.output) | 92 doit(args.audio, args.cover, args.output, args.blur) |
| 85 | 93 |
| 86 if __name__ == "__main__": | 94 if __name__ == "__main__": |
| 87 exit(main()) | 95 exit(main()) |
