Add Pixabay music fallback
This commit is contained in:
parent
e35201496f
commit
53b2e5ecca
2 changed files with 116 additions and 17 deletions
91
movmaker.py
91
movmaker.py
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue