annotate create.py @ 132:71df0cf3aa05 default tip

add create.py this is a script to render out video files from entire albums, singles, or EPs. eventually it can be edited to be more robust (such as automatically finding discogs/musicbrainz links) but I think it's pretty damn good for now. It's basically just an ffmpeg frontend with a few hardcoded options that are suitable for this kind of thing.
author Paper <paper@tflc.us>
date Fri, 02 Jan 2026 10:35:03 -0500
parents
children
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
132
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
1 #!/usr/bin/env python3
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
2 # Simple python script to create a video from a bunch of wav files
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
3 # and an image. Also creates a cue-like track listing that contains
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
4 # track numbers etc.
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
5
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
6 import argparse
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
7 import json
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
8 import subprocess
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
9
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
10 # Build filter string
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
11 def build_filter_string(length: int, cover: bool) -> str:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
12 f = ""
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
13 i = 1 if cover else 0
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
14 for x in range(length):
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
15 f += ("[%d:0]" % (x + i))
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
16 f += ("concat=n=%d:v=0:a=1[out]" % length)
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
17 return f
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
18
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
19 def build_arguments_list(audio_files: list[str], cover, output) -> list:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
20 args = ["ffmpeg", "-y"]
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
21 if not cover is None:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
22 args.extend(["-r", "1", "-loop", "1", "-i", cover])
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
23 # I want you to play that song again
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
24 for f in audio_files:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
25 args.extend(["-i", f])
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
26 args.extend(["-filter_complex", build_filter_string(len(audio_files), False if cover is None else True), "-map", "[out]"])
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
27 if not cover is None:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
28 # Letterbox to 1920x1080
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
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"])
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
30 args.extend(["-shortest"])
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
31 args.append(output + ".mkv")
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
32 return args
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
33
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
34
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
35 # Returns a Popen object that should be waited on
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
36 def run_ffmpeg(audio_files: list[str], cover, output) -> subprocess.Popen:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
37 args = build_arguments_list(audio_files, cover, output)
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
38 print(args)
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
39 return subprocess.Popen(args)
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
40
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
41
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
42 # Returns a Popen object that should be waited on.
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
43 # stdout is also piped...
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
44 def run_ffprobe(audio_file: str) -> subprocess.Popen:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
45 return subprocess.Popen(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", audio_file], stdout=subprocess.PIPE, text=True)
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
46
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
47
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
48 def get_title_from_tags(j, default: str) -> str:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
49 # Is there an easier way to do this...?
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
50 for tag in j:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
51 if tag.lower() == "title":
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
52 return j[tag]
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
53
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
54 # Ugh
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
55 return default
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
56
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
57 def doit(audio_files: list[str], cover, output) -> int:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
58 ffprobes = [run_ffprobe(x) for x in audio_files]
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
59 ffmpeg = run_ffmpeg(audio_files, cover, output)
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
60
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
61 # Wait for all ffprobe processes to finish their jobs
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
62 for f in ffprobes:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
63 f.wait()
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
64
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
65 # Iterate the list again, and add up all the times.
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
66 # This also accounts for milliseconds etc. to be
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
67 # more accurate
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
68 with open(output + ".txt", "w", encoding="utf-8") as fw:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
69 dur = 0.0
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
70 for f in ffprobes:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
71 j = json.load(f.stdout)
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
72 fw.write("%d:%02d %s\n" % (int(dur) // 60, int(dur) % 60, get_title_from_tags(j["format"]["tags"], "OOPS....")))
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
73 dur += float(j["format"]["duration"])
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
74
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
75 # Finally, wait on the big ffmpeg process
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
76 ffmpeg.wait()
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
77
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
78 def main() -> int:
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
79 parser = argparse.ArgumentParser(prog='create', description='Creates album videos', epilog='(c) paper 2025 all rights reversed')
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
80 parser.add_argument('-c', '--cover', required=True)
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
81 parser.add_argument('-o', '--output', required=True)
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
82 parser.add_argument('audio', action='extend', nargs='+', type=str)
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
83 args = parser.parse_args()
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
84 doit(args.audio, args.cover, args.output)
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
85
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
86 if __name__ == "__main__":
71df0cf3aa05 add create.py
Paper <paper@tflc.us>
parents:
diff changeset
87 exit(main())