Add Pixabay music fallback

This commit is contained in:
hbrain 2026-05-25 09:43:51 +00:00
parent e35201496f
commit 53b2e5ecca
2 changed files with 116 additions and 17 deletions

View file

@ -14,6 +14,7 @@ import concurrent.futures
import json
import math
import os
import random
import re
import shlex
import subprocess
@ -33,6 +34,7 @@ DATA_NAMES = {"data.txt", "info.txt", "title.txt"}
BASE_DIR = Path(__file__).resolve().parent
FFMPEG = BASE_DIR / "ffmpeg" / "ffmpeg"
FFPROBE = BASE_DIR / "ffmpeg" / "ffprobe"
PIXABAY_ENV = Path.home() / ".config" / "movmaker" / "pixabay.env"
def tool_path(local_path: Path, fallback: str) -> str:
@ -516,6 +518,71 @@ def normalize_clip(item: MediaItem, out: Path, width: int, height: int, fps: int
run(cmd)
def load_pixabay_api_key() -> str | None:
if os.environ.get("PIXABAY_API_KEY"):
return os.environ["PIXABAY_API_KEY"]
if PIXABAY_ENV.exists():
for line in PIXABAY_ENV.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
if key.strip() == "PIXABAY_API_KEY":
return value.strip().strip("'\"")
return None
def download_pixabay_music(genre: str, out_dir: Path) -> Path | None:
api_key = load_pixabay_api_key()
if not api_key:
print("No audio files found and no Pixabay API key configured; continuing without music.")
return None
query = urllib.parse.urlencode({
"key": api_key,
"q": genre,
"media_type": "music",
"safesearch": "true",
"per_page": "50",
})
request = urllib.request.Request(
f"https://pixabay.com/api/audio/?{query}",
headers={"User-Agent": "movmaker/1.0"},
)
try:
with urllib.request.urlopen(request, timeout=20) as response:
data = json.loads(response.read().decode("utf-8"))
except Exception as exc:
print(f"Could not search Pixabay music: {exc}")
return None
hits = [hit for hit in data.get("hits", []) if hit.get("audio")]
if not hits:
print(f"No Pixabay music found for genre/query: {genre}")
return None
hit = random.choice(hits)
audio_url = hit["audio"]
title = hit.get("title") or "pixabay_music"
filename = f"pixabay_{hit.get('id', 'music')}_{slugify(title)}.mp3"
out_dir.mkdir(parents=True, exist_ok=True)
out = out_dir / filename
print(f"Downloading Pixabay music: {title}")
try:
urllib.request.urlretrieve(audio_url, out)
except Exception as exc:
print(f"Could not download Pixabay music: {exc}")
return None
attribution = out.with_suffix(".txt")
attribution.write_text(
"Pixabay music downloaded by movmaker\n"
f"Title: {title}\n"
f"Artist/User: {hit.get('user', '')}\n"
f"URL: {hit.get('pageURL', '')}\n"
"Source: Pixabay\n",
encoding="utf-8",
)
return out
def concat_or_mix_audio(audio_files: list[Path], out: Path) -> Path | None:
if not audio_files:
return None
@ -602,8 +669,8 @@ def build_video(
final_label = "vout"
title_label = "titled"
title_font = str(Path.home() / ".local/share/fonts/google/BebasNeue-Regular.ttf")
stamp_font = str(Path.home() / ".local/share/fonts/google/NotoSans-SemiBold.ttf")
stamp_bold_font = str(Path.home() / ".local/share/fonts/google/NotoSans-SemiBold.ttf")
stamp_font = "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf"
stamp_bold_font = "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf"
if title_lines:
title_font_size = max(60, height // 10)
subtitle_font_size = max(40, height // 18)
@ -642,7 +709,7 @@ def build_video(
filters.append(f"[{current}]copy[{title_label}]")
caption_label = title_label
caption_size = max(16, int((subtitle_font_size if title_lines else max(40, height // 18)) * 0.5))
caption_size = max(24, int((subtitle_font_size if title_lines else max(40, height // 18)) * 0.7))
for idx, caption in enumerate(captions):
if not caption:
continue
@ -673,18 +740,12 @@ def build_video(
next_label = f"caption{idx}_{line_idx}"
filters.append(
f"[{caption_label}]drawtext=fontfile='{stamp_bold_font}':{textfile_arg(caption_line)}:x=(w-text_w)/2:y={y_expr}:"
f"fontsize={caption_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=2:shadowy=2:"
f"fontsize={caption_size}:fontcolor=white:borderw=1:bordercolor=black:"
f"alpha='{caption_alpha}':enable='between(t,{start:.3f},{end:.3f})'[{next_label}]"
)
bold_label = f"caption{idx}_{line_idx}b"
filters.append(
f"[{next_label}]drawtext=fontfile='{stamp_bold_font}':{textfile_arg(caption_line)}:x=(w-text_w)/2+1:y={y_expr}:"
f"fontsize={caption_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=2:shadowy=2:"
f"alpha='{caption_alpha}':enable='between(t,{start:.3f},{end:.3f})'[{bold_label}]"
)
caption_label = bold_label
caption_label = next_label
stamp_size = max(14, height // 65)
stamp_size = max(16, height // 58)
stamp_lines = [x for x in [data.title, data.date, data.place] if x]
stamp_text = drawtext_escape(" | ".join(stamp_lines))
stamp_left_label = "stamp_left"
@ -742,6 +803,7 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--crf", type=int, default=20, help="x264 quality; lower is better/larger, higher is faster/smaller")
parser.add_argument("--jobs", type=int, default=1, help="parallel clip preparation jobs; try 2 on Raspberry Pi")
parser.add_argument("--preview", action="store_true", help="fast preview: 1280x720, 24 fps, ultrafast, CRF 28")
parser.add_argument("--music-genre", default="cinematic punk rock", help="Pixabay music search query used only when input has no audio files")
args = parser.parse_args(argv)
require_tool("ffmpeg")
@ -768,6 +830,11 @@ def main(argv: list[str] | None = None) -> int:
audio_fade_in = max(0.0, args.audio_fade_in)
video_fade = max(0.0, args.video_fade)
if not audio_files:
downloaded_audio = download_pixabay_music(args.music_genre, args.input)
if downloaded_audio:
audio_files.append(downloaded_audio)
output = default_output_path(media[0].path, data, args.out_dir)
# Image duration means fully visible time, not including crossfades.