Self-hosted video downloader for YouTube and other sites (web UI for yt-dlp)
This report summarizes scanner evidence for alexta69/metube, a Python project with 14,965 GitHub stars. SynthScan examined 29,577 lines of code across 91 source files, recording 647 pattern matches distributed across 9 syntactic categories. The legacy adjusted pattern signal is 21.4 (Elevated pattern signal); it is not an authorship estimate.
Use the findings to ask whether maintainers can explain intent, verify dependencies, simplify unnecessary scaffolding, and focus review effort. Risk Payload v1 adds ownership, supply-chain, over-engineering, and review-effort vectors when available. These are prompts for human verification, not probabilities, security findings, or measured maintenance costs.
Use each vector to guide manual verification of the underlying findings.
Can maintainers explain intent, assumptions, and trade-offs? 593 finding(s).
Are imports declared and package/API names verified? 19 finding(s).
Are wrappers, guards, and repeated structures necessary? 604 finding(s).
Which changes need focused human review? 647 finding(s).
This chart tracks the legacy adjusted pattern score across scan runs. Changes can reflect source changes, scan coverage, or detector/scoring versions; they do not measure AI adoption or prove that a team refactored code. Compare runs only when their scoring versions are compatible. The dashed line tracks the raw pattern-hit count.
Groups findings by configured severity weight. Severity is not a confidence estimate, security rating, or proof that a package is missing. Verify imports against the project's manifests and inspect each finding in its local design context.
This horizontal bar chart shows where configured pattern scores accumulate across directories. Treat the bars as locations to inspect, not as evidence of authorship or code quality. Use the file-level findings to decide whether a directory needs a focused ownership, dependency, complexity, or review check.
The scanner identified 647 distinct pattern matches across 9 syntactic categories. Each entry below represents a discrete location in the source code where the engine recorded a detector match to inspect in context. Expand any category row to inspect the individual file paths, line numbers, code snippets, and the lexical context (CODE, COMMENT, or STRING) in which each match was detected.
Reading the findings table: The Severity column indicates the configured finding weight (CRITICAL / HIGH / MEDIUM / LOW). The Context column identifies whether the match occurred inside executable code, an inline comment, or a string literal — legacy comment-context matches receive a ×1.5 score weight. The ⚡ bolt icon marks clustered matches: three or more patterns within a 10-line window, each receiving an additional ×1.5 density multiplier under the legacy scoring model.
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | app/state_store.py | 192 | def _warn_direct_write_fallback(self, exc: OSError) -> None: | CODE |
| LOW | app/ytdl.py | 392 | def _sanitize_entry_for_pickle(obj, _depth=0): | CODE |
| LOW | app/ytdl.py | 699 | def _short_title_for_failed_url(url: str) -> str: | CODE |
| LOW | app/ytdl.py | 742 | def _download_info_from_record(record: dict[str, Any]) -> DownloadInfo: | CODE |
| LOW | app/ytdl.py | 1473 | def _seconds_until_next_probe(self) -> Optional[float]: | CODE |
| LOW | app/ytdl.py | 1511 | async def _probe_scheduled_download(self, download: Download) -> None: | CODE |
| LOW | app/ytdl.py | 1581 | async def _schedule_upcoming_download(self, download: Download) -> None: | CODE |
| LOW | app/ytdl.py | 1753 | def __write_feed_metadata_sync(self, entry, etype, download_type, folder, | CODE |
| LOW | app/subscriptions.py | 231 | def _is_subscriber_only_entry(entry: dict) -> bool: | CODE |
| LOW | app/subscriptions.py | 341 | def _normalize_subscription_record(rec: dict[str, Any]) -> dict[str, Any]: | CODE |
| LOW | app/subscriptions.py | 359 | def _subscription_from_record(record: Any) -> Optional[SubscriptionInfo]: | CODE |
| LOW | app/subscriptions.py | 372 | def _normalize_title_regex_value(value: Any) -> str: | CODE |
| LOW | app/subscriptions.py | 393 | def validate_subscription_name(value: Any) -> str: | CODE |
| LOW | app/subscriptions.py | 406 | def validate_subscription_folder(value: Any) -> str: | CODE |
| LOW | app/subscriptions.py | 551 | async def _queue_subscription_entries( | CODE |
| LOW | app/dl_formats.py | 56 | def _normalize_subtitle_language(language: str) -> str: | CODE |
| LOW | app/main.py | 38 | def seconds_until_next_daily_time(time_hhmm: str, now: datetime | None = None) -> float: | CODE |
| LOW | app/main.py | 490 | def _parse_ytdl_options_overrides(value, *, enabled: bool) -> dict: | CODE |
| LOW | app/main.py | 554 | def _parse_clip_timestamp_value(value) -> float: | CODE |
| LOW⚡ | app/main.py | 584 | def _clip_field_provided_in_post(raw) -> bool: | CODE |
| LOW⚡ | app/main.py | 592 | def _extract_t_query_from_url(url: str) -> tuple[str, float | None]: | CODE |
| LOW | app/main.py | 626 | def _parse_ytdl_options_presets(post: dict) -> list[str]: | CODE |
| LOW | app/main.py | 643 | def _require_configured_presets(names: list[str]) -> None: | CODE |
| LOW | app/main.py | 734 | async def _shutdown_download_manager(app): | CODE |
| LOW | app/main.py | 769 | async def _subscription_loop_startup(app): | CODE |
| LOW | app/main.py | 790 | async def _start_nightly_update_schedule(app): | CODE |
| LOW | app/main.py | 1235 | def warn_if_cookiefile_shadowed(): | CODE |
| LOW | app/url_guard.py | 153 | def _address_allowed_at_connect(addr: str, is_allowed_endpoint: bool = False) -> bool: | CODE |
| LOW⚡ | app/tests/test_nightly_update.py | 12 | def test_seconds_until_later_today(self): | CODE |
| LOW⚡ | app/tests/test_nightly_update.py | 17 | def test_seconds_until_wraps_to_next_day(self): | CODE |
| LOW⚡ | app/tests/test_nightly_update.py | 22 | def test_seconds_until_same_minute_is_next_day(self): | CODE |
| LOW | app/tests/test_music_metadata.py | 14 | def test_album_uses_existing_order_and_total_when_track_number_is_missing(): | CODE |
| LOW⚡ | app/tests/test_music_metadata.py | 28 | def test_official_track_number_wins_over_album_order(): | CODE |
| LOW⚡ | app/tests/test_music_metadata.py | 38 | def test_inline_official_track_total_is_preserved(): | CODE |
| LOW⚡ | app/tests/test_music_metadata.py | 47 | def test_source_track_number_and_total_are_retained_from_flat_extraction(): | CODE |
| LOW⚡ | app/tests/test_music_metadata.py | 56 | def test_album_title_falls_back_to_source_playlist_title(): | CODE |
| LOW⚡ | app/tests/test_music_metadata.py | 66 | def test_playlist_without_extractor_album_signals_is_not_changed(): | CODE |
| LOW | app/tests/test_music_metadata.py | 80 | def test_regular_video_artwork_is_not_changed(): | CODE |
| LOW | app/tests/test_music_metadata.py | 91 | def test_music_audio_prefers_largest_existing_square_thumbnail(): | CODE |
| LOW | app/tests/test_music_metadata.py | 108 | def test_landscape_only_music_artwork_keeps_existing_order(): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 30 | def test_bare_video_id_allowed(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 33 | def test_ytsearch_prefix_allowed(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 36 | def test_empty_string_allowed(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 55 | def test_localhost_blocked_without_lookup(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 60 | def test_localhost_subdomain_blocked(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 63 | def test_gcp_metadata_name_blocked(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 72 | def test_public_https_allowed(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 78 | def test_link_local_metadata_blocked(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 87 | def test_decimal_ip_form_blocked(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 120 | def test_loopback_blocked_by_default(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 126 | def test_loopback_allowed_only_when_opted_in(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 130 | def test_proxy_opt_in_covers_any_internal_range(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 138 | def test_opt_in_still_rejects_non_addresses(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 141 | def test_link_local_metadata_blocked(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 173 | def test_sixtofour_and_teredo_stay_blocked(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 180 | def test_plain_addresses_unaffected(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 185 | def test_tunnelled_form_blocked_at_ingress(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 224 | def test_internal_only_raises(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 229 | def test_filters_internal_keeps_global(self): | CODE |
| LOW⚡ | app/tests/test_url_guard.py | 235 | def test_loopback_blocked_without_matching_proxy(self): | CODE |
| 505 more matches not shown… | ||||
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| HIGH | app/state_store.py | 234 | except OSError: | CODE |
| HIGH | app/state_store.py | 167 | except OSError: | CODE |
| HIGH | app/state_store.py | 147 | except OSError: | CODE |
| HIGH | app/ytdl.py | 918 | except ValueError: | CODE |
| HIGH | app/ytdl.py | 1051 | except (OSError, AttributeError): | CODE |
| HIGH | app/ytdl.py | 1460 | except RuntimeError: | CODE |
| HIGH | app/ytdl.py | 908 | except OSError: | CODE |
| HIGH | app/ytdl.py | 1469 | except (TypeError, ValueError): | CODE |
| HIGH | app/ytdl.py | 1484 | except asyncio.TimeoutError: | CODE |
| HIGH | app/ytdl.py | 1609 | except OSError: | CODE |
| HIGH | app/ytdl.py | 2372 | except FileNotFoundError: | CODE |
| HIGH | app/main.py | 344 | except Exception: | CODE |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | app/state_store.py | 75 | except Exception as exc: | CODE |
| LOW | app/state_store.py | 113 | except Exception as exc: | CODE |
| LOW | app/state_store.py | 144 | except Exception: | CODE |
| MEDIUM | app/ytdl.py | 1091 | def running(self): | CODE |
| MEDIUM | app/ytdl.py | 1826 | def __write_feed_metadata(self, entry, etype, download_type, folder, | CODE |
| LOW | app/ytdl.py | 416 | except Exception: | CODE |
| LOW | app/ytdl.py | 421 | except Exception: | CODE |
| LOW | app/ytdl.py | 1059 | except Exception as e: | CODE |
| LOW | app/ytdl.py | 1330 | except Exception: | CODE |
| LOW | app/ytdl.py | 1344 | except Exception: | CODE |
| LOW | app/ytdl.py | 1503 | except Exception as exc: | CODE |
| LOW | app/ytdl.py | 1529 | except Exception as exc: | CODE |
| LOW | app/ytdl.py | 1836 | except Exception as exc: | CODE |
| LOW | app/subscriptions.py | 633 | except Exception as e: | CODE |
| LOW | app/subscriptions.py | 810 | except Exception: | CODE |
| LOW | app/subscriptions.py | 835 | except Exception: | CODE |
| LOW | app/subscriptions.py | 919 | except Exception: | CODE |
| LOW | app/subscriptions.py | 984 | except Exception: | CODE |
| LOW | app/subscriptions.py | 1003 | except Exception: | CODE |
| LOW | app/subscriptions.py | 1019 | except Exception: | CODE |
| LOW | app/subscriptions.py | 1151 | except Exception: | CODE |
| LOW | app/main.py | 344 | except Exception: | CODE |
| LOW⚡ | app/main.py | 602 | except Exception: | CODE |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | app/state_store.py | 1 | CODE | |
| LOW | app/ytdl.py | 2 | CODE | |
| LOW | app/subscriptions.py | 3 | CODE | |
| LOW | app/music_metadata.py | 8 | CODE | |
| LOW | app/tests/conftest.py | 3 | CODE | |
| LOW | app/tests/test_nightly_update.py | 3 | CODE | |
| LOW | app/tests/test_music_metadata.py | 3 | CODE | |
| LOW | app/tests/test_url_guard.py | 3 | CODE | |
| LOW | app/tests/test_state_store.py | 1 | CODE | |
| LOW | app/tests/test_format_catalog.py | 4 | CODE | |
| LOW | app/tests/test_subscriptions.py | 1 | CODE | |
| LOW | app/tests/test_dl_formats.py | 3 | CODE | |
| LOW | app/tests/test_config.py | 3 | CODE | |
| LOW | app/tests/test_main_helpers.py | 3 | CODE | |
| LOW | app/tests/test_api.py | 3 | CODE | |
| LOW | app/tests/test_download_queue.py | 3 | CODE | |
| LOW | app/tests/test_ytdl_utils.py | 3 | CODE | |
| LOW | app/tests/test_bg_tasks.py | 3 | CODE | |
| LOW | app/tests/test_persistent_queue.py | 3 | CODE |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | app/ytdl.py | 425 | CODE | |
| LOW | app/ytdl.py | 580 | CODE | |
| LOW | app/ytdl.py | 832 | CODE | |
| LOW | app/ytdl.py | 1100 | CODE | |
| LOW | app/ytdl.py | 1479 | CODE | |
| LOW | app/ytdl.py | 1840 | CODE | |
| LOW | app/ytdl.py | 2337 | CODE | |
| LOW | app/ytdl.py | 849 | CODE | |
| LOW | app/subscriptions.py | 98 | CODE | |
| LOW | app/subscriptions.py | 341 | CODE | |
| LOW | app/subscriptions.py | 962 | CODE | |
| LOW | app/dl_formats.py | 120 | CODE | |
| LOW | app/main.py | 649 | CODE | |
| LOW | app/main.py | 860 | CODE | |
| LOW | app/tests/test_state_store.py | 77 | CODE | |
| LOW | app/tests/test_subscriptions.py | 201 | CODE |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| HIGH | app/dl_formats.py | 62 | Returns yt-dlp format selector. Args: download_type (str): selected content type (video, audio, captions | STRING |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | app/ytdl.py | 121 | # (this app's only supported deployment target, per the Dockerfile) from fork | COMMENT |
| LOW | app/ytdl.py | 1121 | if getattr(self.info, 'download_type', '') == 'captions': | COMMENT |
| LOW | app/dl_formats.py | 1 | """Maps the options in format_catalog onto yt-dlp format selectors and options.""" | COMMENT |
| LOW | app/main.py | 1481 | COMMENT | |
| LOW | app/url_guard.py | 441 | # The proxy resolves this hostname on its own network, so a lookup here | COMMENT |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | app/main.py | 798 | # Check if this path matches our YTDL_OPTIONS_FILE | COMMENT |
| LOW | .github/workflows/update-yt-dlp.yml | 28 | # Check if yt-dlp has updates available | COMMENT |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | app/ytdl.py | 498 | CODE | |
| LOW | app/ytdl.py | 2088 | CODE | |
| LOW | app/ytdl.py | 2240 | CODE | |
| LOW | app/subscriptions.py | 675 | CODE |