Filter Jamendo music licenses

This commit is contained in:
hbrain 2026-05-25 10:43:55 +00:00
parent ecaa6cc959
commit e512b5012b
2 changed files with 27 additions and 13 deletions

View file

@ -183,13 +183,13 @@ python3 movmaker.py input --preview
- includes video clips inline
- crossfades between media items over 3 seconds by default
- loops/trims music to match the video
- if no audio file exists in the input folder, searches Jamendo using `--music-genre` and downloads one random track; default query is `cinematic punk rock`
- if no audio file exists in the input folder, searches Jamendo using `--music-genre` and downloads one random Creative Commons track that allows non-commercial reuse and derivative works; default query is `cinematic punk rock`
- reads the Jamendo client ID from `JAMENDO_CLIENT_ID` or `~/.config/movmaker/jamendo.env`
- fades music in over 2 seconds and out over 10 seconds by default
- fades video in/out over 1.5 seconds by default
- overlays the opening title for the first 6 seconds: large title, then date/location on one line
- displays optional per-file captions at bottom center with a thin black outline; use `|` to split caption lines
- adds persistent bottom-left title/date/place and bottom-right `@bubulescu` stamps
- adds persistent bottom-left title/date/place and bottom-right `Bubulescu.Org` stamps
- preserves special characters in rendered overlays and metadata
- uses safe ASCII only for generated filenames, so special letters become readable equivalents like `Č` -> `C`, `å` -> `a`, `ø` -> `o`
- embeds MP4 metadata: title, date, location/place, QuickTime location name, artist, comment, and creation_time

View file

@ -442,7 +442,7 @@ def ascii_safe(value: str) -> str:
replacements = {
"æ": "ae", "Æ": "Ae",
"ø": "o", "Ø": "O",
"å": "a", "Å": "A",
"å": "aa", "Å": "Aa",
"đ": "d", "Đ": "D",
}
value = "".join(replacements.get(ch, ch) for ch in value)
@ -625,8 +625,10 @@ def download_jamendo_music(genre: str, out_dir: Path) -> Path | None:
"format": "json",
"limit": "50",
"search": genre,
"include": "musicinfo",
"include": "musicinfo licenses",
"audioformat": "mp32",
"ccnc": "true",
"ccnd": "false",
})
request = urllib.request.Request(
f"https://api.jamendo.com/v3.0/tracks/?{query}",
@ -639,7 +641,16 @@ def download_jamendo_music(genre: str, out_dir: Path) -> Path | None:
console(f"Could not search Jamendo music: {exc}")
return None
results = [track for track in data.get("results", []) if track.get("audio")]
results = []
for track in data.get("results", []):
license_url = str(track.get("license_ccurl", "")).lower()
if not track.get("audio"):
continue
if track.get("audiodownload_allowed") is False:
continue
if "creativecommons.org" not in license_url or "/nd" in license_url:
continue
results.append(track)
if not results:
console(f"No Jamendo music found for genre/query: {genre}")
return None
@ -656,14 +667,16 @@ def download_jamendo_music(genre: str, out_dir: Path) -> Path | None:
except Exception as exc:
console(f"Could not download Jamendo music: {exc}")
return None
attribution = out.with_suffix(".txt")
log_dir = out_dir / ".logs"
log_dir.mkdir(parents=True, exist_ok=True)
attribution = log_dir / f"{out.stem}.txt"
musicinfo = track.get("musicinfo", {}) if isinstance(track.get("musicinfo"), dict) else {}
attribution.write_text(
"Jamendo music downloaded by movmaker\n"
f"Title: {title}\n"
f"Artist: {artist}\n"
f"URL: {track.get('shareurl', '')}\n"
f"License: {musicinfo.get('license_ccurl', '')}\n"
f"License: {track.get('license_ccurl', '')}\n"
"Source: Jamendo\n",
encoding="utf-8",
)
@ -757,7 +770,7 @@ def build_video(
title_label = "titled"
title_font = str(Path.home() / ".local/share/fonts/google/BebasNeue-Regular.ttf")
stamp_font = "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf"
stamp_bold_font = "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf"
caption_font = "Montserrat\\:style=Bold"
if title_lines:
title_font_size = max(60, height // 10)
subtitle_font_size = max(40, height // 18)
@ -826,7 +839,7 @@ def build_video(
y_expr = f"h-text_h-62-{block_height - line_idx * line_gap}"
next_label = f"caption{idx}_{line_idx}"
filters.append(
f"[{caption_label}]drawtext=fontfile='{title_font}':{textfile_arg(caption_line)}:x=(w-text_w)/2:y={y_expr}:"
f"[{caption_label}]drawtext=font='{caption_font}':{textfile_arg(caption_line)}:x=(w-text_w)/2:y={y_expr}:"
f"fontsize={caption_size}:fontcolor=white:borderw=1:bordercolor=black:"
f"alpha='{caption_alpha}':enable='between(t,{start:.3f},{end:.3f})'[{next_label}]"
)
@ -844,7 +857,7 @@ def build_video(
else:
filters.append(f"[{caption_label}]copy[{stamp_left_label}]")
filters.append(
f"[{stamp_left_label}]drawtext=fontfile='{stamp_font}':{textfile_arg('@bubulescu')}:x=w-text_w-24:y=h-text_h-18:"
f"[{stamp_left_label}]drawtext=fontfile='{stamp_font}':{textfile_arg('Bubulescu.Org')}:x=w-text_w-24:y=h-text_h-18:"
f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{final_label}]"
)
@ -855,7 +868,7 @@ def build_video(
"location": data.place,
"place": data.place,
"com.apple.quicktime.location.name": data.place,
"artist": "@bubulescu",
"artist": "Bubulescu.Org",
"comment": "Created with movmaker",
"creation_time": creation_date.isoformat(),
}
@ -944,6 +957,7 @@ def main(argv: list[str] | None = None) -> int:
item.duration = item.duration + (2 * fade)
console(f"Found {len(media)} media file(s), {len(audio_files)} audio file(s).")
console(f"Audio: {audio_files[0] if audio_files else 'none'}")
console(f"Output: {output}")
console(f"Log: {LOG_PATH}")
if data.title or data.date or data.place:
@ -954,7 +968,6 @@ def main(argv: list[str] | None = None) -> int:
jobs = max(1, args.jobs)
clips = [tmp / f"clip_{idx:04d}.mp4" for idx in range(len(media))]
if jobs == 1:
console("Preparing clips...")
total_clips = len(media)
for done, (item, out) in enumerate(zip(media, clips), start=1):
percent = done / total_clips * 100
@ -981,6 +994,7 @@ def main(argv: list[str] | None = None) -> int:
captions = [data.captions.get(item.path.name, data.captions.get(str(item.path), "")) for item in media]
audio = concat_or_mix_audio(audio_files, tmp / "music.m4a")
if len(audio_files) > 1:
console(f"Audio: {audio if audio else 'none'}")
console(f"Estimated output: video length {estimated_duration:.1f}s, size about {format_bytes(estimated_size)}")
build_video(clips, durations, audio, output, data, first_media_date, captions, fade, audio_fade, audio_fade_in, video_fade, width, height, fps, preset, crf, tmp)