diff --git a/README.md b/README.md index 5fb29a4..38f7138 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/movmaker.py b/movmaker.py index 5798394..2f4e1d9 100755 --- a/movmaker.py +++ b/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,7 +994,8 @@ 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") - console(f"Audio: {audio if audio else 'none'}") + 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)