Filter Jamendo music licenses
This commit is contained in:
parent
ecaa6cc959
commit
e512b5012b
2 changed files with 27 additions and 13 deletions
|
|
@ -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
|
||||
|
|
|
|||
34
movmaker.py
34
movmaker.py
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue