Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from pathlib import Path
from dataclasses import dataclass
from functools import lru_cache
from typing import Optional


# === Container Detection ===
Expand Down Expand Up @@ -116,7 +115,7 @@ class Settings:
# Paths
VIDEOS_FOLDER: Path
TMP_DOWNLOAD_FOLDER: Path
YOUTUBE_COOKIES_FILE_PATH: Optional[str]
YOUTUBE_COOKIES_FILE_PATH: str | None
COOKIES_FROM_BROWSER: str

# Localization
Expand Down
21 changes: 11 additions & 10 deletions app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
"""

import re
from typing import List, Pattern, Set

# === AUTHENTICATION ERROR PATTERNS ===
# Used to detect authentication-related errors in yt-dlp output.
# Shared between main.py and logs_utils.py
AUTH_ERROR_PATTERNS: List[str] = [
AUTH_ERROR_PATTERNS: list[str] = [
"sign in to confirm",
"please log in",
"login required",
Expand All @@ -29,12 +28,14 @@
# === ANSI ESCAPE PATTERN ===
# Regular expression to strip ANSI color codes and control sequences from logs.
# Used in main.py and logs_utils.py for log cleaning.
ANSI_ESCAPE_PATTERN: Pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
ANSI_ESCAPE_PATTERN: re.Pattern[str] = re.compile(
r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"
)

# === BROWSER SUPPORT ===
# Valid browsers for cookie extraction (yt-dlp --cookies-from-browser).
# The set version is for O(1) lookups, the list is for iteration.
SUPPORTED_BROWSERS_SET: Set[str] = {
SUPPORTED_BROWSERS_SET: set[str] = {
"brave",
"chrome",
"chromium",
Expand All @@ -46,17 +47,17 @@
"whale",
}

SUPPORTED_BROWSERS: List[str] = sorted(SUPPORTED_BROWSERS_SET)
SUPPORTED_BROWSERS: list[str] = sorted(SUPPORTED_BROWSERS_SET)

# === FILE VALIDATION ===
# Minimum size for a valid cookie file (bytes)
MIN_COOKIE_FILE_SIZE: int = 100

# Video file extensions
VIDEO_EXTENSIONS: Set[str] = {".mkv", ".mp4", ".webm", ".avi", ".mov"}
VIDEO_EXTENSIONS: set[str] = {".mkv", ".mp4", ".webm", ".avi", ".mov"}

# Subtitle file extensions
SUBTITLE_EXTENSIONS: Set[str] = {".srt", ".vtt", ".ass", ".ssa"}
SUBTITLE_EXTENSIONS: set[str] = {".srt", ".vtt", ".ass", ".ssa"}

# === DOWNLOAD PROFILE CONSTANTS ===
# Cache expiry for profile resolution (minutes)
Expand All @@ -83,13 +84,13 @@

# === PROGRESS REGEX PATTERNS ===
# Patterns for parsing yt-dlp download progress output
DOWNLOAD_PROGRESS_PATTERN: Pattern = re.compile(
DOWNLOAD_PROGRESS_PATTERN: re.Pattern[str] = re.compile(
r"\[download\]\s+(\d{1,3}\.\d+)%\s+of\s+([\d.]+\w+)\s+at\s+"
r"([\d.]+\w+/s)\s+ETA\s+(\d{2}:\d{2})"
)

FRAGMENT_PROGRESS_PATTERN: Pattern = re.compile(
FRAGMENT_PROGRESS_PATTERN: re.Pattern[str] = re.compile(
r"\[download\]\s+Got fragment\s+(\d+)\s+of\s+(\d+)"
)

GENERIC_PERCENTAGE_PATTERN: Pattern = re.compile(r"(\d{1,3}(?:\.\d+)?)%")
GENERIC_PERCENTAGE_PATTERN: re.Pattern[str] = re.compile(r"(\d{1,3}(?:\.\d+)?)%")
15 changes: 7 additions & 8 deletions app/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import shlex
from pathlib import Path
from typing import Optional, Dict, List, Tuple

from app.file_system_utils import is_valid_cookie_file

Expand All @@ -18,8 +17,8 @@ def build_base_ytdlp_command(
embed_subs: bool,
force_mp4: bool = False,
custom_args: str = "",
quality_strategy: Optional[Dict] = None,
) -> List[str]:
quality_strategy: dict | None = None,
) -> list[str]:
"""Build base yt-dlp command with common options and premium quality strategies"""

# Use premium strategy if provided
Expand Down Expand Up @@ -99,8 +98,8 @@ def build_base_ytdlp_command(


def resolve_ytdlp_argument_conflicts(
base_args: List[str], custom_args: List[str]
) -> List[str]:
base_args: list[str], custom_args: list[str]
) -> list[str]:
"""
Resolve conflicts between base yt-dlp arguments and custom arguments.
Custom arguments take precedence over base arguments.
Expand Down Expand Up @@ -228,7 +227,7 @@ def build_cookies_params(
browser_select: str = "chrome",
browser_profile: str = "",
cookies_file_path: str = "cookies/youtube_cookies.txt",
) -> List[str]:
) -> list[str]:
"""
Builds cookie parameters based on configuration.
Simplified version for testing without Streamlit dependencies.
Expand Down Expand Up @@ -258,7 +257,7 @@ def build_cookies_params(
return ["--no-cookies"]


def build_sponsorblock_params(sb_choice: str) -> List[str]:
def build_sponsorblock_params(sb_choice: str) -> list[str]:
"""
Builds yt-dlp parameters for SponsorBlock based on user choice.
Simplified version for testing without Streamlit dependencies.
Expand Down Expand Up @@ -296,7 +295,7 @@ def build_sponsorblock_params(sb_choice: str) -> List[str]:
return params


def get_sponsorblock_config(sb_choice: str) -> Tuple[List[str], List[str]]:
def get_sponsorblock_config(sb_choice: str) -> tuple[list[str], list[str]]:
"""
Returns the SponsorBlock configuration based on user choice.
Simplified version for testing without Streamlit dependencies.
Expand Down
22 changes: 11 additions & 11 deletions app/cut_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""

from pathlib import Path
from typing import Dict, List, Callable, Tuple
from collections.abc import Callable

from app.translations import t
from app.logs_utils import push_log_generic as push_log
Expand Down Expand Up @@ -108,7 +108,7 @@ def find_nearest_keyframes(
# === SEGMENT MANIPULATION ===


def merge_overlaps(segments: List[Dict], margin: float = 0.0) -> List[Dict]:
def merge_overlaps(segments: list[dict], margin: float = 0.0) -> list[dict]:
"""Merge overlapping segments (keeping main 'sponsor' category as priority)."""
segs = sorted(
[
Expand All @@ -127,8 +127,8 @@ def merge_overlaps(segments: List[Dict], margin: float = 0.0) -> List[Dict]:


def invert_segments(
segments: List[Dict], total_duration: float
) -> List[Tuple[float, float]]:
segments: list[dict], total_duration: float
) -> list[tuple[float, float]]:
"""
Returns the intervals [start,end) to keep when removing 'segments'.

Expand All @@ -152,8 +152,8 @@ def invert_segments(


def invert_segments_tuples(
segments: List[Tuple[int, int]], total_duration: int
) -> List[Tuple[int, int]]:
segments: list[tuple[int, int]], total_duration: int
) -> list[tuple[int, int]]:
"""
LEGACY: Invert segments using tuple format (for backward compatibility).

Expand Down Expand Up @@ -192,8 +192,8 @@ def invert_segments_tuples(


def build_time_remap(
segments: List[Dict], total_duration: float
) -> Tuple[Callable[[float], float], List[Tuple[float, float, float]]]:
segments: list[dict], total_duration: float
) -> tuple[Callable[[float], float], list[tuple[float, float, float]]]:
"""
Builds a mapping original_time -> time_after_cut.
Returns a `remap(t)` function + a list of cumulative jumps.
Expand Down Expand Up @@ -221,7 +221,7 @@ def remap(t: float) -> float:

def remap_interval(
start: float, end: float, remap: Callable[[float], float]
) -> Tuple[float, float]:
) -> tuple[float, float]:
"""Helper to recalculate an interval (start,end) after cutting"""
s2 = remap(start)
e2 = remap(end)
Expand All @@ -239,10 +239,10 @@ def build_cut_command(
final_tmp: Path,
actual_start: float,
duration: float,
processed_subtitle_files: List[Tuple[str, Path]],
processed_subtitle_files: list[tuple[str, Path]],
cut_output: Path,
cut_ext: str,
) -> List[str]:
) -> list[str]:
"""
Build the ffmpeg command for cutting video with subtitles.

Expand Down
16 changes: 7 additions & 9 deletions app/display_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
Functions for formatting time, durations, and other display-related utilities.
"""

from typing import Optional


def fmt_hhmmss(seconds: int) -> str:
"""
Expand All @@ -27,7 +25,7 @@ def fmt_hhmmss(seconds: int) -> str:
return f"{hours:02d}:{minutes:02d}:{secs:02d}"


def parse_time_like(time_str: str) -> Optional[int]:
def parse_time_like(time_str: str) -> int | None:
"""
Parse a time-like string and return the duration in seconds.
Accepts: "11" (sec), "0:11", "00:00:11", "1:02:03"
Expand Down Expand Up @@ -77,12 +75,12 @@ def build_info_items(
platform_emoji: str,
platform_name: str,
media_type: str,
uploader: Optional[str] = None,
duration: Optional[int] = None,
view_count: Optional[int] = None,
like_count: Optional[int] = None,
entries_count: Optional[int] = None,
first_video_title: Optional[str] = None,
uploader: str | None = None,
duration: int | None = None,
view_count: int | None = None,
like_count: int | None = None,
entries_count: int | None = None,
first_video_title: str | None = None,
) -> list:
"""
Build a list of formatted info items for display.
Expand Down
3 changes: 1 addition & 2 deletions app/file_system_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import re
import shutil
from pathlib import Path
from typing import List

import streamlit as st
from app.constants import SUPPORTED_BROWSERS_SET
Expand Down Expand Up @@ -115,7 +114,7 @@ def is_valid_browser(browser: str) -> bool:
# === DIRECTORY OPERATIONS ===


def list_subdirs_recursive(root: Path, max_depth: int = 2) -> List[str]:
def list_subdirs_recursive(root: Path, max_depth: int = 2) -> list[str]:
"""
List subdirectories recursively up to max_depth levels.
Returns paths relative to root, formatted for display.
Expand Down
8 changes: 4 additions & 4 deletions app/integrations_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Callable, Optional
from collections.abc import Callable

import requests

Expand All @@ -20,14 +20,14 @@ class JellyfinScanResult:

success: bool
message: str
status_code: Optional[int] = None
status_code: int | None = None


def trigger_jellyfin_library_scan(
base_url: str,
api_key: str,
session: Optional[requests.Session] = None,
log: Optional[Callable[[str], None]] = None,
session: requests.Session | None = None,
log: Callable[[str], None] | None = None,
) -> JellyfinScanResult:
"""
Trigger a Jellyfin library scan (all libraries).
Expand Down
16 changes: 8 additions & 8 deletions app/json_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@

import json
from pathlib import Path
from typing import Any, Dict, Optional, TypeVar, Union
from typing import Any, TypeVar

# Type alias for JSON-compatible data
JsonData = Dict[str, Any]
JsonData = dict[str, Any]
T = TypeVar("T")


def safe_load_json(
path: Union[Path, str],
default: Optional[T] = None,
path: Path | str,
default: T | None = None,
log_errors: bool = True,
) -> Optional[Union[JsonData, T]]:
) -> JsonData | T | None:
"""
Safely load JSON from a file with consistent error handling.

Expand Down Expand Up @@ -64,7 +64,7 @@ def safe_load_json(


def safe_save_json(
path: Union[Path, str],
path: Path | str,
data: JsonData,
indent: int = 2,
ensure_ascii: bool = False,
Expand Down Expand Up @@ -117,7 +117,7 @@ def safe_save_json(
return False


def json_file_exists(path: Union[Path, str]) -> bool:
def json_file_exists(path: Path | str) -> bool:
"""
Check if a JSON file exists and appears valid.

Expand All @@ -142,7 +142,7 @@ def json_file_exists(path: Union[Path, str]) -> bool:


def update_json_file(
path: Union[Path, str],
path: Path | str,
updates: JsonData,
create_if_missing: bool = True,
log_errors: bool = True,
Expand Down
Loading
Loading