diff --git a/modules/videos/downloader.py b/modules/videos/downloader.py index cde0a767..04e63260 100755 --- a/modules/videos/downloader.py +++ b/modules/videos/downloader.py @@ -124,6 +124,7 @@ def get_youtube_duration(video_url: str) -> Optional[int]: """Get video duration using YouTube's internal API. Returns None if URL is not YouTube or API fails.""" video_id = get_youtube_video_id(video_url) if not video_id: + logger.debug(f'Could not extract YouTube video ID from {video_url}') return None payload = { @@ -145,10 +146,15 @@ def get_youtube_duration(video_url: str) -> Optional[int]: response.raise_for_status() data = response.json() length_seconds = data.get("videoDetails", {}).get("lengthSeconds") - if length_seconds: + if length_seconds is not None: return int(length_seconds) + else: + # Log why we couldn't get duration + status = data.get("playabilityStatus", {}).get("status", "UNKNOWN") + reason = data.get("playabilityStatus", {}).get("reason", "") + logger.warning(f'YouTube API returned no duration for {video_url}: status={status} reason={reason}') except Exception as e: - logger.debug(f'YouTube API failed for {video_url}: {e}') + logger.debug(f'YouTube API request failed for {video_url}: {e}') return None @@ -282,7 +288,8 @@ async def fetch_video_duration(url: str) -> int: return duration # Try YouTube API first (faster than yt-dlp) - if duration := get_youtube_duration(url): + duration = get_youtube_duration(url) + if duration is not None: logger.info(f'Fetched video duration of {duration} from {url} using YouTube API') return duration diff --git a/modules/videos/lib.py b/modules/videos/lib.py index 2723a8b9..a55ea2b4 100644 --- a/modules/videos/lib.py +++ b/modules/videos/lib.py @@ -1098,32 +1098,41 @@ def get_browser_profiles(home: pathlib.Path = WROLPI_HOME) -> dict: def browser_profile_to_yt_dlp_arg(profile: pathlib.Path) -> str: - """Takes a Path and returns a string that can be used as a yt-dlp `--cookies-from-browser` argument.""" + """Takes a Path and returns a string that can be used as a yt-dlp `--cookies-from-browser` argument. + + Format: BROWSER+KEYRING:PROFILE + + We explicitly specify GNOMEKEYRING because yt-dlp's auto-detection relies on desktop environment + variables which are not available when running as a systemd service. + """ profile_str = str(profile) for browser_key, config in BROWSER_CONFIGS.items(): if config['config_path'] in profile_str: - return f"{config['yt_dlp_name']}:{profile.name}" + return f"{config['yt_dlp_name']}+GNOMEKEYRING:{profile.name}" # Fallback to original behavior for unknown browsers *_, browser, profile_name = profile_str.split('/') - return f'{browser}:{profile_name}' + return f'{browser}+GNOMEKEYRING:{profile_name}' def browser_profile_to_yt_dlp_tuple(profile: pathlib.Path) -> tuple: """Takes a Path and returns a tuple that can be used as yt-dlp `cookiesfrombrowser` option in Python API. The yt-dlp Python API expects cookiesfrombrowser as a tuple: (browser, profile, keyring, container) + + We explicitly specify GNOMEKEYRING because yt-dlp's auto-detection relies on desktop environment + variables which are not available when running as a systemd service. """ profile_str = str(profile) for browser_key, config in BROWSER_CONFIGS.items(): if config['config_path'] in profile_str: - return (config['yt_dlp_name'], profile.name, None, None) + return (config['yt_dlp_name'], profile.name, 'GNOMEKEYRING', None) # Fallback for unknown browsers *_, browser, profile_name = profile_str.split('/') - return (browser, profile_name, None, None) + return (browser, profile_name, 'GNOMEKEYRING', None) def format_video_filename( diff --git a/modules/videos/test/test_lib.py b/modules/videos/test/test_lib.py index 67c082ad..1828e2b8 100644 --- a/modules/videos/test/test_lib.py +++ b/modules/videos/test/test_lib.py @@ -370,34 +370,37 @@ async def test_videos_downloader_config_api(async_client, test_directory, test_v @pytest.mark.parametrize('file_name,expected', [ - ('/home/wrolpi/.config/chromium/Default', 'chromium:Default'), - ('/home/wrolpi/.config/chromium/Profile 1', 'chromium:Profile 1'), - ('/home/wrolpi/.mozilla/firefox/29el0wk0.default-release', 'firefox:29el0wk0.default-release'), - ('/home/wrolpi/.config/BraveSoftware/Brave-Browser/Default', 'brave:Default'), - ('/home/wrolpi/.config/BraveSoftware/Brave-Browser/Profile 1', 'brave:Profile 1'), - ('/home/wrolpi/.config/google-chrome/Default', 'chrome:Default'), - ('/home/wrolpi/.config/google-chrome/Profile 1', 'chrome:Profile 1'), + ('/home/wrolpi/.config/chromium/Default', 'chromium+GNOMEKEYRING:Default'), + ('/home/wrolpi/.config/chromium/Profile 1', 'chromium+GNOMEKEYRING:Profile 1'), + ('/home/wrolpi/.mozilla/firefox/29el0wk0.default-release', 'firefox+GNOMEKEYRING:29el0wk0.default-release'), + ('/home/wrolpi/.config/BraveSoftware/Brave-Browser/Default', 'brave+GNOMEKEYRING:Default'), + ('/home/wrolpi/.config/BraveSoftware/Brave-Browser/Profile 1', 'brave+GNOMEKEYRING:Profile 1'), + ('/home/wrolpi/.config/google-chrome/Default', 'chrome+GNOMEKEYRING:Default'), + ('/home/wrolpi/.config/google-chrome/Profile 1', 'chrome+GNOMEKEYRING:Profile 1'), ]) def test_browser_profile_to_yt_dlp_arg(file_name, expected): """ Test the conversion of a browser profile to yt-dlp arguments. + Format: BROWSER+KEYRING:PROFILE + Explicitly uses GNOMEKEYRING because yt-dlp auto-detection doesn't work in systemd services. """ file_name = pathlib.Path(file_name) assert lib.browser_profile_to_yt_dlp_arg(file_name) == expected @pytest.mark.parametrize('file_name,expected', [ - ('/home/wrolpi/.config/chromium/Default', ('chromium', 'Default', None, None)), - ('/home/wrolpi/.config/chromium/Profile 1', ('chromium', 'Profile 1', None, None)), - ('/home/wrolpi/.mozilla/firefox/29el0wk0.default-release', ('firefox', '29el0wk0.default-release', None, None)), - ('/home/wrolpi/.config/BraveSoftware/Brave-Browser/Default', ('brave', 'Default', None, None)), - ('/home/wrolpi/.config/BraveSoftware/Brave-Browser/Profile 1', ('brave', 'Profile 1', None, None)), - ('/home/wrolpi/.config/google-chrome/Default', ('chrome', 'Default', None, None)), - ('/home/wrolpi/.config/google-chrome/Profile 1', ('chrome', 'Profile 1', None, None)), + ('/home/wrolpi/.config/chromium/Default', ('chromium', 'Default', 'GNOMEKEYRING', None)), + ('/home/wrolpi/.config/chromium/Profile 1', ('chromium', 'Profile 1', 'GNOMEKEYRING', None)), + ('/home/wrolpi/.mozilla/firefox/29el0wk0.default-release', ('firefox', '29el0wk0.default-release', 'GNOMEKEYRING', None)), + ('/home/wrolpi/.config/BraveSoftware/Brave-Browser/Default', ('brave', 'Default', 'GNOMEKEYRING', None)), + ('/home/wrolpi/.config/BraveSoftware/Brave-Browser/Profile 1', ('brave', 'Profile 1', 'GNOMEKEYRING', None)), + ('/home/wrolpi/.config/google-chrome/Default', ('chrome', 'Default', 'GNOMEKEYRING', None)), + ('/home/wrolpi/.config/google-chrome/Profile 1', ('chrome', 'Profile 1', 'GNOMEKEYRING', None)), ]) def test_browser_profile_to_yt_dlp_tuple(file_name, expected): """ Test the conversion of a browser profile to yt-dlp cookiesfrombrowser tuple. + Explicitly uses GNOMEKEYRING because yt-dlp auto-detection doesn't work in systemd services. """ file_name = pathlib.Path(file_name) assert lib.browser_profile_to_yt_dlp_tuple(file_name) == expected diff --git a/requirements.txt b/requirements.txt index 0104a6d2..8b8d732b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,6 +25,7 @@ pytz==2024.2 sanic-ext==25.12.0 sanic-testing==24.6.0 sanic==25.12.0 +secretstorage==3.3.3 selenium==4.9.1 srt==3.5.3 tqdm==4.67.1