Mercurial > codedump
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 135:0c3cd90e91f7 | 136:da4f7200665f |
|---|---|
| 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 import os | |
| 10 import math #ceil | |
| 11 import typing | |
| 9 | 12 |
| 10 def build_vf(blur) -> list[str]: | 13 def build_vf(blur) -> list[str]: |
| 11 if blur: | 14 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"] | 15 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 | 16 |
| 14 return ["-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"] | 17 return ["-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"] |
| 15 | 18 |
| 16 # Build filter string | 19 # Build filter string |
| 17 def build_filter_string(length: int, cover: bool) -> str: | 20 def build_filter_string(length: int, offset: int) -> str: |
| 18 f = "" | 21 f = "" |
| 19 i = 1 if cover else 0 | 22 i = offset |
| 20 for x in range(length): | 23 for x in range(length): |
| 21 f += ("[%d:0]" % (x + i)) | 24 f += ("[%d:0]" % (x + i)) |
| 22 f += ("concat=n=%d:v=0:a=1[out]" % length) | 25 f += ("concat=n=%d:v=0:a=1[out]" % length) |
| 23 return f | 26 return f |
| 24 | 27 |
| 25 def build_arguments_list(audio_files: list[str], cover, output, blur) -> list: | 28 def build_arguments_list(audio_files: list[str], cover: typing.Optional[str], output: str, blur: bool, duration: float) -> list: |
| 26 args = ["ffmpeg", "-y"] | 29 args = ["ffmpeg", "-y"] |
| 27 if not cover is None: | 30 # I want you to play that song again |
| 28 args.extend(["-r", "1", "-loop", "1", "-i", cover]) | 31 if not cover is None: |
| 29 # I want you to play that song again | 32 args.extend(["-r", "1", "-loop", "1", "-i", cover]) |
| 30 for f in audio_files: | 33 for f in audio_files: |
| 31 args.extend(["-i", f]) | 34 args.extend(["-i", f]) |
| 32 args.extend(["-filter_complex", build_filter_string(len(audio_files), False if cover is None else True), "-map", "[out]"]) | 35 args.extend(["-filter_complex", build_filter_string(len(audio_files), 1 if cover else 0), "-map", "[out]"]) |
| 33 if not cover is None: | 36 if not cover is None: |
| 34 # Letterbox to 1920x1080 | 37 # Letterbox to 1920x1080 |
| 35 args.extend(["-map", "0:v", "-tune", "stillimage", "-c:v", "libx264"]) | 38 args.extend(["-map", "0:v", |
| 36 args.extend(build_vf(blur)) | 39 "-tune", "stillimage", |
| 37 args.extend(["-shortest"]) | 40 "-c:v", "libx264", |
| 38 args.append(output + ".mkv") | 41 # Add a second to account for inconsistencies in durations |
| 39 return args | 42 "-t", "%f" % (duration + 1.0)]) |
| 43 args.extend(build_vf(blur)) | |
| 44 args.append(output + ".mkv") | |
| 45 return args | |
| 40 | 46 |
| 41 | 47 |
| 42 # Returns a Popen object that should be waited on | 48 # Returns a Popen object that should be waited on |
| 43 def run_ffmpeg(audio_files: list[str], cover, output, blur) -> subprocess.Popen: | 49 def run_ffmpeg(audio_files: list[str], cover: typing.Optional[str], output: str, blur: bool, duration: float) -> subprocess.Popen: |
| 44 args = build_arguments_list(audio_files, cover, output, blur) | 50 args = build_arguments_list(audio_files, cover, output, blur, duration) |
| 45 print(args) | 51 print(args) |
| 46 return subprocess.Popen(args) | 52 return subprocess.Popen(args) |
| 47 | 53 |
| 48 | 54 |
| 49 # Returns a Popen object that should be waited on. | 55 # Returns a Popen object that should be waited on. |
| 50 # stdout is also piped... | 56 # stdout is also piped... |
| 51 def run_ffprobe(audio_file: str) -> subprocess.Popen: | 57 def run_ffprobe(audio_file: str) -> subprocess.Popen: |
| 52 return subprocess.Popen(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", audio_file], stdout=subprocess.PIPE, text=True) | 58 return subprocess.Popen(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", audio_file], stdout=subprocess.PIPE, text=True) |
| 53 | 59 |
| 54 | 60 |
| 55 def get_title_from_tags(j, default: str) -> str: | 61 def get_title_from_tags(j: list, default: str) -> str: |
| 56 # Is there an easier way to do this...? | 62 # Is there an easier way to do this...? |
| 57 for tag in j: | 63 for tag in j: |
| 58 if tag.lower() == "title": | 64 if tag.lower() == "title": |
| 59 return j[tag] | 65 return j[tag] |
| 60 | 66 |
| 61 # Ugh | 67 # Ugh |
| 62 return default | 68 return default |
| 63 | 69 |
| 64 def doit(audio_files: list[str], cover, output, blur) -> int: | 70 def doit(audio_files: list[str], cover, output, blur) -> int: |
| 65 ffprobes = [run_ffprobe(x) for x in audio_files] | 71 dur = 0.0 |
| 66 ffmpeg = run_ffmpeg(audio_files, cover, output, blur) | |
| 67 | 72 |
| 68 # Wait for all ffprobe processes to finish their jobs | 73 ffprobes = [run_ffprobe(x) for x in audio_files] |
| 69 for f in ffprobes: | |
| 70 f.wait() | |
| 71 | 74 |
| 72 # Iterate the list again, and add up all the times. | 75 # Create output txt file with timestamps; we also use this duration |
| 73 # This also accounts for milliseconds etc. to be | 76 # to force ffmpeg to do the right thing instead of elongating our |
| 74 # more accurate | 77 # video |
| 75 with open(output + ".txt", "w", encoding="utf-8") as fw: | 78 with open(output + ".txt", "w", encoding="utf-8") as fw: |
| 76 dur = 0.0 | 79 for f in ffprobes: |
| 77 for f in ffprobes: | 80 f.wait() |
| 78 j = json.load(f.stdout) | 81 j = json.load(f.stdout) |
| 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...."))) | 82 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...."))) |
| 80 dur += float(j["format"]["duration"]) | 83 dur += float(j["format"]["duration"]) |
| 81 | 84 |
| 82 # Finally, wait on the big ffmpeg process | 85 ffmpeg = run_ffmpeg(audio_files, cover, output, blur, dur) |
| 83 ffmpeg.wait() | 86 |
| 87 # Finally, wait on the big ffmpeg process | |
| 88 ffmpeg.wait() | |
| 84 | 89 |
| 85 def main() -> int: | 90 def main() -> int: |
| 86 parser = argparse.ArgumentParser(prog='create', description='Creates album videos', epilog='(c) paper 2025 all rights reversed') | 91 parser = argparse.ArgumentParser(prog='create', description='Creates album videos', epilog='(c) paper 2025 all rights reversed') |
| 87 parser.add_argument('-c', '--cover', required=True) | 92 parser.add_argument('-c', '--cover', required=True) |
| 88 parser.add_argument('-o', '--output', required=True) | 93 parser.add_argument('-o', '--output', required=True) |
| 89 parser.add_argument('-b', '--blur', action='store_true') | 94 parser.add_argument('-b', '--blur', action='store_true') |
| 90 parser.add_argument('audio', action='extend', nargs='+', type=str) | 95 parser.add_argument('audio', action='extend', nargs='+', type=str) |
| 91 args = parser.parse_args() | 96 args = parser.parse_args() |
| 92 doit(args.audio, args.cover, args.output, args.blur) | 97 doit(args.audio, args.cover, args.output, args.blur) |
| 93 | 98 |
| 94 if __name__ == "__main__": | 99 if __name__ == "__main__": |
| 95 exit(main()) | 100 exit(main()) |
