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
|
- includes video clips inline
|
||||||
- crossfades between media items over 3 seconds by default
|
- crossfades between media items over 3 seconds by default
|
||||||
- loops/trims music to match the video
|
- 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`
|
- 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 music in over 2 seconds and out over 10 seconds by default
|
||||||
- fades video in/out over 1.5 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
|
- 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
|
- 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
|
- 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`
|
- 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
|
- 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 = {
|
replacements = {
|
||||||
"æ": "ae", "Æ": "Ae",
|
"æ": "ae", "Æ": "Ae",
|
||||||
"ø": "o", "Ø": "O",
|
"ø": "o", "Ø": "O",
|
||||||
"å": "a", "Å": "A",
|
"å": "aa", "Å": "Aa",
|
||||||
"đ": "d", "Đ": "D",
|
"đ": "d", "Đ": "D",
|
||||||
}
|
}
|
||||||
value = "".join(replacements.get(ch, ch) for ch in value)
|
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",
|
"format": "json",
|
||||||
"limit": "50",
|
"limit": "50",
|
||||||
"search": genre,
|
"search": genre,
|
||||||
"include": "musicinfo",
|
"include": "musicinfo licenses",
|
||||||
"audioformat": "mp32",
|
"audioformat": "mp32",
|
||||||
|
"ccnc": "true",
|
||||||
|
"ccnd": "false",
|
||||||
})
|
})
|
||||||
request = urllib.request.Request(
|
request = urllib.request.Request(
|
||||||
f"https://api.jamendo.com/v3.0/tracks/?{query}",
|
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}")
|
console(f"Could not search Jamendo music: {exc}")
|
||||||
return None
|
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:
|
if not results:
|
||||||
console(f"No Jamendo music found for genre/query: {genre}")
|
console(f"No Jamendo music found for genre/query: {genre}")
|
||||||
return None
|
return None
|
||||||
|
|
@ -656,14 +667,16 @@ def download_jamendo_music(genre: str, out_dir: Path) -> Path | None:
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
console(f"Could not download Jamendo music: {exc}")
|
console(f"Could not download Jamendo music: {exc}")
|
||||||
return None
|
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 {}
|
musicinfo = track.get("musicinfo", {}) if isinstance(track.get("musicinfo"), dict) else {}
|
||||||
attribution.write_text(
|
attribution.write_text(
|
||||||
"Jamendo music downloaded by movmaker\n"
|
"Jamendo music downloaded by movmaker\n"
|
||||||
f"Title: {title}\n"
|
f"Title: {title}\n"
|
||||||
f"Artist: {artist}\n"
|
f"Artist: {artist}\n"
|
||||||
f"URL: {track.get('shareurl', '')}\n"
|
f"URL: {track.get('shareurl', '')}\n"
|
||||||
f"License: {musicinfo.get('license_ccurl', '')}\n"
|
f"License: {track.get('license_ccurl', '')}\n"
|
||||||
"Source: Jamendo\n",
|
"Source: Jamendo\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
@ -757,7 +770,7 @@ def build_video(
|
||||||
title_label = "titled"
|
title_label = "titled"
|
||||||
title_font = str(Path.home() / ".local/share/fonts/google/BebasNeue-Regular.ttf")
|
title_font = str(Path.home() / ".local/share/fonts/google/BebasNeue-Regular.ttf")
|
||||||
stamp_font = "/usr/share/fonts/truetype/noto/NotoSans-Bold.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:
|
if title_lines:
|
||||||
title_font_size = max(60, height // 10)
|
title_font_size = max(60, height // 10)
|
||||||
subtitle_font_size = max(40, height // 18)
|
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}"
|
y_expr = f"h-text_h-62-{block_height - line_idx * line_gap}"
|
||||||
next_label = f"caption{idx}_{line_idx}"
|
next_label = f"caption{idx}_{line_idx}"
|
||||||
filters.append(
|
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"fontsize={caption_size}:fontcolor=white:borderw=1:bordercolor=black:"
|
||||||
f"alpha='{caption_alpha}':enable='between(t,{start:.3f},{end:.3f})'[{next_label}]"
|
f"alpha='{caption_alpha}':enable='between(t,{start:.3f},{end:.3f})'[{next_label}]"
|
||||||
)
|
)
|
||||||
|
|
@ -844,7 +857,7 @@ def build_video(
|
||||||
else:
|
else:
|
||||||
filters.append(f"[{caption_label}]copy[{stamp_left_label}]")
|
filters.append(f"[{caption_label}]copy[{stamp_left_label}]")
|
||||||
filters.append(
|
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}]"
|
f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{final_label}]"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -855,7 +868,7 @@ def build_video(
|
||||||
"location": data.place,
|
"location": data.place,
|
||||||
"place": data.place,
|
"place": data.place,
|
||||||
"com.apple.quicktime.location.name": data.place,
|
"com.apple.quicktime.location.name": data.place,
|
||||||
"artist": "@bubulescu",
|
"artist": "Bubulescu.Org",
|
||||||
"comment": "Created with movmaker",
|
"comment": "Created with movmaker",
|
||||||
"creation_time": creation_date.isoformat(),
|
"creation_time": creation_date.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
@ -944,6 +957,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
item.duration = item.duration + (2 * fade)
|
item.duration = item.duration + (2 * fade)
|
||||||
|
|
||||||
console(f"Found {len(media)} media file(s), {len(audio_files)} audio file(s).")
|
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"Output: {output}")
|
||||||
console(f"Log: {LOG_PATH}")
|
console(f"Log: {LOG_PATH}")
|
||||||
if data.title or data.date or data.place:
|
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)
|
jobs = max(1, args.jobs)
|
||||||
clips = [tmp / f"clip_{idx:04d}.mp4" for idx in range(len(media))]
|
clips = [tmp / f"clip_{idx:04d}.mp4" for idx in range(len(media))]
|
||||||
if jobs == 1:
|
if jobs == 1:
|
||||||
console("Preparing clips...")
|
|
||||||
total_clips = len(media)
|
total_clips = len(media)
|
||||||
for done, (item, out) in enumerate(zip(media, clips), start=1):
|
for done, (item, out) in enumerate(zip(media, clips), start=1):
|
||||||
percent = done / total_clips * 100
|
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]
|
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")
|
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"Audio: {audio if audio else 'none'}")
|
||||||
console(f"Estimated output: video length {estimated_duration:.1f}s, size about {format_bytes(estimated_size)}")
|
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)
|
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