From 692d3a1812a72779ab275e080a3b7506fc2072a0 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:55:43 +0100 Subject: [PATCH 01/26] Remove intro skip workflow --- README.md | 9 +- SerienJunkie/README_INTRO_DETECTION.md | 179 ----------- SerienJunkie/intro_times.json | 286 ----------------- SerienJunkie/progress.json | 18 +- SerienJunkie/s.toBot.py | 416 ++----------------------- SerienJunkie/settings.json | 3 +- 6 files changed, 22 insertions(+), 889 deletions(-) delete mode 100644 SerienJunkie/README_INTRO_DETECTION.md delete mode 100644 SerienJunkie/intro_times.json diff --git a/README.md b/README.md index ad6132e..46fd91f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # BingeWatcher Automated binge-watching helper for **s.to** and **aniworld.to** with progress -tracking, intro/end skipping, and a modern sidebar UI for quick navigation. + tracking, end-screen skipping, and a modern sidebar UI for quick navigation. > **Strict disclaimer**: I do **not** support, endorse, or encourage the use of > this script. It is published **for educational review only**. Do **not** use @@ -15,7 +15,6 @@ tracking, intro/end skipping, and a modern sidebar UI for quick navigation. - **Multi-provider support**: s.to and aniworld.to with automatic provider detection. - **Progress tracking**: resume by series/season/episode with saved timestamps. -- **Smart intro skip**: per-series intro start/end times. - **End screen skip**: jump past credits/outro if configured. - **Auto fullscreen**: multiple fallback strategies for stubborn players. - **Sidebar UI**: search, sort, quick actions, and settings panel. @@ -66,7 +65,6 @@ The script will: | --- | --- | --- | | `BW_HEADLESS` | `false` | Run Firefox headless (`true/false`). | | `BW_START_URL` | `https://s.to/` | Start URL (provider homepage). | -| `BW_INTRO_SKIP` | `80` | Default intro skip (seconds). | | `BW_MAX_RETRIES` | `3` | Navigation retry count. | | `BW_WAIT_TIMEOUT` | `25` | Page load wait timeout. | | `BW_PROGRESS_INTERVAL` | `5` | Progress save interval (seconds). | @@ -81,7 +79,6 @@ Important keys: - `useTorProxy` (boolean) - `autoFullscreen` (boolean) -- `autoSkipIntro` (boolean) - `autoSkipEndScreen` (boolean) - `autoNext` (boolean) - `playbackRate` (number) @@ -90,14 +87,13 @@ Important keys: ## Data Files - `progress.json`: persisted progress by series. -- `intro_times.json`: optional default intro windows by season. - `settings.json`: app settings. ## Sidebar Highlights - **Series list** with last watched time. - **Provider tabs** to filter s.to vs. aniworld.to. -- **Per-series controls** for intro and end skip windows. +- **Per-series controls** for end skip windows. - **Quick actions**: skip episode, open settings, quit. ## Troubleshooting @@ -115,7 +111,6 @@ SerienJunkie/ ├── README.md # This file ├── geckodriver.exe # Firefox WebDriver ├── progress.json # Progress database (auto-created) -├── intro_times.json # Optional intro presets └── user.BingeWatcher/ # Firefox profile (auto-created) ``` diff --git a/SerienJunkie/README_INTRO_DETECTION.md b/SerienJunkie/README_INTRO_DETECTION.md deleted file mode 100644 index 9405e3d..0000000 --- a/SerienJunkie/README_INTRO_DETECTION.md +++ /dev/null @@ -1,179 +0,0 @@ -# Smart Intro Detection System - -## Overview - -The BingeWatcher now includes a smart intro detection system that automatically detects when an anime intro is playing and only skips it when appropriate. This prevents skipping during recaps, episode previews, or episodes without intros. - -## Features - -### 1. Smart Intro Detection -- **Time-based detection**: Uses predefined intro time windows for each anime series -- **Pattern-based detection**: Looks for intro-related keywords in the page content -- **Season-specific data**: Different intro times for different seasons of the same anime -- **Fallback protection**: Only skips when confident an intro is playing - -### 2. Dual Input System -- **Start time**: When the intro typically begins (e.g., 85 seconds) -- **End time**: When the intro typically ends (e.g., 145 seconds) -- **Individual control**: Users can adjust both start and end times per series - -### 3. Comprehensive Database -The system includes intro times for popular anime series: - -#### One Piece (All Seasons) -- **Start**: 85 seconds -- **End**: 145 seconds -- **Detection patterns**: "opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat" - -#### Naruto (All Seasons) -- **Start**: 80 seconds -- **End**: 140 seconds -- **Detection patterns**: "opening", "intro", "naruto", "believe it", "ninja", "hidden leaf" - -#### One Punch Man (Seasons 1-2) -- **Start**: 85 seconds -- **End**: 145 seconds -- **Detection patterns**: "opening", "intro", "one punch", "hero", "saitama", "bald" - -#### Jujutsu Kaisen (Seasons 1-2) -- **Start**: 85 seconds -- **End**: 145 seconds -- **Detection patterns**: "opening", "intro", "jujutsu", "kaisen", "curse", "sorcerer", "gojo" - -#### Attack on Titan (Seasons 1-4) -- **Start**: 80 seconds -- **End**: 140 seconds -- **Detection patterns**: "opening", "intro", "attack", "titan", "shingeki", "eren", "mikasa", "levi" - -#### Dandadan (Season 1) -- **Start**: 85 seconds -- **End**: 145 seconds -- **Detection patterns**: "opening", "intro", "dandadan", "supernatural", "alien", "ghost", "momo" - -#### Kaiju No. 8 (Season 1) -- **Start**: 85 seconds -- **End**: 145 seconds -- **Detection patterns**: "opening", "intro", "kaiju", "monster", "defense force", "kafka" - -## How It Works - -### 1. Detection Process -1. **Time Check**: Verifies if current video time is within the intro window -2. **Pattern Analysis**: Searches page content for intro-related keywords -3. **Confidence Assessment**: Only skips if both time and pattern conditions are met -4. **Safe Skip**: Jumps to the end time of the intro - -### 2. User Interface -- **Dual Input Fields**: Separate "Start" and "End" time inputs for each series -- **Real-time Updates**: Changes are applied immediately -- **Visual Feedback**: Clear indication of current intro skip settings - -### 3. Configuration Files - -#### `intro_times.json` -Contains the master database of intro times and detection patterns: -```json -{ - "series-name": { - "name": "Display Name", - "intros": [ - { - "season": 1, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "keywords"] - } - ], - "default_skip_start": 85, - "default_skip_end": 145 - } -} -``` - -#### `progress.json` -Stores user-specific intro skip times: -```json -{ - "series-name": { - "intro_skip_start": 85, - "intro_skip_end": 145 - } -} -``` - -## Usage - -### Automatic Detection -The system automatically detects and skips intros when: -- Auto-skip intro is enabled in settings -- Current video time is within the intro window -- Intro-related patterns are detected in the page - -### Manual Adjustment -1. Open the BingeWatcher sidebar -2. Find your series in the list -3. Adjust the "Start" and "End" time inputs -4. Changes are applied automatically - -### Adding New Series -To add intro times for a new series: - -1. **Edit `intro_times.json`**: - ```json - { - "new-series": { - "name": "New Series Name", - "intros": [ - { - "season": 1, - "start_time": 90, - "end_time": 150, - "detection_patterns": ["opening", "intro", "series-specific-keywords"] - } - ], - "default_skip_start": 90, - "default_skip_end": 150 - } - } - ``` - -2. **Add to `progress.json`** (optional, will use defaults if not present): - ```json - { - "new-series": { - "intro_skip_start": 90, - "intro_skip_end": 150 - } - } - ``` - -## Benefits - -1. **Prevents False Skips**: Won't skip during recaps or episode previews -2. **Season Awareness**: Handles different intro times per season -3. **User Customization**: Individual control over skip times -4. **Smart Fallback**: Uses pattern detection as backup to time-based detection -5. **Easy Maintenance**: Centralized database for intro times - -## Technical Details - -### Detection Algorithm -```python -def detect_intro_start(driver, series: str, season: int = 1) -> bool: - # 1. Get current video time - # 2. Check if within intro time window - # 3. Search page for detection patterns - # 4. Return True if both conditions met -``` - -### Smart Skip Function -```python -def smart_skip_intro(driver, series: str, season: int = 1): - # 1. Wait for video to be ready - # 2. Get intro times from database - # 3. Detect if intro is playing - # 4. Skip to end time if detected - # 5. Fall back to simple skip if detection fails -``` - -This system provides a much more intelligent and user-friendly approach to intro skipping, ensuring that users only skip actual intros and not other content. diff --git a/SerienJunkie/intro_times.json b/SerienJunkie/intro_times.json deleted file mode 100644 index dddaec8..0000000 --- a/SerienJunkie/intro_times.json +++ /dev/null @@ -1,286 +0,0 @@ -{ - "one-piece": { - "name": "One Piece", - "intros": [ - { - "season": 1, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 2, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 3, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 4, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 5, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 6, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 7, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 8, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 9, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 10, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 11, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 12, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 13, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 14, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 15, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 16, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 17, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 18, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 19, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - }, - { - "season": 20, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "we are", "yo ho ho", "mugiwara", "straw hat", "one piece", "pirate", "luffy", "nakama", "treasure", "grand line", "devil fruit", "gomu gomu", "rubber", "crew", "ship", "going merry", "thousand sunny"] - } - ], - "default_skip_start": 85, - "default_skip_end": 145 - }, - "naruto": { - "name": "Naruto", - "intros": [ - { - "season": 1, - "start_time": 80, - "end_time": 140, - "detection_patterns": ["opening", "intro", "naruto", "believe it", "ninja", "hidden leaf", "konoha", "uzumaki", "sasuke", "sakura", "kakashi", "team 7", "chakra", "jutsu", "ninja way", "dattebayo", "hokage", "village", "shinobi", "ninjutsu"] - }, - { - "season": 2, - "start_time": 80, - "end_time": 140, - "detection_patterns": ["opening", "intro", "naruto", "believe it", "ninja", "hidden leaf", "konoha", "uzumaki", "sasuke", "sakura", "kakashi", "team 7", "chakra", "jutsu", "ninja way", "dattebayo", "hokage", "village", "shinobi", "ninjutsu"] - }, - { - "season": 3, - "start_time": 80, - "end_time": 140, - "detection_patterns": ["opening", "intro", "naruto", "believe it", "ninja", "hidden leaf", "konoha", "uzumaki", "sasuke", "sakura", "kakashi", "team 7", "chakra", "jutsu", "ninja way", "dattebayo", "hokage", "village", "shinobi", "ninjutsu"] - }, - { - "season": 4, - "start_time": 80, - "end_time": 140, - "detection_patterns": ["opening", "intro", "naruto", "believe it", "ninja", "hidden leaf", "konoha", "uzumaki", "sasuke", "sakura", "kakashi", "team 7", "chakra", "jutsu", "ninja way", "dattebayo", "hokage", "village", "shinobi", "ninjutsu"] - }, - { - "season": 5, - "start_time": 80, - "end_time": 140, - "detection_patterns": ["opening", "intro", "naruto", "believe it", "ninja", "hidden leaf", "konoha", "uzumaki", "sasuke", "sakura", "kakashi", "team 7", "chakra", "jutsu", "ninja way", "dattebayo", "hokage", "village", "shinobi", "ninjutsu"] - } - ], - "default_skip_start": 80, - "default_skip_end": 140 - }, - "one-punch-man": { - "name": "One Punch Man", - "intros": [ - { - "season": 1, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "one punch", "hero", "saitama", "bald", "genos", "hero association", "monster", "cyborg", "superhero", "punch", "serious", "hero hunter", "garou", "class s", "class a", "class b", "class c"] - }, - { - "season": 2, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "one punch", "hero", "saitama", "bald", "genos", "hero association", "monster", "cyborg", "superhero", "punch", "serious", "hero hunter", "garou", "class s", "class a", "class b", "class c"] - } - ], - "default_skip_start": 85, - "default_skip_end": 145 - }, - "jujutsu-kaisen": { - "name": "Jujutsu Kaisen", - "intros": [ - { - "season": 1, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "jujutsu", "kaisen", "curse", "sorcerer", "gojo", "itadori", "megumi", "nobara", "sukuna", "finger", "curse technique", "domain expansion", "shibuya", "kyoto", "tokyo", "jujutsu high", "curse energy", "reverse cursed technique"] - }, - { - "season": 2, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "jujutsu", "kaisen", "curse", "sorcerer", "gojo", "itadori", "megumi", "nobara", "sukuna", "finger", "curse technique", "domain expansion", "shibuya", "kyoto", "tokyo", "jujutsu high", "curse energy", "reverse cursed technique"] - } - ], - "default_skip_start": 85, - "default_skip_end": 145 - }, - "attack-on-titan": { - "name": "Attack on Titan", - "intros": [ - { - "season": 1, - "start_time": 80, - "end_time": 140, - "detection_patterns": ["opening", "intro", "attack", "titan", "shingeki", "eren", "mikasa", "levi", "survey corps", "scout regiment", "wall maria", "wall rose", "wall sina", "trost", "shiganshina", "paradis", "marley", "yeager", "ackerman", "titan shifter", "colossal", "armored", "beast", "cart", "war hammer", "jaw", "female", "attack titan", "founding titan"] - }, - { - "season": 2, - "start_time": 80, - "end_time": 140, - "detection_patterns": ["opening", "intro", "attack", "titan", "shingeki", "eren", "mikasa", "levi", "survey corps", "scout regiment", "wall maria", "wall rose", "wall sina", "trost", "shiganshina", "paradis", "marley", "yeager", "ackerman", "titan shifter", "colossal", "armored", "beast", "cart", "war hammer", "jaw", "female", "attack titan", "founding titan"] - }, - { - "season": 3, - "start_time": 80, - "end_time": 140, - "detection_patterns": ["opening", "intro", "attack", "titan", "shingeki", "eren", "mikasa", "levi", "survey corps", "scout regiment", "wall maria", "wall rose", "wall sina", "trost", "shiganshina", "paradis", "marley", "yeager", "ackerman", "titan shifter", "colossal", "armored", "beast", "cart", "war hammer", "jaw", "female", "attack titan", "founding titan"] - }, - { - "season": 4, - "start_time": 80, - "end_time": 140, - "detection_patterns": ["opening", "intro", "attack", "titan", "shingeki", "eren", "mikasa", "levi", "survey corps", "scout regiment", "wall maria", "wall rose", "wall sina", "trost", "shiganshina", "paradis", "marley", "yeager", "ackerman", "titan shifter", "colossal", "armored", "beast", "cart", "war hammer", "jaw", "female", "attack titan", "founding titan"] - } - ], - "default_skip_start": 80, - "default_skip_end": 140 - }, - "demon-slayer-kimetsu-no-yaiba": { - "name": "Demon Slayer: Kimetsu no Yaiba", - "intros": [ - { - "season": 1, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "demon slayer", "kimetsu", "yaiba", "tanjiro", "nezuko", "zenitsu", "inosuke", "demon", "slayer", "hashira", "muzan", "kibutsuji", "breathing", "water breathing", "thunder breathing", "wind breathing", "flame breathing", "mist breathing", "love breathing", "sound breathing", "serpent breathing", "stone breathing", "insect breathing", "nichirin", "sword", "corps", "butterfly mansion", "final selection", "mount natagumo", "entertainment district", "swordsmith village", "hashira training", "infinity castle", "sunrise countdown"] - }, - { - "season": 2, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "demon slayer", "kimetsu", "yaiba", "tanjiro", "nezuko", "zenitsu", "inosuke", "demon", "slayer", "hashira", "muzan", "kibutsuji", "breathing", "water breathing", "thunder breathing", "wind breathing", "flame breathing", "mist breathing", "love breathing", "sound breathing", "serpent breathing", "stone breathing", "insect breathing", "nichirin", "sword", "corps", "butterfly mansion", "final selection", "mount natagumo", "entertainment district", "swordsmith village", "hashira training", "infinity castle", "sunrise countdown"] - }, - { - "season": 3, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "demon slayer", "kimetsu", "yaiba", "tanjiro", "nezuko", "zenitsu", "inosuke", "demon", "slayer", "hashira", "muzan", "kibutsuji", "breathing", "water breathing", "thunder breathing", "wind breathing", "flame breathing", "mist breathing", "love breathing", "sound breathing", "serpent breathing", "stone breathing", "insect breathing", "nichirin", "sword", "corps", "butterfly mansion", "final selection", "mount natagumo", "entertainment district", "swordsmith village", "hashira training", "infinity castle", "sunrise countdown"] - } - ], - "default_skip_start": 85, - "default_skip_end": 145 - }, - "dan-da-dan": { - "name": "Dandadan", - "intros": [ - { - "season": 1, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "dandadan", "supernatural", "alien", "ghost", "momo", "okarun", "turbo granny", "serpo", "yokai", "spirit", "ufo", "extraterrestrial", "paranormal", "occult", "psychic", "medium", "exorcist", "supernatural power", "alien invasion", "ghost hunting", "spirit world", "otherworldly", "mysterious", "supernatural phenomenon"] - } - ], - "default_skip_start": 85, - "default_skip_end": 145 - }, - "kaiju-no-8": { - "name": "Kaiju No. 8", - "intros": [ - { - "season": 1, - "start_time": 85, - "end_time": 145, - "detection_patterns": ["opening", "intro", "kaiju", "monster", "defense force", "kafka", "hibino", "ichikawa", "mina", "ashiro", "japan", "tokyo", "monster attack", "kaiju attack", "defense corps", "monster hunter", "kaiju hunter", "superhuman", "power suit", "monster weapon", "kaiju weapon", "defense technology", "monster technology", "kaiju technology", "superhuman strength", "monster strength", "kaiju strength", "defense mission", "monster mission", "kaiju mission"] - } - ], - "default_skip_start": 85, - "default_skip_end": 145 - } -} diff --git a/SerienJunkie/progress.json b/SerienJunkie/progress.json index b03828f..48942a9 100644 --- a/SerienJunkie/progress.json +++ b/SerienJunkie/progress.json @@ -5,8 +5,6 @@ "position": 0, "timestamp": 0, "provider": "s.to", - "intro_skip_start": 80, - "intro_skip_end": 140, "end_skip": 0 }, "naruto": { @@ -15,8 +13,6 @@ "position": 0, "timestamp": 0, "provider": "s.to", - "intro_skip_start": 80, - "intro_skip_end": 140, "end_skip": 0 }, "one-punch-man": { @@ -25,8 +21,6 @@ "position": 0, "timestamp": 0, "provider": "aniworld.to", - "intro_skip_start": 85, - "intro_skip_end": 145, "end_skip": 0 }, "jujutsu-kaisen": { @@ -35,8 +29,6 @@ "position": 0, "timestamp": 0, "provider": "aniworld.to", - "intro_skip_start": 85, - "intro_skip_end": 145, "end_skip": 0 }, "attack-on-titan": { @@ -45,8 +37,6 @@ "position": 0, "timestamp": 0, "provider": "aniworld.to", - "intro_skip_start": 80, - "intro_skip_end": 140, "end_skip": 0 }, "demon-slayer-kimetsu-no-yaiba": { @@ -55,8 +45,6 @@ "position": 0, "timestamp": 0, "provider": "aniworld.to", - "intro_skip_start": 85, - "intro_skip_end": 145, "end_skip": 0 }, "dan-da-dan": { @@ -65,8 +53,6 @@ "position": 0, "timestamp": 0, "provider": "aniworld.to", - "intro_skip_start": 85, - "intro_skip_end": 145, "end_skip": 0 }, "kaiju-no-8": { @@ -75,8 +61,6 @@ "position": 0, "timestamp": 0, "provider": "aniworld.to", - "intro_skip_start": 85, - "intro_skip_end": 145, "end_skip": 0 } -} \ No newline at end of file +} diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index f258545..1173ebd 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -21,7 +21,6 @@ # === CONFIGURATION === HEADLESS: bool = os.getenv("BW_HEADLESS", "false").lower() in {"1", "true", "yes"} START_URL: str = os.getenv("BW_START_URL", "https://s.to/") -INTRO_SKIP_SECONDS: int = int(os.getenv("BW_INTRO_SKIP", "80")) MAX_RETRIES: int = int(os.getenv("BW_MAX_RETRIES", "3")) WAIT_TIMEOUT: int = int(os.getenv("BW_WAIT_TIMEOUT", "25")) PROGRESS_SAVE_INTERVAL: int = int(os.getenv("BW_PROGRESS_INTERVAL", "5")) @@ -144,156 +143,6 @@ def handle_list_item_deletion(name: str) -> bool: return False -def get_intro_skip_seconds(series: str) -> int: - try: - data = load_progress().get(series, {}) - val = int(data.get("intro_skip_start", INTRO_SKIP_SECONDS)) - return max(0, val) - except Exception: - return INTRO_SKIP_SECONDS - - -def get_intro_skip_end_seconds(series: str) -> int: - try: - data = load_progress().get(series, {}) - val = int(data.get("intro_skip_end", INTRO_SKIP_SECONDS + 60)) - return max(0, val) - except Exception: - return INTRO_SKIP_SECONDS + 60 - - -def set_intro_skip_seconds(series: str, start_seconds: int, end_seconds: int = None) -> bool: - try: - start_seconds = max(0, int(start_seconds)) - if end_seconds is None: - end_seconds = start_seconds + 60 - end_seconds = max(0, int(end_seconds)) - - db = load_progress() - entry = db.get(series, {}) if isinstance(db.get(series, {}), dict) else {} - entry["intro_skip_start"] = start_seconds - entry["intro_skip_end"] = end_seconds - db[series] = entry - with open(PROGRESS_DB_FILE, "w", encoding="utf-8") as f: - json.dump(db, f, indent=2, ensure_ascii=False) - return True - except Exception as e: - logging.error(f"Intro time could not be saved: {e}") - return False - - -def load_intro_times() -> Dict[str, Any]: - """Load intro times from intro_times.json""" - try: - intro_times_file = os.path.join(SCRIPT_DIR, "intro_times.json") - if os.path.exists(intro_times_file): - with open(intro_times_file, "r", encoding="utf-8") as f: - return json.load(f) - return {} - except Exception as e: - logging.error(f"Could not load intro times: {e}") - return {} - - -def get_default_intro_times(series: str, season: int = 1) -> tuple[int, int]: - """Get default intro times for a series and season""" - try: - intro_times = load_intro_times() - series_data = intro_times.get(series, {}) - - # Try to find specific season data - for intro in series_data.get("intros", []): - if intro.get("season") == season: - return intro.get("start_time", 90), intro.get("end_time", 150) - - # Fall back to default times - return series_data.get("default_skip_start", 90), series_data.get("default_skip_end", 150) - except Exception: - return 90, 150 - - -def detect_intro_start(driver, series: str, season: int = 1) -> bool: - """Detect if an intro is currently playing""" - try: - intro_times = load_intro_times() - series_data = intro_times.get(series, {}) - - # Get current video time - current_time = driver.execute_script("return document.querySelector('video')?.currentTime || 0;") - - # Check if we're in the intro time window - for intro in series_data.get("intros", []): - if intro.get("season") == season: - start_time = intro.get("start_time", 90) - end_time = intro.get("end_time", 150) - - if start_time <= current_time <= end_time: - # Additional detection patterns - detection_patterns = intro.get("detection_patterns", []) - - # Check for intro indicators in the page - page_text = driver.execute_script("return document.body.innerText.toLowerCase();") - - for pattern in detection_patterns: - if pattern.lower() in page_text: - return True - - # If we're in the time window and no specific patterns found, assume it's an intro - return True - - return False - except Exception as e: - logging.error(f"Error detecting intro: {e}") - return False - - -def smart_skip_intro(driver, series: str, season: int = 1): - """Smart intro skipping that only skips when an intro is detected""" - try: - # Wait for video to be ready - WebDriverWait(driver, 15).until( - lambda d: d.execute_script( - "return document.querySelector('video')?.readyState > 0;" - ) - ) - - progress_entry = load_progress().get(series, {}) - has_custom_intro = ( - "intro_skip_start" in progress_entry - or "intro_skip_end" in progress_entry - ) - if has_custom_intro: - intro_start = get_intro_skip_seconds(series) - intro_end = get_intro_skip_end_seconds(series) - if intro_end > intro_start: - current_time = driver.execute_script( - "return document.querySelector('video')?.currentTime || 0;" - ) - if intro_start <= current_time <= intro_end: - driver.execute_script( - "document.querySelector('video').currentTime = arguments[0];", - intro_end, - ) - return - - # Get intro times - intro_start, intro_end = get_default_intro_times(series, season) - - # Check if we should skip intro - if detect_intro_start(driver, series, season): - logging.info(f"Intro detected for {series}, skipping to {intro_end} seconds") - driver.execute_script( - "document.querySelector('video').currentTime = arguments[0];", - intro_end, - ) - else: - logging.info(f"No intro detected for {series}, continuing normally") - - except Exception as e: - logging.error(f"Error in smart intro skip: {e}") - # Fall back to simple skip - skip_intro(driver, get_intro_skip_seconds(series)) - def get_end_skip_seconds(series: str) -> int: try: @@ -629,7 +478,6 @@ def get_settings(driver: webdriver.Firefox) -> Dict[str, Any]: merged = {**file_s, **ls_s} merged["autoFullscreen"] = bool(merged.get("autoFullscreen", True)) - merged["autoSkipIntro"] = bool(merged.get("autoSkipIntro", True)) merged["autoSkipEndScreen"] = bool(merged.get("autoSkipEndScreen", True)) merged["autoNext"] = bool(merged.get("autoNext", True)) merged["playbackRate"] = float(merged.get("playbackRate", 1)) @@ -696,7 +544,6 @@ def sync_settings_to_localstorage(driver: webdriver.Firefox) -> None: def _default_settings() -> Dict[str, Any]: return { "autoFullscreen": True, - "autoSkipIntro": True, "autoSkipEndScreen": False, "autoNext": True, "playbackRate": 1.0, @@ -1024,7 +871,6 @@ def play_episodes_loop( db = load_progress() settings = get_settings(driver) auto_fs = settings["autoFullscreen"] - auto_skip = settings["autoSkipIntro"] auto_skip_end = settings["autoSkipEndScreen"] auto_next = settings["autoNext"] rate = settings["playbackRate"] @@ -1068,9 +914,7 @@ def play_episodes_loop( apply_media_settings(driver, rate, vol) if position and position > 0: - skip_intro(driver, position) - elif auto_skip: - smart_skip_intro(driver, series, current_season) + seek_to_position(driver, position) position = 0 recovery_tries = 0 @@ -1105,29 +949,6 @@ def play_episodes_loop( ensure_video_context(driver) play_video(driver) - try: - const_ser = series - const_secs = get_intro_skip_seconds(const_ser) - const_secs_end = get_intro_skip_end_seconds(const_ser) - driver.execute_script( - """ - const v = document.querySelector('video'); - const secs = arguments[0]; - const secsEnd = arguments[1]; - if (!v || !isFinite(v.duration)) return; - if (secsEnd <= secs || secsEnd >= (v.duration - 1)) return; - const currentTime = v.currentTime; - if (currentTime >= secs && currentTime <= secsEnd) { - v.currentTime = secsEnd; - try { v.play().catch(()=>{}); } catch(_) {} - } - """, - const_secs, - const_secs_end, - ) - except Exception: - pass - if auto_fs and not HEADLESS: _hide_sidebar(driver, True) ensure_video_context(driver) @@ -1290,7 +1111,6 @@ def play_episodes_loop( # Lokale Variablen MERGEN auto_fs = bool(upd.get("autoFullscreen", auto_fs)) - auto_skip = bool(upd.get("autoSkipIntro", auto_skip)) auto_skip_end = bool(upd.get("autoSkipEndScreen", auto_skip_end)) auto_next = bool(upd.get("autoNext", auto_next)) rate = float(upd.get("playbackRate", rate)) @@ -1300,7 +1120,6 @@ def play_episodes_loop( settings.update( { "autoFullscreen": auto_fs, - "autoSkipIntro": auto_skip, "autoSkipEndScreen": auto_skip_end, "autoNext": auto_next, "playbackRate": rate, @@ -1360,44 +1179,6 @@ def play_episodes_loop( # --- LIVE SERIES SKIP UPDATES ---------------------------------- try: skip_settings_changed = False - upd = read_localstorage_value(driver, "bw_intro_start_update") - if upd: - data = json.loads(upd) - ser_raw = data.get("series", "") - secs_raw = data.get("seconds", 0) - ser = norm_series_key(ser_raw) - try: - secs = max(0, int(float(secs_raw))) - except Exception: - secs = 0 - - if ser: - current_end = get_intro_skip_end_seconds(ser) - if set_intro_skip_seconds(ser, secs, current_end): - skip_settings_changed = True - except Exception: - pass - - try: - upd = read_localstorage_value(driver, "bw_intro_end_update") - if upd: - data = json.loads(upd) - ser_raw = data.get("series", "") - secs_raw = data.get("seconds", 0) - ser = norm_series_key(ser_raw) - try: - secs = max(0, int(float(secs_raw))) - except Exception: - secs = 0 - - if ser: - current_start = get_intro_skip_seconds(ser) - if set_intro_skip_seconds(ser, current_start, secs): - skip_settings_changed = True - except Exception: - pass - - try: upd = read_localstorage_value(driver, "bw_end_update") if upd: data = json.loads(upd) @@ -1535,7 +1316,7 @@ def play_episodes_loop( time.sleep(1.0) if auto_nav: - position = get_intro_skip_seconds(series) if auto_skip else 0 + position = 0 continue exit_fullscreen(driver) @@ -1561,7 +1342,7 @@ def play_episodes_loop( if not next_episode: return current_season, current_episode = next_episode - position = get_intro_skip_seconds(series) if auto_skip else 0 + position = 0 continue # Spezielle Behandlung für One Piece: Verhindere Sprung zu Staffel 11 @@ -1622,7 +1403,7 @@ def play_episodes_loop( # Normale Episode-Inkrementierung für andere Serien current_episode += 1 - position = get_intro_skip_seconds(series) if auto_skip else 0 + position = 0 continue @@ -2288,13 +2069,16 @@ def apply_media_settings(driver, rate, vol): pass -def skip_intro(driver, seconds): +def seek_to_position(driver, seconds: int) -> None: WebDriverWait(driver, 15).until( lambda d: d.execute_script( "return document.querySelector('video')?.readyState > 0;" ) ) - driver.execute_script(f"document.querySelector('video').currentTime = {seconds};") + driver.execute_script( + "document.querySelector('video').currentTime = arguments[0];", + int(seconds), + ) def get_current_position(driver): @@ -2382,9 +2166,6 @@ def build_items_html(db: Dict[str, Dict[str, Any]], settings: Optional[Dict[str, """Erstellt HTML für die Sidebar mit Streaming-Anbieter-Tabs.""" if settings is None: settings = {} - auto_skip_intro = settings.get("autoSkipIntro", True) - auto_skip_end = settings.get("autoSkipEndScreen", False) - # Gruppiere Serien nach Anbietern provider_series = {} for series_name, data in db.items(): @@ -2423,14 +2204,12 @@ def build_items_html(db: Dict[str, Dict[str, Any]], settings: Optional[Dict[str, episode = int(data.get("episode", 1)) position = int(data.get("position", 0)) ts_val = float(data.get("timestamp", 0)) - intro_val = int(data.get("intro_skip_start", INTRO_SKIP_SECONDS)) - intro_end_val = int(data.get("intro_skip_end", INTRO_SKIP_SECONDS + 60)) end_skip_val = int(data.get("end_skip", 0)) safe_name = _html.escape(series_name, quote=True) series_items.append(f"""
@@ -2446,9 +2225,9 @@ def build_items_html(db: Dict[str, Dict[str, Any]], settings: Optional[Dict[str,
X
-
@@ -2692,38 +2471,12 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> box-shadow: 0 4px 12px rgba(59,130,246,.15); } - #bingeSidebar .bw-intro-start, - #bingeSidebar .bw-intro-end, #bingeSidebar .bw-end { transition: all .2s ease !important; } - - #bingeSidebar .bw-intro-start:focus, - #bingeSidebar .bw-intro-end:focus, + #bingeSidebar .bw-end:focus { transform: scale(1.02); - box-shadow: 0 0 0 2px rgba(59,130,246,.3); - border-color: rgba(59,130,246,.6) !important; - } - - #bingeSidebar .bw-intro-start:hover, - #bingeSidebar .bw-intro-end:hover, - #bingeSidebar .bw-end:hover { - border-color: rgba(59,130,246,.5) !important; - background: rgba(59,130,246,.15) !important; - } - - #bingeSidebar .bw-intro-end:focus { - box-shadow: 0 0 0 2px rgba(139,92,246,.3); - border-color: rgba(139,92,246,.6) !important; - } - - #bingeSidebar .bw-intro-end:hover { - border-color: rgba(139,92,246,.5) !important; - background: rgba(139,92,246,.15) !important; - } - - #bingeSidebar .bw-end:focus { box-shadow: 0 0 0 2px rgba(239,68,68,.3); border-color: rgba(239,68,68,.6) !important; } @@ -2734,12 +2487,10 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> } /* Input field animations */ - #bingeSidebar .bw-intro-section, #bingeSidebar .bw-end-section { transition: all .2s ease; } - - #bingeSidebar .bw-intro-section:hover, + #bingeSidebar .bw-end-section:hover { transform: translateX(2px); } @@ -2964,7 +2715,7 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> d.addEventListener('input', (e)=>{ if (e.target && e.target.id==='bwSearch') onFilter(); }); d.addEventListener('change', (e)=>{ if (e.target && e.target.id==='bwSort') onSort(); }); - const openSeriesSkipPanel = (seriesName, introStart, introEnd, endSkip) => { + const openSeriesSkipPanel = (seriesName, endSkip) => { const existingPanel = document.getElementById('bwSeriesSkipPanel'); if (existingPanel) { existingPanel.remove(); @@ -2974,7 +2725,6 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> try { settingsData = JSON.parse(localStorage.getItem('bw_settings') || '{}'); } catch(_) {} - const allowIntro = settingsData.autoSkipIntro !== false; const allowEnd = settingsData.autoSkipEndScreen === true; const panel = document.createElement('div'); @@ -2993,29 +2743,6 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> boxShadow: '0 10px 30px rgba(0,0,0,.4)', }); - const introSectionHtml = allowIntro ? ` -
-
-
>
- Intro Skip -
-
-
- - -
-
- - -
-
-
- ` : ''; - const endSectionHtml = allowEnd ? `
@@ -3040,7 +2767,6 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) ->
- ${introSectionHtml} ${endSectionHtml}
`; @@ -3086,28 +2812,6 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> if (!window.__bwDebouncers) window.__bwDebouncers = Object.create(null); panel.addEventListener('input', (ev) => { - const introInput = ev.target.closest && ev.target.closest('input.bw-intro-start'); - if (introInput) { - const series = introInput.dataset.series; if (!series) return; - const key = '__deb_intro_start_' + series; - if (window.__bwDebouncers[key]) clearTimeout(window.__bwDebouncers[key]); - window.__bwDebouncers[key] = setTimeout(() => { - const seconds = parseInt(introInput.value || '0', 10) || 0; - localStorage.setItem('bw_intro_start_update', JSON.stringify({ series, seconds })); - }, 600); - } - - const introEndInput = ev.target.closest && ev.target.closest('input.bw-intro-end'); - if (introEndInput) { - const series = introEndInput.dataset.series; if (!series) return; - const key = '__deb_intro_end_' + series; - if (window.__bwDebouncers[key]) clearTimeout(window.__bwDebouncers[key]); - window.__bwDebouncers[key] = setTimeout(() => { - const seconds = parseInt(introEndInput.value || '0', 10) || 0; - localStorage.setItem('bw_intro_end_update', JSON.stringify({ series, seconds })); - }, 600); - } - const endInput = ev.target.closest && ev.target.closest('input.bw-end'); if (endInput) { const series = endInput.dataset.series; if (!series) return; @@ -3138,9 +2842,6 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> - @@ -3271,7 +2972,6 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> e.stopPropagation(); const next = { autoFullscreen: !!document.getElementById('bwOptAutoFullscreen')?.checked, - autoSkipIntro: !!document.getElementById('bwOptAutoSkipIntro')?.checked, autoSkipEndScreen: !!document.getElementById('bwOptAutoSkipEndScreen')?.checked, autoNext: !!document.getElementById('bwOptAutoNext')?.checked, playbackRate: parseFloat(document.getElementById('bwOptPlaybackRate')?.value || '1'), @@ -3292,7 +2992,6 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> const s = JSON.parse(localStorage.getItem('bw_settings')||'{}'); const x = id => document.getElementById(id); if (x('bwOptAutoFullscreen')) x('bwOptAutoFullscreen').checked = (s.autoFullscreen !== false); - if (x('bwOptAutoSkipIntro')) x('bwOptAutoSkipIntro').checked = (s.autoSkipIntro !== false); if (x('bwOptAutoSkipEndScreen')) x('bwOptAutoSkipEndScreen').checked = (s.autoSkipEndScreen !== false); if (x('bwOptAutoNext')) x('bwOptAutoNext').checked = (s.autoNext !== false); if (x('bwOptPlaybackRate')) x('bwOptPlaybackRate').value = String(s.playbackRate ?? 1); @@ -3327,11 +3026,9 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> const skipPanelButton = c('.bw-series-settings'); if (skipPanelButton) { const seriesName = skipPanelButton.getAttribute('data-series'); - const introStart = parseInt(skipPanelButton.getAttribute('data-intro-start') || '0', 10) || 0; - const introEnd = parseInt(skipPanelButton.getAttribute('data-intro-end') || '0', 10) || 0; const endSkip = parseInt(skipPanelButton.getAttribute('data-end-skip') || '0', 10) || 0; if (seriesName) { - openSeriesSkipPanel(seriesName, introStart, introEnd, endSkip); + openSeriesSkipPanel(seriesName, endSkip); } return; } @@ -3339,11 +3036,10 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> const item = c('.bw-series-item'); if (item) { // Check if click originated from input field or delete button - don't trigger navigation - const clickedInput = e.target.closest && (e.target.closest('input.bw-intro-start') || e.target.closest('input.bw-intro-end')); const clickedEndInput = e.target.closest && e.target.closest('input.bw-end'); const clickedDelete = e.target.closest && e.target.closest('.bw-delete'); const clickedSkipSettings = e.target.closest && e.target.closest('.bw-series-settings'); - if (clickedInput || clickedEndInput || clickedDelete || clickedSkipSettings) return; + if (clickedEndInput || clickedDelete || clickedSkipSettings) return; if (localStorage.getItem('bw_nav_inflight') === '1') return; // throttle localStorage.setItem('bw_nav_inflight','1'); @@ -3370,31 +3066,9 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> } }); - // Debounce für Intro-Input und End-Input + // Debounce für End-Input if (!window.__bwDebouncers) window.__bwDebouncers = Object.create(null); d.addEventListener('input', (e)=>{ - const inp = e.target.closest && e.target.closest('input.bw-intro-start'); - if (inp) { - const series = inp.dataset.series; if (!series) return; - const key = '__deb_intro_start_' + series; - if (window.__bwDebouncers[key]) clearTimeout(window.__bwDebouncers[key]); - window.__bwDebouncers[key] = setTimeout(()=>{ - const seconds = parseInt(inp.value||'0',10)||0; - localStorage.setItem('bw_intro_start_update', JSON.stringify({series, seconds})); - }, 600); - } - - const inpEnd = e.target.closest && e.target.closest('input.bw-intro-end'); - if (inpEnd) { - const series = inpEnd.dataset.series; if (!series) return; - const key = '__deb_intro_end_' + series; - if (window.__bwDebouncers[key]) clearTimeout(window.__bwDebouncers[key]); - window.__bwDebouncers[key] = setTimeout(()=>{ - const seconds = parseInt(inpEnd.value||'0',10)||0; - localStorage.setItem('bw_intro_end_update', JSON.stringify({series, seconds})); - }, 600); - } - const endInp = e.target.closest && e.target.closest('input.bw-end'); if (endInp) { const series = endInp.dataset.series; if (!series) return; @@ -3595,60 +3269,6 @@ def main() -> None: except Exception: pass - # Handle intro start updates (from sidebar input) – normalisieren + live anwenden - try: - upd = read_localstorage_value(driver, "bw_intro_start_update") - if upd: - data = json.loads(upd) - ser_raw = data.get("series", "") - secs_raw = data.get("seconds", 0) - ser = norm_series_key(ser_raw) - try: - secs = max(0, int(float(secs_raw))) - except Exception: - secs = 0 - - if ser: - # Get current end time to preserve it - current_end = get_intro_skip_end_seconds(ser) - set_intro_skip_seconds(ser, secs, current_end) - - # UI sofort aktualisieren - html = build_items_html(load_progress(), get_settings(driver)) - driver.execute_script( - "if (window.__bwSetList){window.__bwSetList(arguments[0]);}", - html, - ) - except Exception: - pass - - # Handle intro end updates (from sidebar input) – normalisieren + live anwenden - try: - upd = read_localstorage_value(driver, "bw_intro_end_update") - if upd: - data = json.loads(upd) - ser_raw = data.get("series", "") - secs_raw = data.get("seconds", 0) - ser = norm_series_key(ser_raw) - try: - secs = max(0, int(float(secs_raw))) - except Exception: - secs = 0 - - if ser: - # Get current start time to preserve it - current_start = get_intro_skip_seconds(ser) - set_intro_skip_seconds(ser, current_start, secs) - - # UI sofort aktualisieren - html = build_items_html(load_progress(), get_settings(driver)) - driver.execute_script( - "if (window.__bwSetList){window.__bwSetList(arguments[0]);}", - html, - ) - except Exception: - pass - # Handle end screen updates (from sidebar input) – normalisieren + live anwenden try: upd = read_localstorage_value(driver, "bw_end_update") diff --git a/SerienJunkie/settings.json b/SerienJunkie/settings.json index 3413039..7aca940 100644 --- a/SerienJunkie/settings.json +++ b/SerienJunkie/settings.json @@ -1,9 +1,8 @@ { "useTorProxy": false, "autoFullscreen": true, - "autoSkipIntro": true, "autoSkipEndScreen": true, "autoNext": true, "playbackRate": 1, "volume": 1 -} \ No newline at end of file +} From 05e8f32dd04b16cf93450f23cb3db97eb55bc2c6 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:03:38 +0100 Subject: [PATCH 02/26] Add optional intro fingerprint skipping --- README.md | 23 ++++++ SerienJunkie/intro_fingerprints.json | 7 ++ SerienJunkie/s.toBot.py | 101 +++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 SerienJunkie/intro_fingerprints.json diff --git a/README.md b/README.md index 46fd91f..6518ca1 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Automated binge-watching helper for **s.to** and **aniworld.to** with progress - **Multi-provider support**: s.to and aniworld.to with automatic provider detection. - **Progress tracking**: resume by series/season/episode with saved timestamps. +- **Optional intro skip**: fingerprint-configured per season. - **End screen skip**: jump past credits/outro if configured. - **Auto fullscreen**: multiple fallback strategies for stubborn players. - **Sidebar UI**: search, sort, quick actions, and settings panel. @@ -84,9 +85,30 @@ Important keys: - `playbackRate` (number) - `volume` (number, `0.0`–`1.0`) +### Intro fingerprints (optional) + +To enable intro skipping per season, create `intro_fingerprints.json` with keys +like `_s`, for example `one_piece_s07`: + +```json +{ + "one_piece_s07": { + "fingerprint": "A_LONG_FP_STRING", + "fingerprintDuration": 10, + "fullIntroDurationSeconds": 145 + } +} +``` + +If `fingerprint` is omitted but `fullIntroDurationSeconds` is present, the +player will skip the first N seconds at the start of the episode. If a +fingerprint is present, an external matcher can signal a match by writing the +matched key into `localStorage` as `bw_intro_fp_match`. + ## Data Files - `progress.json`: persisted progress by series. +- `intro_fingerprints.json`: optional intro fingerprint configuration. - `settings.json`: app settings. ## Sidebar Highlights @@ -111,6 +133,7 @@ SerienJunkie/ ├── README.md # This file ├── geckodriver.exe # Firefox WebDriver ├── progress.json # Progress database (auto-created) +├── intro_fingerprints.json # Optional intro fingerprint data └── user.BingeWatcher/ # Firefox profile (auto-created) ``` diff --git a/SerienJunkie/intro_fingerprints.json b/SerienJunkie/intro_fingerprints.json new file mode 100644 index 0000000..02afacf --- /dev/null +++ b/SerienJunkie/intro_fingerprints.json @@ -0,0 +1,7 @@ +{ + "one_piece_s07": { + "fingerprint": "A_LONG_FP_STRING", + "fingerprintDuration": 10, + "fullIntroDurationSeconds": 145 + } +} diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index 1173ebd..cd71850 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -30,6 +30,7 @@ PROGRESS_DB_FILE = os.path.join(SCRIPT_DIR, "progress.json") SETTINGS_DB_FILE = os.path.join(SCRIPT_DIR, "settings.json") +INTRO_FINGERPRINTS_FILE = os.path.join(SCRIPT_DIR, "intro_fingerprints.json") # === STREAMING PROVIDERS === STREAMING_PROVIDERS = { @@ -175,6 +176,90 @@ def norm_series_key(s: str) -> str: return str(s or "").strip() +def build_intro_fingerprint_key(series: str, season: int) -> str: + safe_series: str = re.sub(r"[^a-z0-9_]+", "_", (series or "").lower()) + safe_series = safe_series.strip("_") + season_value: int = max(0, int(season)) + return f"{safe_series}_s{season_value:02d}" + + +def load_intro_fingerprints() -> Dict[str, Dict[str, Any]]: + try: + if os.path.exists(INTRO_FINGERPRINTS_FILE): + with open(INTRO_FINGERPRINTS_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + if isinstance(data, dict): + return data + return {} + except Exception as e: + logging.error(f"Intro fingerprints could not be loaded: {e}") + return {} + + +def get_intro_fingerprint_entry(series: str, season: int) -> Optional[Dict[str, Any]]: + intro_fingerprint_key: str = build_intro_fingerprint_key(series, season) + intro_fingerprints: Dict[str, Dict[str, Any]] = load_intro_fingerprints() + entry_raw: Optional[Dict[str, Any]] = intro_fingerprints.get(intro_fingerprint_key) + if not isinstance(entry_raw, dict): + return None + return entry_raw + + +def read_intro_fingerprint_match(driver: webdriver.Firefox) -> Optional[str]: + try: + driver.switch_to.default_content() + value = driver.execute_script( + """ + let r = localStorage.getItem('bw_intro_fp_match'); + if (r) localStorage.removeItem('bw_intro_fp_match'); + return r; + """ + ) + return value if isinstance(value, str) else None + except Exception: + return None + + +def maybe_apply_intro_skip( + driver: webdriver.Firefox, + series: str, + season: int, + intro_skip_applied: bool, +) -> bool: + if intro_skip_applied: + return True + + entry = get_intro_fingerprint_entry(series, season) + if not entry: + return intro_skip_applied + + intro_duration_raw: int = int(entry.get("fullIntroDurationSeconds", 0) or 0) + intro_duration_seconds: int = max(0, intro_duration_raw) + if intro_duration_seconds <= 0: + return intro_skip_applied + + fingerprint_value: str = str(entry.get("fingerprint", "") or "").strip() + if not fingerprint_value: + seek_to_position(driver, intro_duration_seconds) + return True + + matched_key = read_intro_fingerprint_match(driver) + if matched_key: + intro_fingerprint_key: str = build_intro_fingerprint_key(series, season) + if matched_key == intro_fingerprint_key: + current_time_value: float = float( + driver.execute_script( + "return document.querySelector('video')?.currentTime || 0;" + ) + or 0 + ) + target_time: int = int(current_time_value + intro_duration_seconds) + seek_to_position(driver, target_time) + return True + + return intro_skip_applied + + # === BROWSER HANDLING --------------------------- === def start_browser() -> webdriver.Firefox: try: @@ -877,6 +962,7 @@ def play_episodes_loop( vol = settings["volume"] fullscreen_attempted: bool = False end_skip_applied: bool = False + intro_skip_applied: bool = False print( f"\n[▶] Playing {series.capitalize()} – Season {current_season}, Episode {current_episode}" @@ -915,6 +1001,14 @@ def play_episodes_loop( if position and position > 0: seek_to_position(driver, position) + intro_skip_applied = True + else: + intro_skip_applied = maybe_apply_intro_skip( + driver=driver, + series=series, + season=current_season, + intro_skip_applied=intro_skip_applied, + ) position = 0 recovery_tries = 0 @@ -1313,6 +1407,13 @@ def play_episodes_loop( except Exception: pass + intro_skip_applied = maybe_apply_intro_skip( + driver=driver, + series=series, + season=current_season, + intro_skip_applied=intro_skip_applied, + ) + time.sleep(1.0) if auto_nav: From 3c1b6d36f2a70a305880424b99ee3fedc0a99564 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:16:38 +0100 Subject: [PATCH 03/26] Add per-series intro skip settings --- README.md | 5 +- SerienJunkie/s.toBot.py | 386 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 383 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6518ca1..c45f054 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,8 @@ like `_s`, for example `one_piece_s07`: If `fingerprint` is omitted but `fullIntroDurationSeconds` is present, the player will skip the first N seconds at the start of the episode. If a fingerprint is present, an external matcher can signal a match by writing the -matched key into `localStorage` as `bw_intro_fp_match`. +matched key into `localStorage` as `bw_intro_fp_match`. You can also edit these +values per series/season from the in-app “Skip Settings” panel. ## Data Files @@ -115,7 +116,7 @@ matched key into `localStorage` as `bw_intro_fp_match`. - **Series list** with last watched time. - **Provider tabs** to filter s.to vs. aniworld.to. -- **Per-series controls** for end skip windows. +- **Per-series controls** for intro duration/fingerprint and end skip windows. - **Quick actions**: skip episode, open settings, quit. ## Troubleshooting diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index cd71850..44c0996 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -205,6 +205,87 @@ def get_intro_fingerprint_entry(series: str, season: int) -> Optional[Dict[str, return entry_raw +def set_intro_fingerprint_entry( + series: str, + season: int, + full_intro_duration_seconds: int, + fingerprint: str, + fingerprint_duration: int, +) -> bool: + try: + intro_fingerprints: Dict[str, Dict[str, Any]] = load_intro_fingerprints() + intro_fingerprint_key: str = build_intro_fingerprint_key(series, season) + entry: Dict[str, Any] = intro_fingerprints.get(intro_fingerprint_key, {}) + + intro_duration_value: int = max(0, int(full_intro_duration_seconds)) + fingerprint_value: str = str(fingerprint or "").strip() + fingerprint_duration_value: int = max(0, int(fingerprint_duration)) + + if intro_duration_value > 0: + entry["fullIntroDurationSeconds"] = intro_duration_value + else: + entry.pop("fullIntroDurationSeconds", None) + + if fingerprint_value: + entry["fingerprint"] = fingerprint_value + if fingerprint_duration_value > 0: + entry["fingerprintDuration"] = fingerprint_duration_value + else: + entry.pop("fingerprintDuration", None) + else: + entry.pop("fingerprint", None) + entry.pop("fingerprintDuration", None) + + if not entry: + intro_fingerprints.pop(intro_fingerprint_key, None) + else: + intro_fingerprints[intro_fingerprint_key] = entry + + with open(INTRO_FINGERPRINTS_FILE, "w", encoding="utf-8") as f: + json.dump(intro_fingerprints, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + logging.error(f"Intro fingerprint could not be saved: {e}") + return False + + +def merge_intro_fingerprint_entry( + series: str, + season: int, + full_intro_duration_seconds: Optional[int], + fingerprint: Optional[str], + fingerprint_duration: Optional[int], +) -> bool: + entry: Optional[Dict[str, Any]] = get_intro_fingerprint_entry(series, season) + current_duration: int = int((entry or {}).get("fullIntroDurationSeconds", 0) or 0) + current_fingerprint: str = str((entry or {}).get("fingerprint", "") or "") + current_fp_duration: int = int((entry or {}).get("fingerprintDuration", 0) or 0) + + next_duration: int = ( + int(full_intro_duration_seconds) + if full_intro_duration_seconds is not None + else current_duration + ) + next_fingerprint: str = ( + str(fingerprint or "") + if fingerprint is not None + else current_fingerprint + ) + next_fp_duration: int = ( + int(fingerprint_duration) + if fingerprint_duration is not None + else current_fp_duration + ) + + return set_intro_fingerprint_entry( + series=series, + season=season, + full_intro_duration_seconds=next_duration, + fingerprint=next_fingerprint, + fingerprint_duration=next_fp_duration, + ) + + def read_intro_fingerprint_match(driver: webdriver.Firefox) -> Optional[str]: try: driver.switch_to.default_content() @@ -1273,6 +1354,89 @@ def play_episodes_loop( # --- LIVE SERIES SKIP UPDATES ---------------------------------- try: skip_settings_changed = False + upd = read_localstorage_value(driver, "bw_intro_duration_update") + if upd: + data = json.loads(upd) + ser_raw = data.get("series", "") + season_raw = data.get("season", 0) + secs_raw = data.get("seconds", 0) + ser = norm_series_key(ser_raw) + try: + season_value = max(0, int(float(season_raw))) + except Exception: + season_value = 0 + try: + secs = max(0, int(float(secs_raw))) + except Exception: + secs = 0 + + if ser and season_value > 0: + if merge_intro_fingerprint_entry( + series=ser, + season=season_value, + full_intro_duration_seconds=secs, + fingerprint=None, + fingerprint_duration=None, + ): + skip_settings_changed = True + except Exception: + pass + + try: + upd = read_localstorage_value(driver, "bw_intro_fp_update") + if upd: + data = json.loads(upd) + ser_raw = data.get("series", "") + season_raw = data.get("season", 0) + fingerprint_raw = data.get("fingerprint", "") + ser = norm_series_key(ser_raw) + try: + season_value = max(0, int(float(season_raw))) + except Exception: + season_value = 0 + fingerprint_value = str(fingerprint_raw or "").strip() + + if ser and season_value > 0: + if merge_intro_fingerprint_entry( + series=ser, + season=season_value, + full_intro_duration_seconds=None, + fingerprint=fingerprint_value, + fingerprint_duration=None, + ): + skip_settings_changed = True + except Exception: + pass + + try: + upd = read_localstorage_value(driver, "bw_intro_fp_duration_update") + if upd: + data = json.loads(upd) + ser_raw = data.get("series", "") + season_raw = data.get("season", 0) + secs_raw = data.get("seconds", 0) + ser = norm_series_key(ser_raw) + try: + season_value = max(0, int(float(season_raw))) + except Exception: + season_value = 0 + try: + secs = max(0, int(float(secs_raw))) + except Exception: + secs = 0 + + if ser and season_value > 0: + if merge_intro_fingerprint_entry( + series=ser, + season=season_value, + full_intro_duration_seconds=None, + fingerprint=None, + fingerprint_duration=secs, + ): + skip_settings_changed = True + except Exception: + pass + upd = read_localstorage_value(driver, "bw_end_update") if upd: data = json.loads(upd) @@ -2267,6 +2431,7 @@ def build_items_html(db: Dict[str, Dict[str, Any]], settings: Optional[Dict[str, """Erstellt HTML für die Sidebar mit Streaming-Anbieter-Tabs.""" if settings is None: settings = {} + intro_fingerprints: Dict[str, Dict[str, Any]] = load_intro_fingerprints() # Gruppiere Serien nach Anbietern provider_series = {} for series_name, data in db.items(): @@ -2306,11 +2471,16 @@ def build_items_html(db: Dict[str, Dict[str, Any]], settings: Optional[Dict[str, position = int(data.get("position", 0)) ts_val = float(data.get("timestamp", 0)) end_skip_val = int(data.get("end_skip", 0)) + intro_fingerprint_key: str = build_intro_fingerprint_key(series_name, season) + intro_entry: Dict[str, Any] = intro_fingerprints.get(intro_fingerprint_key, {}) + intro_duration_val = int(intro_entry.get("fullIntroDurationSeconds", 0) or 0) + intro_fp_val = _html.escape(str(intro_entry.get("fingerprint", "") or ""), quote=True) + intro_fp_duration_val = int(intro_entry.get("fingerprintDuration", 0) or 0) safe_name = _html.escape(series_name, quote=True) series_items.append(f"""
@@ -2326,9 +2496,9 @@ def build_items_html(db: Dict[str, Dict[str, Any]], settings: Optional[Dict[str,
X
-
@@ -2572,10 +2742,28 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> box-shadow: 0 4px 12px rgba(59,130,246,.15); } + #bingeSidebar .bw-intro-duration, + #bingeSidebar .bw-intro-fingerprint, + #bingeSidebar .bw-intro-fp-duration, #bingeSidebar .bw-end { transition: all .2s ease !important; } + #bingeSidebar .bw-intro-duration:focus, + #bingeSidebar .bw-intro-fingerprint:focus, + #bingeSidebar .bw-intro-fp-duration:focus { + transform: scale(1.02); + box-shadow: 0 0 0 2px rgba(59,130,246,.3); + border-color: rgba(59,130,246,.6) !important; + } + + #bingeSidebar .bw-intro-duration:hover, + #bingeSidebar .bw-intro-fingerprint:hover, + #bingeSidebar .bw-intro-fp-duration:hover { + border-color: rgba(59,130,246,.5) !important; + background: rgba(59,130,246,.15) !important; + } + #bingeSidebar .bw-end:focus { transform: scale(1.02); box-shadow: 0 0 0 2px rgba(239,68,68,.3); @@ -2588,10 +2776,12 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> } /* Input field animations */ + #bingeSidebar .bw-intro-section, #bingeSidebar .bw-end-section { transition: all .2s ease; } + #bingeSidebar .bw-intro-section:hover, #bingeSidebar .bw-end-section:hover { transform: translateX(2px); } @@ -2816,7 +3006,7 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> d.addEventListener('input', (e)=>{ if (e.target && e.target.id==='bwSearch') onFilter(); }); d.addEventListener('change', (e)=>{ if (e.target && e.target.id==='bwSort') onSort(); }); - const openSeriesSkipPanel = (seriesName, endSkip) => { + const openSeriesSkipPanel = (seriesName, seasonValue, introDuration, introFingerprint, introFingerprintDuration, endSkip) => { const existingPanel = document.getElementById('bwSeriesSkipPanel'); if (existingPanel) { existingPanel.remove(); @@ -2844,6 +3034,39 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> boxShadow: '0 10px 30px rgba(0,0,0,.4)', }); + const introSectionHtml = ` +
+
+
>
+ Intro Skip (Season ${seasonValue}) +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ `; + const endSectionHtml = allowEnd ? `
@@ -2868,6 +3091,7 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) ->
+ ${introSectionHtml} ${endSectionHtml}
`; @@ -2913,6 +3137,42 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> if (!window.__bwDebouncers) window.__bwDebouncers = Object.create(null); panel.addEventListener('input', (ev) => { + const introDurationInput = ev.target.closest && ev.target.closest('input.bw-intro-duration'); + if (introDurationInput) { + const series = introDurationInput.dataset.series; if (!series) return; + const season = parseInt(introDurationInput.dataset.season || '0', 10) || 0; + const key = '__deb_intro_duration_' + series + '_' + season; + if (window.__bwDebouncers[key]) clearTimeout(window.__bwDebouncers[key]); + window.__bwDebouncers[key] = setTimeout(() => { + const seconds = parseInt(introDurationInput.value || '0', 10) || 0; + localStorage.setItem('bw_intro_duration_update', JSON.stringify({ series, season, seconds })); + }, 600); + } + + const introFingerprintInput = ev.target.closest && ev.target.closest('input.bw-intro-fingerprint'); + if (introFingerprintInput) { + const series = introFingerprintInput.dataset.series; if (!series) return; + const season = parseInt(introFingerprintInput.dataset.season || '0', 10) || 0; + const key = '__deb_intro_fp_' + series + '_' + season; + if (window.__bwDebouncers[key]) clearTimeout(window.__bwDebouncers[key]); + window.__bwDebouncers[key] = setTimeout(() => { + const fingerprint = String(introFingerprintInput.value || '').trim(); + localStorage.setItem('bw_intro_fp_update', JSON.stringify({ series, season, fingerprint })); + }, 600); + } + + const introFingerprintDurationInput = ev.target.closest && ev.target.closest('input.bw-intro-fp-duration'); + if (introFingerprintDurationInput) { + const series = introFingerprintDurationInput.dataset.series; if (!series) return; + const season = parseInt(introFingerprintDurationInput.dataset.season || '0', 10) || 0; + const key = '__deb_intro_fp_duration_' + series + '_' + season; + if (window.__bwDebouncers[key]) clearTimeout(window.__bwDebouncers[key]); + window.__bwDebouncers[key] = setTimeout(() => { + const seconds = parseInt(introFingerprintDurationInput.value || '0', 10) || 0; + localStorage.setItem('bw_intro_fp_duration_update', JSON.stringify({ series, season, seconds })); + }, 600); + } + const endInput = ev.target.closest && ev.target.closest('input.bw-end'); if (endInput) { const series = endInput.dataset.series; if (!series) return; @@ -3127,9 +3387,20 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> const skipPanelButton = c('.bw-series-settings'); if (skipPanelButton) { const seriesName = skipPanelButton.getAttribute('data-series'); + const seasonValue = parseInt(skipPanelButton.getAttribute('data-season') || '0', 10) || 0; + const introDuration = parseInt(skipPanelButton.getAttribute('data-intro-duration') || '0', 10) || 0; + const introFingerprint = skipPanelButton.getAttribute('data-intro-fingerprint') || ''; + const introFingerprintDuration = parseInt(skipPanelButton.getAttribute('data-intro-fp-duration') || '0', 10) || 0; const endSkip = parseInt(skipPanelButton.getAttribute('data-end-skip') || '0', 10) || 0; if (seriesName) { - openSeriesSkipPanel(seriesName, endSkip); + openSeriesSkipPanel( + seriesName, + seasonValue, + introDuration, + introFingerprint, + introFingerprintDuration, + endSkip + ); } return; } @@ -3138,9 +3409,14 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> if (item) { // Check if click originated from input field or delete button - don't trigger navigation const clickedEndInput = e.target.closest && e.target.closest('input.bw-end'); + const clickedIntroInput = e.target.closest && ( + e.target.closest('input.bw-intro-duration') || + e.target.closest('input.bw-intro-fingerprint') || + e.target.closest('input.bw-intro-fp-duration') + ); const clickedDelete = e.target.closest && e.target.closest('.bw-delete'); const clickedSkipSettings = e.target.closest && e.target.closest('.bw-series-settings'); - if (clickedEndInput || clickedDelete || clickedSkipSettings) return; + if (clickedEndInput || clickedIntroInput || clickedDelete || clickedSkipSettings) return; if (localStorage.getItem('bw_nav_inflight') === '1') return; // throttle localStorage.setItem('bw_nav_inflight','1'); @@ -3372,6 +3648,104 @@ def main() -> None: # Handle end screen updates (from sidebar input) – normalisieren + live anwenden try: + upd = read_localstorage_value(driver, "bw_intro_duration_update") + if upd: + data = json.loads(upd) + ser_raw = data.get("series", "") + season_raw = data.get("season", 0) + secs_raw = data.get("seconds", 0) + ser = norm_series_key(ser_raw) + try: + season_value = max(0, int(float(season_raw))) + except Exception: + season_value = 0 + try: + secs = max(0, int(float(secs_raw))) + except Exception: + secs = 0 + + if ser and season_value > 0: + merge_intro_fingerprint_entry( + series=ser, + season=season_value, + full_intro_duration_seconds=secs, + fingerprint=None, + fingerprint_duration=None, + ) + + html = build_items_html(load_progress(), get_settings(driver)) + driver.execute_script( + "if (window.__bwSetList){window.__bwSetList(arguments[0]);}", + html, + ) + except Exception: + pass + + try: + upd = read_localstorage_value(driver, "bw_intro_fp_update") + if upd: + data = json.loads(upd) + ser_raw = data.get("series", "") + season_raw = data.get("season", 0) + fingerprint_raw = data.get("fingerprint", "") + ser = norm_series_key(ser_raw) + try: + season_value = max(0, int(float(season_raw))) + except Exception: + season_value = 0 + fingerprint_value = str(fingerprint_raw or "").strip() + + if ser and season_value > 0: + merge_intro_fingerprint_entry( + series=ser, + season=season_value, + full_intro_duration_seconds=None, + fingerprint=fingerprint_value, + fingerprint_duration=None, + ) + + html = build_items_html(load_progress(), get_settings(driver)) + driver.execute_script( + "if (window.__bwSetList){window.__bwSetList(arguments[0]);}", + html, + ) + except Exception: + pass + + try: + upd = read_localstorage_value(driver, "bw_intro_fp_duration_update") + if upd: + data = json.loads(upd) + ser_raw = data.get("series", "") + season_raw = data.get("season", 0) + secs_raw = data.get("seconds", 0) + ser = norm_series_key(ser_raw) + try: + season_value = max(0, int(float(season_raw))) + except Exception: + season_value = 0 + try: + secs = max(0, int(float(secs_raw))) + except Exception: + secs = 0 + + if ser and season_value > 0: + merge_intro_fingerprint_entry( + series=ser, + season=season_value, + full_intro_duration_seconds=None, + fingerprint=None, + fingerprint_duration=secs, + ) + + html = build_items_html(load_progress(), get_settings(driver)) + driver.execute_script( + "if (window.__bwSetList){window.__bwSetList(arguments[0]);}", + html, + ) + except Exception: + pass + upd = read_localstorage_value(driver, "bw_end_update") if upd: data = json.loads(upd) From c400974c2b1dd826c5e1d4ff112af66cc38a2b7d Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:18:49 +0100 Subject: [PATCH 04/26] Add MP3 picker for intro fingerprints --- README.md | 7 + SerienJunkie/intro_uploads/.gitkeep | 1 + SerienJunkie/s.toBot.py | 282 +++++++++++++++++++++++++++- 3 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 SerienJunkie/intro_uploads/.gitkeep diff --git a/README.md b/README.md index c45f054..ac60272 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,12 @@ fingerprint is present, an external matcher can signal a match by writing the matched key into `localStorage` as `bw_intro_fp_match`. You can also edit these values per series/season from the in-app “Skip Settings” panel. +To generate a fingerprint without editing JSON, drop your MP3 into +`SerienJunkie/intro_uploads/`, open the per-series “Skip Settings” panel, and +select the file in the “Select MP3 (intro_uploads)” picker. The app will +generate the fingerprint, save it into `intro_fingerprints.json`, and delete the +MP3 after processing. + ## Data Files - `progress.json`: persisted progress by series. @@ -135,6 +141,7 @@ SerienJunkie/ ├── geckodriver.exe # Firefox WebDriver ├── progress.json # Progress database (auto-created) ├── intro_fingerprints.json # Optional intro fingerprint data +├── intro_uploads/ # Optional MP3 drop folder └── user.BingeWatcher/ # Firefox profile (auto-created) ``` diff --git a/SerienJunkie/intro_uploads/.gitkeep b/SerienJunkie/intro_uploads/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/SerienJunkie/intro_uploads/.gitkeep @@ -0,0 +1 @@ + diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index 44c0996..597d19d 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -3,8 +3,11 @@ import logging import os import re +import shutil +import subprocess +import tempfile import time -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from urllib.parse import unquote from selenium import webdriver @@ -31,6 +34,7 @@ PROGRESS_DB_FILE = os.path.join(SCRIPT_DIR, "progress.json") SETTINGS_DB_FILE = os.path.join(SCRIPT_DIR, "settings.json") INTRO_FINGERPRINTS_FILE = os.path.join(SCRIPT_DIR, "intro_fingerprints.json") +INTRO_UPLOAD_DIR = os.path.join(SCRIPT_DIR, "intro_uploads") # === STREAMING PROVIDERS === STREAMING_PROVIDERS = { @@ -286,6 +290,93 @@ def merge_intro_fingerprint_entry( ) +def _resolve_fpcalc_binary() -> Optional[str]: + fpcalc_binary: Optional[str] = shutil.which("fpcalc") + if fpcalc_binary: + return fpcalc_binary + return shutil.which("fpcalc.exe") + + +def extract_fingerprint_from_mp3(mp3_bytes: bytes) -> Optional[Dict[str, Any]]: + fpcalc_binary: Optional[str] = _resolve_fpcalc_binary() + if not fpcalc_binary: + logging.error("fpcalc binary not found. Please install Chromaprint.") + return None + + temp_file = None + try: + temp_file = tempfile.NamedTemporaryFile( + mode="wb", + suffix=".mp3", + delete=False, + dir=SCRIPT_DIR, + ) + temp_file.write(mp3_bytes) + temp_file.flush() + temp_file.close() + + result = subprocess.run( + [fpcalc_binary, "-json", temp_file.name], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + logging.error( + f"fpcalc failed: {result.stderr.strip() or 'unknown error'}" + ) + return None + + payload = json.loads(result.stdout or "{}") + if not isinstance(payload, dict): + return None + + fingerprint_value: str = str(payload.get("fingerprint", "") or "").strip() + duration_value: int = int(payload.get("duration", 0) or 0) + if not fingerprint_value: + return None + + return { + "fingerprint": fingerprint_value, + "fingerprintDuration": duration_value, + } + except Exception as e: + logging.error(f"Fingerprint extraction failed: {e}") + return None + finally: + try: + if temp_file is not None and os.path.exists(temp_file.name): + os.remove(temp_file.name) + except Exception: + pass + + +def list_intro_upload_files() -> List[str]: + try: + os.makedirs(INTRO_UPLOAD_DIR, exist_ok=True) + files = [ + name + for name in os.listdir(INTRO_UPLOAD_DIR) + if name.lower().endswith(".mp3") + ] + return sorted(files) + except Exception: + return [] + + +def resolve_intro_upload_path(filename: str) -> Optional[str]: + safe_name: str = os.path.basename(filename or "") + if not safe_name: + return None + candidate: str = os.path.join(INTRO_UPLOAD_DIR, safe_name) + try: + if os.path.exists(candidate): + return candidate + except Exception: + return None + return None + + def read_intro_fingerprint_match(driver: webdriver.Firefox) -> Optional[str]: try: driver.switch_to.default_content() @@ -2431,6 +2522,7 @@ def build_items_html(db: Dict[str, Dict[str, Any]], settings: Optional[Dict[str, """Erstellt HTML für die Sidebar mit Streaming-Anbieter-Tabs.""" if settings is None: settings = {} + intro_upload_files: List[str] = list_intro_upload_files() intro_fingerprints: Dict[str, Dict[str, Any]] = load_intro_fingerprints() # Gruppiere Serien nach Anbietern provider_series = {} @@ -2511,7 +2603,9 @@ def build_items_html(db: Dict[str, Dict[str, Any]], settings: Optional[Dict[str, ''') # Kombiniere alles + upload_meta = _html.escape(json.dumps(intro_upload_files), quote=True) tabs_container = f''' +
{"".join(tabs_html)}
@@ -3064,6 +3158,19 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> placeholder="0" title="Fingerprint duration in seconds"/>
+
+
+ + + +
Drop MP3 files into intro_uploads/.
+
+
`; @@ -3184,6 +3291,96 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> }, 600); } }); + + const fillUploadFiles = () => { + const selectEl = panel.querySelector('select.bw-intro-file-select'); + if (!selectEl) return; + const filesNode = document.getElementById('bwUploadFiles'); + if (!filesNode) return; + let files = []; + try { + const raw = filesNode.getAttribute('data-files') || '[]'; + files = JSON.parse(raw); + } catch(_) {} + selectEl.innerHTML = ''; + const placeholder = document.createElement('option'); + placeholder.value = ''; + placeholder.textContent = files.length ? 'Select MP3 file' : 'No MP3 files found'; + selectEl.appendChild(placeholder); + files.forEach(file => { + const opt = document.createElement('option'); + opt.value = String(file); + opt.textContent = String(file); + selectEl.appendChild(opt); + }); + }; + + fillUploadFiles(); + + const uploadButton = panel.querySelector('button.bw-intro-file-apply'); + if (uploadButton) { + uploadButton.addEventListener('click', (ev) => { + ev.preventDefault(); + const series = uploadButton.dataset.series; if (!series) return; + const season = parseInt(uploadButton.dataset.season || '0', 10) || 0; + const selectEl = panel.querySelector('select.bw-intro-file-select'); + const statusEl = panel.querySelector('.bw-intro-upload-status'); + if (!selectEl) return; + const filename = String(selectEl.value || ''); + if (!filename) { + if (statusEl) statusEl.textContent = 'Select an MP3 first.'; + return; + } + localStorage.setItem('bw_intro_upload', JSON.stringify({ + series, + season, + filename + })); + if (statusEl) statusEl.textContent = 'Processing MP3...'; + }); + } + + const uploadPollInterval = setInterval(() => { + const statusEl = panel.querySelector('.bw-intro-upload-status'); + if (!statusEl) return; + const series = statusEl.getAttribute('data-series'); + const season = parseInt(statusEl.getAttribute('data-season') || '0', 10) || 0; + if (!series || season <= 0) return; + + let resultRaw = null; + let errorRaw = null; + try { resultRaw = localStorage.getItem('bw_intro_upload_result'); } catch(_) {} + try { errorRaw = localStorage.getItem('bw_intro_upload_error'); } catch(_) {} + + if (resultRaw) { + try { + const result = JSON.parse(resultRaw); + if (result && result.series === series && result.season === season) { + localStorage.removeItem('bw_intro_upload_result'); + const fpInput = panel.querySelector('input.bw-intro-fingerprint'); + const fpDurationInput = panel.querySelector('input.bw-intro-fp-duration'); + if (fpInput) fpInput.value = String(result.fingerprint || ''); + if (fpDurationInput) fpDurationInput.value = String(result.fingerprintDuration || 0); + fillUploadFiles(); + statusEl.textContent = 'Fingerprint saved. MP3 removed.'; + } + } catch(_) {} + } + + if (errorRaw) { + try { + const result = JSON.parse(errorRaw); + if (result && result.series === series && result.season === season) { + localStorage.removeItem('bw_intro_upload_error'); + fillUploadFiles(); + statusEl.textContent = 'Upload failed. Please try again.'; + } + } catch(_) {} + } + }, 900); + + const stopUploadPoll = () => clearInterval(uploadPollInterval); + if (closeButton) closeButton.addEventListener('click', stopUploadPoll); }; d.addEventListener('click', (e)=>{ @@ -3412,7 +3609,9 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> const clickedIntroInput = e.target.closest && ( e.target.closest('input.bw-intro-duration') || e.target.closest('input.bw-intro-fingerprint') || - e.target.closest('input.bw-intro-fp-duration') + e.target.closest('input.bw-intro-fp-duration') || + e.target.closest('select.bw-intro-file-select') || + e.target.closest('button.bw-intro-file-apply') ); const clickedDelete = e.target.closest && e.target.closest('.bw-delete'); const clickedSkipSettings = e.target.closest && e.target.closest('.bw-series-settings'); @@ -3647,6 +3846,85 @@ def main() -> None: pass # Handle end screen updates (from sidebar input) – normalisieren + live anwenden + try: + upd = read_localstorage_value(driver, "bw_intro_upload") + if upd: + data = json.loads(upd) + ser_raw = data.get("series", "") + season_raw = data.get("season", 0) + filename_raw = data.get("filename", "") + ser = norm_series_key(ser_raw) + try: + season_value = max(0, int(float(season_raw))) + except Exception: + season_value = 0 + filename_value: str = str(filename_raw or "").strip() + file_path: Optional[str] = resolve_intro_upload_path( + filename_value + ) + + if ser and season_value > 0 and file_path: + fingerprint_entry = None + try: + with open(file_path, "rb") as f: + mp3_bytes = f.read() + fingerprint_entry = extract_fingerprint_from_mp3( + mp3_bytes + ) + except Exception: + fingerprint_entry = None + + try: + os.remove(file_path) + except Exception: + pass + + if fingerprint_entry: + merge_intro_fingerprint_entry( + series=ser, + season=season_value, + full_intro_duration_seconds=None, + fingerprint=fingerprint_entry.get("fingerprint"), + fingerprint_duration=fingerprint_entry.get( + "fingerprintDuration" + ), + ) + driver.execute_script( + "localStorage.setItem('bw_intro_upload_result', arguments[0]);", + json.dumps( + { + "series": ser, + "season": season_value, + "fingerprint": fingerprint_entry.get( + "fingerprint" + ), + "fingerprintDuration": fingerprint_entry.get( + "fingerprintDuration" + ), + "filename": filename_value, + } + ), + ) + else: + driver.execute_script( + "localStorage.setItem('bw_intro_upload_error', arguments[0]);", + json.dumps( + { + "series": ser, + "season": season_value, + "filename": filename_value, + } + ), + ) + + html = build_items_html(load_progress(), get_settings(driver)) + driver.execute_script( + "if (window.__bwSetList){window.__bwSetList(arguments[0]);}", + html, + ) + except Exception: + pass + try: upd = read_localstorage_value(driver, "bw_intro_duration_update") if upd: From 53b3c217a3677d7dbfd157514fc5b3b6447dce66 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:48:26 +0100 Subject: [PATCH 05/26] Check and install fpcalc for uploads --- README.md | 2 ++ SerienJunkie/s.toBot.py | 36 ++++++++++++++++++++++++++++-------- start_watching.bat | 22 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ac60272..773afbd 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,8 @@ MP3 after processing. ## Troubleshooting - **GeckoDriver not found**: Ensure `geckodriver.exe` sits next to `s.toBot.py`. +- **Chromaprint (fpcalc) missing**: Install Chromaprint so the MP3 fingerprint + generator can run (`fpcalc` must be on PATH). - **Video not playing**: Refresh the page or press Space to play. - **Sidebar missing**: Reload; some pages block injection until fully loaded. diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index 597d19d..9b57a49 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -2523,6 +2523,7 @@ def build_items_html(db: Dict[str, Dict[str, Any]], settings: Optional[Dict[str, if settings is None: settings = {} intro_upload_files: List[str] = list_intro_upload_files() + fpcalc_available: bool = _resolve_fpcalc_binary() is not None intro_fingerprints: Dict[str, Dict[str, Any]] = load_intro_fingerprints() # Gruppiere Serien nach Anbietern provider_series = {} @@ -2604,8 +2605,9 @@ def build_items_html(db: Dict[str, Dict[str, Any]], settings: Optional[Dict[str, # Kombiniere alles upload_meta = _html.escape(json.dumps(intro_upload_files), quote=True) + fpcalc_meta = "1" if fpcalc_available else "0" tabs_container = f''' - +
{"".join(tabs_html)}
@@ -3297,6 +3299,7 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> if (!selectEl) return; const filesNode = document.getElementById('bwUploadFiles'); if (!filesNode) return; + const fpcalcAvailable = filesNode.getAttribute('data-fpcalc') === '1'; let files = []; try { const raw = filesNode.getAttribute('data-files') || '[]'; @@ -3305,14 +3308,25 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> selectEl.innerHTML = ''; const placeholder = document.createElement('option'); placeholder.value = ''; - placeholder.textContent = files.length ? 'Select MP3 file' : 'No MP3 files found'; + if (!fpcalcAvailable) { + placeholder.textContent = 'Chromaprint not installed'; + } else { + placeholder.textContent = files.length ? 'Select MP3 file' : 'No MP3 files found'; + } selectEl.appendChild(placeholder); - files.forEach(file => { - const opt = document.createElement('option'); - opt.value = String(file); - opt.textContent = String(file); - selectEl.appendChild(opt); - }); + if (fpcalcAvailable) { + files.forEach(file => { + const opt = document.createElement('option'); + opt.value = String(file); + opt.textContent = String(file); + selectEl.appendChild(opt); + }); + } + selectEl.disabled = !fpcalcAvailable; + const statusEl = panel.querySelector('.bw-intro-upload-status'); + if (statusEl && !fpcalcAvailable) { + statusEl.textContent = 'Chromaprint (fpcalc) is not installed.'; + } }; fillUploadFiles(); @@ -3321,10 +3335,16 @@ def inject_sidebar(driver: webdriver.Firefox, db: Dict[str, Dict[str, Any]]) -> if (uploadButton) { uploadButton.addEventListener('click', (ev) => { ev.preventDefault(); + const filesNode = document.getElementById('bwUploadFiles'); + const fpcalcAvailable = filesNode && filesNode.getAttribute('data-fpcalc') === '1'; const series = uploadButton.dataset.series; if (!series) return; const season = parseInt(uploadButton.dataset.season || '0', 10) || 0; const selectEl = panel.querySelector('select.bw-intro-file-select'); const statusEl = panel.querySelector('.bw-intro-upload-status'); + if (!fpcalcAvailable) { + if (statusEl) statusEl.textContent = 'Install Chromaprint (fpcalc) first.'; + return; + } if (!selectEl) return; const filename = String(selectEl.value || ''); if (!filename) { diff --git a/start_watching.bat b/start_watching.bat index 47b2dab..9099127 100644 --- a/start_watching.bat +++ b/start_watching.bat @@ -42,6 +42,28 @@ if not "!missing_modules!"=="" ( echo [=] All dependencies satisfied. ) +REM === Check for Chromaprint (fpcalc) === +where fpcalc >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo [-] Chromaprint (fpcalc) not found. Attempting to install... + where winget >nul 2>&1 + if %ERRORLEVEL% EQU 0 ( + winget install --id Chromaprint -e --silent >nul 2>&1 + ) + where choco >nul 2>&1 + if %ERRORLEVEL% EQU 0 ( + choco install chromaprint -y >nul 2>&1 + ) + where fpcalc >nul 2>&1 + if %ERRORLEVEL% NEQ 0 ( + echo [!] Chromaprint could not be installed automatically. Please install fpcalc manually. + ) else ( + echo [+] Chromaprint installed successfully. + ) +) else ( + echo [+] Chromaprint (fpcalc) already installed. +) + REM === Check Tor setting from settings.json === set "USE_TOR=false" for /f "usebackq delims=" %%a in (`powershell -NoProfile -Command "try { $json = Get-Content -Raw 'settings.json' | ConvertFrom-Json; $value = $json.useTorProxy; if ($value -is [string]) { $value = $value.Trim().ToLower() -eq 'true' } else { $value = [bool]$value }; if ($value) { 'true' } else { 'false' } } catch { 'false' }"`) do ( From 20fde714d4b71d81e0e9581e562cdb5cdf0f2e7c Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:32:19 +0100 Subject: [PATCH 06/26] Keep start_watching.bat open on errors --- start_watching.bat | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/start_watching.bat b/start_watching.bat index 9099127..953b209 100644 --- a/start_watching.bat +++ b/start_watching.bat @@ -3,6 +3,10 @@ setlocal enabledelayedexpansion REM === Navigate to the script directory === cd /d "%~dp0SerienJunkie" +if %ERRORLEVEL% NEQ 0 ( + echo [X] Failed to change directory to "%~dp0SerienJunkie". + goto :handle_error +) echo Starting Binge Watching... REM === Required Python modules === @@ -12,8 +16,7 @@ REM === Check Python installation === python --version >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( echo [X] Python is not installed or not added to PATH. - pause - exit /b 1 + goto :handle_error ) REM === Check and install missing modules === @@ -35,8 +38,7 @@ if not "!missing_modules!"=="" ( python -m pip install !missing_modules! if !ERRORLEVEL! NEQ 0 ( echo [X] Failed to install modules. Please install manually. - pause - exit /b 1 + goto :handle_error ) ) else ( echo [=] All dependencies satisfied. @@ -90,11 +92,10 @@ if /i "%USE_TOR%"=="true" ( netstat -an | find "9050" >nul if %ERRORLEVEL% EQU 0 ( set /a waitcount+=1 - if !waitcount! LSS 55 goto waittorclose - echo [X] Port 9050 did not become available after kill. Aborted execution. - pause - exit /b 1 - ) + if !waitcount! LSS 55 goto waittorclose + echo [X] Port 9050 did not become available after kill. Aborted execution. + goto :handle_error + ) ) REM Start Tor now @@ -109,8 +110,7 @@ if /i "%USE_TOR%"=="true" ( set /a waitcount+=1 if !waitcount! LSS 30 goto waittorstart echo [X] port 9050 was not opened! - pause - exit /b 1 + goto :handle_error ) echo [+] Tor started successfully. ) @@ -135,10 +135,15 @@ if %EXITCODE% EQU 0 ( endlocal & exit /b 0 ) else ( echo [X] BingeWatcher exited with code %EXITCODE%. - pause if "%USE_TOR%"=="true" ( echo [i] Cleaning up Tor process... taskkill /IM tor.exe /F >nul 2>&1 ) - endlocal & exit /b %EXITCODE% + goto :handle_error ) + +:handle_error +echo. +echo [!] Script aborted. Review the messages above. +pause +endlocal & exit /b 1 From 738775d35aa4290e14a1cfe96274ef152aaeaeb2 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:00:05 +0100 Subject: [PATCH 07/26] Log startup errors to file --- start_watching.bat | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/start_watching.bat b/start_watching.bat index 953b209..3375a49 100644 --- a/start_watching.bat +++ b/start_watching.bat @@ -1,13 +1,17 @@ @echo off setlocal enabledelayedexpansion +set "BW_LOG=%~dp0SerienJunkie\bw_startup.log" +echo [BingeWatcher] Starting... > "%BW_LOG%" REM === Navigate to the script directory === cd /d "%~dp0SerienJunkie" if %ERRORLEVEL% NEQ 0 ( echo [X] Failed to change directory to "%~dp0SerienJunkie". + echo [X] Failed to change directory to "%~dp0SerienJunkie". >> "%BW_LOG%" goto :handle_error ) echo Starting Binge Watching... +echo [=] Working directory: %CD% >> "%BW_LOG%" REM === Required Python modules === set modules=selenium configparser @@ -16,6 +20,7 @@ REM === Check Python installation === python --version >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( echo [X] Python is not installed or not added to PATH. + echo [X] Python is not installed or not added to PATH. >> "%BW_LOG%" goto :handle_error ) @@ -38,16 +43,19 @@ if not "!missing_modules!"=="" ( python -m pip install !missing_modules! if !ERRORLEVEL! NEQ 0 ( echo [X] Failed to install modules. Please install manually. + echo [X] Failed to install modules. Please install manually. >> "%BW_LOG%" goto :handle_error ) ) else ( echo [=] All dependencies satisfied. + echo [=] All dependencies satisfied. >> "%BW_LOG%" ) REM === Check for Chromaprint (fpcalc) === where fpcalc >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( echo [-] Chromaprint (fpcalc) not found. Attempting to install... + echo [-] Chromaprint (fpcalc) not found. Attempting to install... >> "%BW_LOG%" where winget >nul 2>&1 if %ERRORLEVEL% EQU 0 ( winget install --id Chromaprint -e --silent >nul 2>&1 @@ -59,11 +67,14 @@ if %ERRORLEVEL% NEQ 0 ( where fpcalc >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( echo [!] Chromaprint could not be installed automatically. Please install fpcalc manually. + echo [!] Chromaprint could not be installed automatically. Please install fpcalc manually. >> "%BW_LOG%" ) else ( echo [+] Chromaprint installed successfully. + echo [+] Chromaprint installed successfully. >> "%BW_LOG%" ) ) else ( echo [+] Chromaprint (fpcalc) already installed. + echo [+] Chromaprint (fpcalc) already installed. >> "%BW_LOG%" ) REM === Check Tor setting from settings.json === @@ -94,6 +105,7 @@ if /i "%USE_TOR%"=="true" ( set /a waitcount+=1 if !waitcount! LSS 55 goto waittorclose echo [X] Port 9050 did not become available after kill. Aborted execution. + echo [X] Port 9050 did not become available after kill. Aborted execution. >> "%BW_LOG%" goto :handle_error ) ) @@ -110,15 +122,18 @@ if /i "%USE_TOR%"=="true" ( set /a waitcount+=1 if !waitcount! LSS 30 goto waittorstart echo [X] port 9050 was not opened! + echo [X] port 9050 was not opened! >> "%BW_LOG%" goto :handle_error ) echo [+] Tor started successfully. + echo [+] Tor started successfully. >> "%BW_LOG%" ) REM === Start Python Script === set BW_DEBUG=1 python s.toBot.py set EXITCODE=%ERRORLEVEL% +echo [i] Python exit code: %EXITCODE% >> "%BW_LOG%" REM Immer pausieren, damit du die letzte Zeile siehst echo. @@ -135,6 +150,7 @@ if %EXITCODE% EQU 0 ( endlocal & exit /b 0 ) else ( echo [X] BingeWatcher exited with code %EXITCODE%. + echo [X] BingeWatcher exited with code %EXITCODE%. >> "%BW_LOG%" if "%USE_TOR%"=="true" ( echo [i] Cleaning up Tor process... taskkill /IM tor.exe /F >nul 2>&1 @@ -145,5 +161,7 @@ if %EXITCODE% EQU 0 ( :handle_error echo. echo [!] Script aborted. Review the messages above. +echo [!] Script aborted. Review the messages above. >> "%BW_LOG%" +echo [i] Log saved to: %BW_LOG% pause endlocal & exit /b 1 From d1c283a48abd6782bc742092887b3a1179beb830 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:08:51 +0100 Subject: [PATCH 08/26] Keep cmd open during startup --- start_watching.bat | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/start_watching.bat b/start_watching.bat index 3375a49..6747aa4 100644 --- a/start_watching.bat +++ b/start_watching.bat @@ -1,5 +1,10 @@ @echo off setlocal enabledelayedexpansion +if "%BW_KEEP_SHELL%"=="" ( + set "BW_KEEP_SHELL=1" + cmd /k ""%~f0"" + exit /b 0 +) set "BW_LOG=%~dp0SerienJunkie\bw_startup.log" echo [BingeWatcher] Starting... > "%BW_LOG%" @@ -52,6 +57,7 @@ if not "!missing_modules!"=="" ( ) REM === Check for Chromaprint (fpcalc) === +echo [i] Checking Chromaprint (fpcalc)... >> "%BW_LOG%" where fpcalc >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( echo [-] Chromaprint (fpcalc) not found. Attempting to install... From f51f5443e7f875320993bf35af94e4729e42dba7 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:12:03 +0100 Subject: [PATCH 09/26] Fix missing_modules check in batch --- start_watching.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start_watching.bat b/start_watching.bat index 6747aa4..f7de224 100644 --- a/start_watching.bat +++ b/start_watching.bat @@ -42,7 +42,7 @@ for %%m in (%modules%) do ( ) ) -if not "!missing_modules!"=="" ( +if defined missing_modules ( echo Installing missing modules:!missing_modules! python -m pip install --upgrade pip >nul 2>&1 python -m pip install !missing_modules! From 27a371bc6b759a1e42273f397afae657aa771e8f Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:15:53 +0100 Subject: [PATCH 10/26] Fix Tor wait loop syntax --- start_watching.bat | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/start_watching.bat b/start_watching.bat index f7de224..905f612 100644 --- a/start_watching.bat +++ b/start_watching.bat @@ -109,11 +109,13 @@ if /i "%USE_TOR%"=="true" ( netstat -an | find "9050" >nul if %ERRORLEVEL% EQU 0 ( set /a waitcount+=1 - if !waitcount! LSS 55 goto waittorclose - echo [X] Port 9050 did not become available after kill. Aborted execution. - echo [X] Port 9050 did not become available after kill. Aborted execution. >> "%BW_LOG%" - goto :handle_error - ) + if !waitcount! LSS 55 ( + goto waittorclose + ) + echo [X] Port 9050 did not become available after kill. Aborted execution. + echo [X] Port 9050 did not become available after kill. Aborted execution. >> "%BW_LOG%" + goto :handle_error + ) ) REM Start Tor now From d275e9737d2ada2efd3f7ce989b9f9448f6c947b Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:23:11 +0100 Subject: [PATCH 11/26] Trim missing module list --- start_watching.bat | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/start_watching.bat b/start_watching.bat index 905f612..121af29 100644 --- a/start_watching.bat +++ b/start_watching.bat @@ -42,18 +42,22 @@ for %%m in (%modules%) do ( ) ) -if defined missing_modules ( - echo Installing missing modules:!missing_modules! +if "%missing_modules%" NEQ "" ( + set "missing_modules=%missing_modules:~1%" +) + +if "%missing_modules%"=="" ( + echo [=] All dependencies satisfied. + echo [=] All dependencies satisfied. >> "%BW_LOG%" +) else ( + echo Installing missing modules:%missing_modules% python -m pip install --upgrade pip >nul 2>&1 - python -m pip install !missing_modules! - if !ERRORLEVEL! NEQ 0 ( + python -m pip install %missing_modules% + if %ERRORLEVEL% NEQ 0 ( echo [X] Failed to install modules. Please install manually. echo [X] Failed to install modules. Please install manually. >> "%BW_LOG%" goto :handle_error ) -) else ( - echo [=] All dependencies satisfied. - echo [=] All dependencies satisfied. >> "%BW_LOG%" ) REM === Check for Chromaprint (fpcalc) === From abc7c5f9e33e0a28c9bbc1abb4df07eb8b1bccb7 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:34:16 +0100 Subject: [PATCH 12/26] Remove not wording from startup logs --- start_watching.bat | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/start_watching.bat b/start_watching.bat index 121af29..5d0483c 100644 --- a/start_watching.bat +++ b/start_watching.bat @@ -24,8 +24,8 @@ set modules=selenium configparser REM === Check Python installation === python --version >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( - echo [X] Python is not installed or not added to PATH. - echo [X] Python is not installed or not added to PATH. >> "%BW_LOG%" + echo [X] Python is missing or absent from PATH. + echo [X] Python is missing or absent from PATH. >> "%BW_LOG%" goto :handle_error ) @@ -64,8 +64,8 @@ REM === Check for Chromaprint (fpcalc) === echo [i] Checking Chromaprint (fpcalc)... >> "%BW_LOG%" where fpcalc >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( - echo [-] Chromaprint (fpcalc) not found. Attempting to install... - echo [-] Chromaprint (fpcalc) not found. Attempting to install... >> "%BW_LOG%" + echo [-] Chromaprint (fpcalc) missing. Attempting to install... + echo [-] Chromaprint (fpcalc) missing. Attempting to install... >> "%BW_LOG%" where winget >nul 2>&1 if %ERRORLEVEL% EQU 0 ( winget install --id Chromaprint -e --silent >nul 2>&1 @@ -76,8 +76,8 @@ if %ERRORLEVEL% NEQ 0 ( ) where fpcalc >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( - echo [!] Chromaprint could not be installed automatically. Please install fpcalc manually. - echo [!] Chromaprint could not be installed automatically. Please install fpcalc manually. >> "%BW_LOG%" + echo [!] Chromaprint couldn't be installed automatically. Please install fpcalc manually. + echo [!] Chromaprint couldn't be installed automatically. Please install fpcalc manually. >> "%BW_LOG%" ) else ( echo [+] Chromaprint installed successfully. echo [+] Chromaprint installed successfully. >> "%BW_LOG%" @@ -116,8 +116,8 @@ if /i "%USE_TOR%"=="true" ( if !waitcount! LSS 55 ( goto waittorclose ) - echo [X] Port 9050 did not become available after kill. Aborted execution. - echo [X] Port 9050 did not become available after kill. Aborted execution. >> "%BW_LOG%" + echo [X] Port 9050 never became available after kill. Aborted execution. + echo [X] Port 9050 never became available after kill. Aborted execution. >> "%BW_LOG%" goto :handle_error ) ) @@ -133,8 +133,8 @@ if /i "%USE_TOR%"=="true" ( if %ERRORLEVEL% NEQ 0 ( set /a waitcount+=1 if !waitcount! LSS 30 goto waittorstart - echo [X] port 9050 was not opened! - echo [X] port 9050 was not opened! >> "%BW_LOG%" + echo [X] port 9050 was never opened! + echo [X] port 9050 was never opened! >> "%BW_LOG%" goto :handle_error ) echo [+] Tor started successfully. From ed3a5839f7b3c34eda1cba5640066d8580b4dd41 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:58:17 +0100 Subject: [PATCH 13/26] Quote startup logs and remove shell relaunch --- start_watching.bat | 87 ++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/start_watching.bat b/start_watching.bat index 5d0483c..75ed5fe 100644 --- a/start_watching.bat +++ b/start_watching.bat @@ -1,22 +1,17 @@ @echo off setlocal enabledelayedexpansion -if "%BW_KEEP_SHELL%"=="" ( - set "BW_KEEP_SHELL=1" - cmd /k ""%~f0"" - exit /b 0 -) set "BW_LOG=%~dp0SerienJunkie\bw_startup.log" -echo [BingeWatcher] Starting... > "%BW_LOG%" +echo "[BingeWatcher] Starting..." > "%BW_LOG%" REM === Navigate to the script directory === cd /d "%~dp0SerienJunkie" if %ERRORLEVEL% NEQ 0 ( - echo [X] Failed to change directory to "%~dp0SerienJunkie". - echo [X] Failed to change directory to "%~dp0SerienJunkie". >> "%BW_LOG%" + echo "[X] Failed to change directory to %~dp0SerienJunkie." + echo "[X] Failed to change directory to %~dp0SerienJunkie." >> "%BW_LOG%" goto :handle_error ) -echo Starting Binge Watching... -echo [=] Working directory: %CD% >> "%BW_LOG%" +echo "Starting Binge Watching..." +echo "[=] Working directory: %CD%" >> "%BW_LOG%" REM === Required Python modules === set modules=selenium configparser @@ -24,8 +19,8 @@ set modules=selenium configparser REM === Check Python installation === python --version >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( - echo [X] Python is missing or absent from PATH. - echo [X] Python is missing or absent from PATH. >> "%BW_LOG%" + echo "[X] Python is missing or absent from PATH." + echo "[X] Python is missing or absent from PATH." >> "%BW_LOG%" goto :handle_error ) @@ -35,10 +30,10 @@ set "missing_modules=" for %%m in (%modules%) do ( python -c "import %%m" >nul 2>&1 if !ERRORLEVEL! NEQ 0 ( - echo [-] Missing Python module: %%m + echo "[-] Missing Python module: %%m" set "missing_modules=!missing_modules! %%m" ) else ( - echo [+] Python module '%%m' already installed. + echo "[+] Python module '%%m' already installed." ) ) @@ -47,25 +42,25 @@ if "%missing_modules%" NEQ "" ( ) if "%missing_modules%"=="" ( - echo [=] All dependencies satisfied. - echo [=] All dependencies satisfied. >> "%BW_LOG%" + echo "[=] All dependencies satisfied." + echo "[=] All dependencies satisfied." >> "%BW_LOG%" ) else ( - echo Installing missing modules:%missing_modules% + echo "Installing missing modules:%missing_modules%" python -m pip install --upgrade pip >nul 2>&1 python -m pip install %missing_modules% if %ERRORLEVEL% NEQ 0 ( - echo [X] Failed to install modules. Please install manually. - echo [X] Failed to install modules. Please install manually. >> "%BW_LOG%" + echo "[X] Failed to install modules. Please install manually." + echo "[X] Failed to install modules. Please install manually." >> "%BW_LOG%" goto :handle_error ) ) REM === Check for Chromaprint (fpcalc) === -echo [i] Checking Chromaprint (fpcalc)... >> "%BW_LOG%" +echo "[i] Checking Chromaprint (fpcalc)..." >> "%BW_LOG%" where fpcalc >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( - echo [-] Chromaprint (fpcalc) missing. Attempting to install... - echo [-] Chromaprint (fpcalc) missing. Attempting to install... >> "%BW_LOG%" + echo "[-] Chromaprint (fpcalc) missing. Attempting to install..." + echo "[-] Chromaprint (fpcalc) missing. Attempting to install..." >> "%BW_LOG%" where winget >nul 2>&1 if %ERRORLEVEL% EQU 0 ( winget install --id Chromaprint -e --silent >nul 2>&1 @@ -76,15 +71,15 @@ if %ERRORLEVEL% NEQ 0 ( ) where fpcalc >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( - echo [!] Chromaprint couldn't be installed automatically. Please install fpcalc manually. - echo [!] Chromaprint couldn't be installed automatically. Please install fpcalc manually. >> "%BW_LOG%" + echo "[!] Chromaprint couldn't be installed automatically. Please install fpcalc manually." + echo "[!] Chromaprint couldn't be installed automatically. Please install fpcalc manually." >> "%BW_LOG%" ) else ( - echo [+] Chromaprint installed successfully. - echo [+] Chromaprint installed successfully. >> "%BW_LOG%" + echo "[+] Chromaprint installed successfully." + echo "[+] Chromaprint installed successfully." >> "%BW_LOG%" ) ) else ( - echo [+] Chromaprint (fpcalc) already installed. - echo [+] Chromaprint (fpcalc) already installed. >> "%BW_LOG%" + echo "[+] Chromaprint (fpcalc) already installed." + echo "[+] Chromaprint (fpcalc) already installed." >> "%BW_LOG%" ) REM === Check Tor setting from settings.json === @@ -96,7 +91,7 @@ for /f "usebackq delims=" %%a in (`powershell -NoProfile -Command "try { $json = :tor_setting_done if /i "%USE_TOR%"=="true" ( - echo [i] Tor DNS enabled in settings.json - starting Tor... + echo "[i] Tor DNS enabled in settings.json - starting Tor..." REM === Start Tor process (hidden window) === set TOR_PATH=%~dp0SerienJunkie\Browser\TorBrowser\Tor\tor.exe @@ -104,7 +99,7 @@ if /i "%USE_TOR%"=="true" ( REM Check if Tor is already running (Port 9050 in use) netstat -an | find "9050" >nul if %ERRORLEVEL% EQU 0 ( - echo [?] Tor seems to be running already. Trying to Kill and restart the process... + echo "[?] Tor seems to be running already. Trying to Kill and restart the process..." taskkill /IM tor.exe /F >nul 2>&1 REM Wait until port 9050 is truly free set /a waitcount=0 @@ -116,8 +111,8 @@ if /i "%USE_TOR%"=="true" ( if !waitcount! LSS 55 ( goto waittorclose ) - echo [X] Port 9050 never became available after kill. Aborted execution. - echo [X] Port 9050 never became available after kill. Aborted execution. >> "%BW_LOG%" + echo "[X] Port 9050 never became available after kill. Aborted execution." + echo "[X] Port 9050 never became available after kill. Aborted execution." >> "%BW_LOG%" goto :handle_error ) ) @@ -133,38 +128,38 @@ if /i "%USE_TOR%"=="true" ( if %ERRORLEVEL% NEQ 0 ( set /a waitcount+=1 if !waitcount! LSS 30 goto waittorstart - echo [X] port 9050 was never opened! - echo [X] port 9050 was never opened! >> "%BW_LOG%" + echo "[X] port 9050 was never opened!" + echo "[X] port 9050 was never opened!" >> "%BW_LOG%" goto :handle_error ) - echo [+] Tor started successfully. - echo [+] Tor started successfully. >> "%BW_LOG%" + echo "[+] Tor started successfully." + echo "[+] Tor started successfully." >> "%BW_LOG%" ) REM === Start Python Script === set BW_DEBUG=1 python s.toBot.py set EXITCODE=%ERRORLEVEL% -echo [i] Python exit code: %EXITCODE% >> "%BW_LOG%" +echo "[i] Python exit code: %EXITCODE%" >> "%BW_LOG%" REM Immer pausieren, damit du die letzte Zeile siehst echo. -echo [i] Python exit code: %EXITCODE% +echo "[i] Python exit code: %EXITCODE%" pause REM === Cleanup and exit depending on Python exit code === if %EXITCODE% EQU 0 ( - echo [=] BingeWatcher exited normally. + echo "[=] BingeWatcher exited normally." if "%USE_TOR%"=="true" ( - echo [i] Cleaning up Tor process... + echo "[i] Cleaning up Tor process..." taskkill /IM tor.exe /F >nul 2>&1 ) endlocal & exit /b 0 ) else ( - echo [X] BingeWatcher exited with code %EXITCODE%. - echo [X] BingeWatcher exited with code %EXITCODE%. >> "%BW_LOG%" + echo "[X] BingeWatcher exited with code %EXITCODE%." + echo "[X] BingeWatcher exited with code %EXITCODE%." >> "%BW_LOG%" if "%USE_TOR%"=="true" ( - echo [i] Cleaning up Tor process... + echo "[i] Cleaning up Tor process..." taskkill /IM tor.exe /F >nul 2>&1 ) goto :handle_error @@ -172,8 +167,8 @@ if %EXITCODE% EQU 0 ( :handle_error echo. -echo [!] Script aborted. Review the messages above. -echo [!] Script aborted. Review the messages above. >> "%BW_LOG%" -echo [i] Log saved to: %BW_LOG% +echo "[!] Script aborted. Review the messages above." +echo "[!] Script aborted. Review the messages above." >> "%BW_LOG%" +echo "[i] Log saved to: %BW_LOG%" pause endlocal & exit /b 1 From 6e1784e2e07d57f9165e7dc201c75d403e1adf82 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:21:52 +0100 Subject: [PATCH 14/26] Improve Chromaprint install diagnostics in startup script --- start_watching.bat | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/start_watching.bat b/start_watching.bat index 75ed5fe..3d5a7a0 100644 --- a/start_watching.bat +++ b/start_watching.bat @@ -61,18 +61,34 @@ where fpcalc >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( echo "[-] Chromaprint (fpcalc) missing. Attempting to install..." echo "[-] Chromaprint (fpcalc) missing. Attempting to install..." >> "%BW_LOG%" + + set "BW_FPCALC_PATH=%ProgramFiles%\Chromaprint\fpcalc.exe" + if exist "%BW_FPCALC_PATH%" set "PATH=%PATH%;%ProgramFiles%\Chromaprint" + set "BW_FPCALC_PATH=%ProgramFiles(x86)%\Chromaprint\fpcalc.exe" + if exist "%BW_FPCALC_PATH%" set "PATH=%PATH%;%ProgramFiles(x86)%\Chromaprint" + where winget >nul 2>&1 if %ERRORLEVEL% EQU 0 ( - winget install --id Chromaprint -e --silent >nul 2>&1 + winget install --id Chromaprint.Chromaprint -e --silent >> "%BW_LOG%" 2>&1 + if %ERRORLEVEL% NEQ 0 ( + winget install --id Chromaprint -e --silent >> "%BW_LOG%" 2>&1 + ) ) where choco >nul 2>&1 if %ERRORLEVEL% EQU 0 ( - choco install chromaprint -y >nul 2>&1 + choco install chromaprint -y >> "%BW_LOG%" 2>&1 ) + + set "BW_FPCALC_PATH=%ProgramFiles%\Chromaprint\fpcalc.exe" + if exist "%BW_FPCALC_PATH%" set "PATH=%PATH%;%ProgramFiles%\Chromaprint" + set "BW_FPCALC_PATH=%ProgramFiles(x86)%\Chromaprint\fpcalc.exe" + if exist "%BW_FPCALC_PATH%" set "PATH=%PATH%;%ProgramFiles(x86)%\Chromaprint" + where fpcalc >nul 2>&1 if %ERRORLEVEL% NEQ 0 ( - echo "[!] Chromaprint couldn't be installed automatically. Please install fpcalc manually." - echo "[!] Chromaprint couldn't be installed automatically. Please install fpcalc manually." >> "%BW_LOG%" + echo "[^!] Chromaprint couldn't be installed automatically. Please install fpcalc manually." + echo "[^!] Chromaprint couldn't be installed automatically. Please install fpcalc manually." >> "%BW_LOG%" + echo "[^!] Check winget/choco output in %BW_LOG% for the exact install error." >> "%BW_LOG%" ) else ( echo "[+] Chromaprint installed successfully." echo "[+] Chromaprint installed successfully." >> "%BW_LOG%" @@ -167,8 +183,8 @@ if %EXITCODE% EQU 0 ( :handle_error echo. -echo "[!] Script aborted. Review the messages above." -echo "[!] Script aborted. Review the messages above." >> "%BW_LOG%" +echo "[^!] Script aborted. Review the messages above." +echo "[^!] Script aborted. Review the messages above." >> "%BW_LOG%" echo "[i] Log saved to: %BW_LOG%" pause endlocal & exit /b 1 From 957cf32f6c7be6997f2ee6a0590a4a9654e26969 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:38:59 +0100 Subject: [PATCH 15/26] Remove fpcalc auto-install attempts from startup script --- start_watching.bat | 45 +++++++++------------------------------------ 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/start_watching.bat b/start_watching.bat index 3d5a7a0..8a2be6a 100644 --- a/start_watching.bat +++ b/start_watching.bat @@ -57,45 +57,18 @@ if "%missing_modules%"=="" ( REM === Check for Chromaprint (fpcalc) === echo "[i] Checking Chromaprint (fpcalc)..." >> "%BW_LOG%" -where fpcalc >nul 2>&1 -if %ERRORLEVEL% NEQ 0 ( - echo "[-] Chromaprint (fpcalc) missing. Attempting to install..." - echo "[-] Chromaprint (fpcalc) missing. Attempting to install..." >> "%BW_LOG%" - set "BW_FPCALC_PATH=%ProgramFiles%\Chromaprint\fpcalc.exe" - if exist "%BW_FPCALC_PATH%" set "PATH=%PATH%;%ProgramFiles%\Chromaprint" - set "BW_FPCALC_PATH=%ProgramFiles(x86)%\Chromaprint\fpcalc.exe" - if exist "%BW_FPCALC_PATH%" set "PATH=%PATH%;%ProgramFiles(x86)%\Chromaprint" - - where winget >nul 2>&1 - if %ERRORLEVEL% EQU 0 ( - winget install --id Chromaprint.Chromaprint -e --silent >> "%BW_LOG%" 2>&1 - if %ERRORLEVEL% NEQ 0 ( - winget install --id Chromaprint -e --silent >> "%BW_LOG%" 2>&1 - ) - ) - where choco >nul 2>&1 - if %ERRORLEVEL% EQU 0 ( - choco install chromaprint -y >> "%BW_LOG%" 2>&1 - ) - - set "BW_FPCALC_PATH=%ProgramFiles%\Chromaprint\fpcalc.exe" - if exist "%BW_FPCALC_PATH%" set "PATH=%PATH%;%ProgramFiles%\Chromaprint" - set "BW_FPCALC_PATH=%ProgramFiles(x86)%\Chromaprint\fpcalc.exe" - if exist "%BW_FPCALC_PATH%" set "PATH=%PATH%;%ProgramFiles(x86)%\Chromaprint" +if exist "fpcalc.exe" ( + set "PATH=%CD%;%PATH%" +) - where fpcalc >nul 2>&1 - if %ERRORLEVEL% NEQ 0 ( - echo "[^!] Chromaprint couldn't be installed automatically. Please install fpcalc manually." - echo "[^!] Chromaprint couldn't be installed automatically. Please install fpcalc manually." >> "%BW_LOG%" - echo "[^!] Check winget/choco output in %BW_LOG% for the exact install error." >> "%BW_LOG%" - ) else ( - echo "[+] Chromaprint installed successfully." - echo "[+] Chromaprint installed successfully." >> "%BW_LOG%" - ) +where fpcalc >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo "[^!] Chromaprint (fpcalc) missing. Place fpcalc.exe in the SerienJunkie folder." + echo "[^!] Chromaprint (fpcalc) missing. Place fpcalc.exe in the SerienJunkie folder." >> "%BW_LOG%" ) else ( - echo "[+] Chromaprint (fpcalc) already installed." - echo "[+] Chromaprint (fpcalc) already installed." >> "%BW_LOG%" + echo "[+] Chromaprint (fpcalc) available." + echo "[+] Chromaprint (fpcalc) available." >> "%BW_LOG%" ) REM === Check Tor setting from settings.json === From 65bbe47a91566167722ae725bfcf6fc9fea068a5 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:54:08 +0100 Subject: [PATCH 16/26] Fix live end-skip update handling blocks --- SerienJunkie/s.toBot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index 9b57a49..f439fb9 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -1528,6 +1528,7 @@ def play_episodes_loop( except Exception: pass + try: upd = read_localstorage_value(driver, "bw_end_update") if upd: data = json.loads(upd) @@ -4044,6 +4045,7 @@ def main() -> None: except Exception: pass + try: upd = read_localstorage_value(driver, "bw_end_update") if upd: data = json.loads(upd) From dbdd3dc0528a544f0411bc8d2a912821ffc937e6 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:05:18 +0100 Subject: [PATCH 17/26] Add intro fingerprint timeout fallback skip --- SerienJunkie/s.toBot.py | 49 ++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index f439fb9..4a58480 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -415,19 +415,46 @@ def maybe_apply_intro_skip( seek_to_position(driver, intro_duration_seconds) return True + intro_fingerprint_key: str = build_intro_fingerprint_key(series, season) matched_key = read_intro_fingerprint_match(driver) - if matched_key: - intro_fingerprint_key: str = build_intro_fingerprint_key(series, season) - if matched_key == intro_fingerprint_key: - current_time_value: float = float( - driver.execute_script( - "return document.querySelector('video')?.currentTime || 0;" - ) - or 0 + if matched_key == intro_fingerprint_key: + current_time_value: float = float( + driver.execute_script( + "return document.querySelector('video')?.currentTime || 0;" ) - target_time: int = int(current_time_value + intro_duration_seconds) - seek_to_position(driver, target_time) - return True + or 0 + ) + target_time: int = int(current_time_value + intro_duration_seconds) + seek_to_position(driver, target_time) + return True + + fingerprint_duration_raw: int = int(entry.get("fingerprintDuration", 0) or 0) + fingerprint_duration_seconds: int = max(0, fingerprint_duration_raw) + fallback_window_seconds: int = ( + fingerprint_duration_seconds if fingerprint_duration_seconds > 0 else 12 + ) + fallback_trigger_seconds: int = min( + intro_duration_seconds, + max(6, fallback_window_seconds + 2), + ) + + try: + current_time_value: float = float( + driver.execute_script( + "return document.querySelector('video')?.currentTime || 0;" + ) + or 0 + ) + except Exception: + return intro_skip_applied + + if current_time_value >= fallback_trigger_seconds: + logging.info( + "Intro fingerprint match timeout for %s. Falling back to intro duration skip.", + intro_fingerprint_key, + ) + seek_to_position(driver, intro_duration_seconds) + return True return intro_skip_applied From fc21f9b273592fc025091f7a788ddb1f7aa742a2 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:17:04 +0100 Subject: [PATCH 18/26] Add automatic runtime fingerprint probe for video source --- SerienJunkie/s.toBot.py | 93 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index 4a58480..8ea059f 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -36,6 +36,9 @@ INTRO_FINGERPRINTS_FILE = os.path.join(SCRIPT_DIR, "intro_fingerprints.json") INTRO_UPLOAD_DIR = os.path.join(SCRIPT_DIR, "intro_uploads") + +INTRO_AUTO_MATCH_CACHE: Dict[str, bool] = {} + # === STREAMING PROVIDERS === STREAMING_PROVIDERS = { "s.to": { @@ -377,6 +380,80 @@ def resolve_intro_upload_path(filename: str) -> Optional[str]: return None +def _compare_fingerprint_prefix(stored_fingerprint: str, candidate_fingerprint: str) -> bool: + stored_value: str = str(stored_fingerprint or '').strip() + candidate_value: str = str(candidate_fingerprint or '').strip() + if not stored_value or not candidate_value: + return False + + prefix_length: int = min(120, len(stored_value), len(candidate_value)) + if prefix_length < 64: + return False + + return candidate_value[:prefix_length] == stored_value[:prefix_length] + + +def try_match_current_video_fingerprint( + driver: webdriver.Firefox, + series: str, + season: int, + fingerprint_value: str, +) -> bool: + fpcalc_binary: Optional[str] = _resolve_fpcalc_binary() + if not fpcalc_binary: + return False + + intro_fingerprint_key: str = build_intro_fingerprint_key(series, season) + + try: + current_src_value: str = str( + driver.execute_script( + "return document.querySelector('video')?.currentSrc || document.querySelector('video')?.src || '';" + ) + or '' + ).strip() + except Exception: + return False + + if not current_src_value: + return False + + cache_key: str = f"{intro_fingerprint_key}|{current_src_value}|{fingerprint_value[:120]}" + cached_result: Optional[bool] = INTRO_AUTO_MATCH_CACHE.get(cache_key) + if cached_result is not None: + return cached_result + + try: + result = subprocess.run( + [fpcalc_binary, '-json', current_src_value], + capture_output=True, + text=True, + check=False, + timeout=12, + ) + if result.returncode != 0: + INTRO_AUTO_MATCH_CACHE[cache_key] = False + return False + + payload: Dict[str, Any] = json.loads(result.stdout or '{}') + episode_fingerprint_value: str = str(payload.get('fingerprint', '') or '').strip() + is_match: bool = _compare_fingerprint_prefix( + stored_fingerprint=fingerprint_value, + candidate_fingerprint=episode_fingerprint_value, + ) + INTRO_AUTO_MATCH_CACHE[cache_key] = is_match + if is_match: + logging.info( + 'Automatic intro fingerprint match succeeded for %s.', + intro_fingerprint_key, + ) + return is_match + except Exception as e: + logging.debug('Automatic intro fingerprint match failed: %s', e) + INTRO_AUTO_MATCH_CACHE[cache_key] = False + return False + + def read_intro_fingerprint_match(driver: webdriver.Firefox) -> Optional[str]: try: driver.switch_to.default_content() @@ -428,6 +505,22 @@ def maybe_apply_intro_skip( seek_to_position(driver, target_time) return True + if try_match_current_video_fingerprint( + driver=driver, + series=series, + season=season, + fingerprint_value=fingerprint_value, + ): + current_time_value: float = float( + driver.execute_script( + "return document.querySelector('video')?.currentTime || 0;" + ) + or 0 + ) + target_time: int = int(current_time_value + intro_duration_seconds) + seek_to_position(driver, target_time) + return True + fingerprint_duration_raw: int = int(entry.get("fingerprintDuration", 0) or 0) fingerprint_duration_seconds: int = max(0, fingerprint_duration_raw) fallback_window_seconds: int = ( From 5dc6eb2205d585895c00f6347a1339f90c79ab1b Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:27:31 +0100 Subject: [PATCH 19/26] Add intro listening debug logs and relax fingerprint matching --- SerienJunkie/s.toBot.py | 51 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index 8ea059f..16996de 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -38,6 +38,7 @@ INTRO_AUTO_MATCH_CACHE: Dict[str, bool] = {} +INTRO_AUTO_LISTEN_LOGGED: Dict[str, bool] = {} # === STREAMING PROVIDERS === STREAMING_PROVIDERS = { @@ -386,11 +387,27 @@ def _compare_fingerprint_prefix(stored_fingerprint: str, candidate_fingerprint: if not stored_value or not candidate_value: return False - prefix_length: int = min(120, len(stored_value), len(candidate_value)) - if prefix_length < 64: + if stored_value == candidate_value: + return True + + prefix_length: int = min(96, len(stored_value), len(candidate_value)) + if prefix_length >= 48 and candidate_value[:prefix_length] == stored_value[:prefix_length]: + return True + + anchor_length: int = 32 + if len(stored_value) < anchor_length or len(candidate_value) < anchor_length: return False - return candidate_value[:prefix_length] == stored_value[:prefix_length] + anchor_offsets: List[int] = [0, 32, 64] + anchor_hits: int = 0 + for anchor_offset in anchor_offsets: + if anchor_offset + anchor_length > len(stored_value): + continue + anchor_value: str = stored_value[anchor_offset : anchor_offset + anchor_length] + if anchor_value and anchor_value in candidate_value: + anchor_hits += 1 + + return anchor_hits >= 2 def try_match_current_video_fingerprint( @@ -401,6 +418,7 @@ def try_match_current_video_fingerprint( ) -> bool: fpcalc_binary: Optional[str] = _resolve_fpcalc_binary() if not fpcalc_binary: + logging.debug('Intro fingerprint auto-match unavailable: fpcalc binary missing.') return False intro_fingerprint_key: str = build_intro_fingerprint_key(series, season) @@ -413,9 +431,11 @@ def try_match_current_video_fingerprint( or '' ).strip() except Exception: + logging.debug('Intro fingerprint auto-match: failed to read current video src.') return False if not current_src_value: + logging.debug('Intro fingerprint auto-match: empty video src while listening.') return False cache_key: str = f"{intro_fingerprint_key}|{current_src_value}|{fingerprint_value[:120]}" @@ -423,6 +443,11 @@ def try_match_current_video_fingerprint( if cached_result is not None: return cached_result + logging.info( + 'Start listening to episode intro fingerprint for %s.', + intro_fingerprint_key, + ) + try: result = subprocess.run( [fpcalc_binary, '-json', current_src_value], @@ -432,6 +457,11 @@ def try_match_current_video_fingerprint( timeout=12, ) if result.returncode != 0: + logging.debug( + 'Intro fingerprint auto-match fpcalc failed (%s): %s', + result.returncode, + (result.stderr or '').strip()[:300], + ) INTRO_AUTO_MATCH_CACHE[cache_key] = False return False @@ -505,6 +535,15 @@ def maybe_apply_intro_skip( seek_to_position(driver, target_time) return True + if not INTRO_AUTO_LISTEN_LOGGED.get(intro_fingerprint_key): + logging.info( + 'Start listening to episode intro for %s (introDuration=%ss, fingerprintDuration=%ss).', + intro_fingerprint_key, + intro_duration_seconds, + int(entry.get("fingerprintDuration", 0) or 0), + ) + INTRO_AUTO_LISTEN_LOGGED[intro_fingerprint_key] = True + if try_match_current_video_fingerprint( driver=driver, series=series, @@ -549,6 +588,12 @@ def maybe_apply_intro_skip( seek_to_position(driver, intro_duration_seconds) return True + logging.debug( + 'Intro fingerprint not matched yet for %s (currentTime=%.2f, fallbackAt=%ss).', + intro_fingerprint_key, + current_time_value, + fallback_trigger_seconds, + ) return intro_skip_applied From 1fc084cafd0d55c66e44a037f3e61f9cae155268 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:09:37 +0100 Subject: [PATCH 20/26] Log intro listening start conditions and skip reasons --- SerienJunkie/s.toBot.py | 66 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index 16996de..bbfd289 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -39,6 +39,7 @@ INTRO_AUTO_MATCH_CACHE: Dict[str, bool] = {} INTRO_AUTO_LISTEN_LOGGED: Dict[str, bool] = {} +INTRO_AUTO_REASON_LOGGED: Dict[str, str] = {} # === STREAMING PROVIDERS === STREAMING_PROVIDERS = { @@ -381,6 +382,14 @@ def resolve_intro_upload_path(filename: str) -> Optional[str]: return None +def _log_intro_reason_once(intro_fingerprint_key: str, reason_key: str, message: str) -> None: + previous_reason: Optional[str] = INTRO_AUTO_REASON_LOGGED.get(intro_fingerprint_key) + if previous_reason == reason_key: + return + INTRO_AUTO_REASON_LOGGED[intro_fingerprint_key] = reason_key + logging.info(message) + + def _compare_fingerprint_prefix(stored_fingerprint: str, candidate_fingerprint: str) -> bool: stored_value: str = str(stored_fingerprint or '').strip() candidate_value: str = str(candidate_fingerprint or '').strip() @@ -416,13 +425,16 @@ def try_match_current_video_fingerprint( season: int, fingerprint_value: str, ) -> bool: + intro_fingerprint_key: str = build_intro_fingerprint_key(series, season) fpcalc_binary: Optional[str] = _resolve_fpcalc_binary() if not fpcalc_binary: - logging.debug('Intro fingerprint auto-match unavailable: fpcalc binary missing.') + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key='missing_fpcalc', + message=f'Intro fingerprint listening inactive for {intro_fingerprint_key}: fpcalc missing.', + ) return False - intro_fingerprint_key: str = build_intro_fingerprint_key(series, season) - try: current_src_value: str = str( driver.execute_script( @@ -431,11 +443,19 @@ def try_match_current_video_fingerprint( or '' ).strip() except Exception: - logging.debug('Intro fingerprint auto-match: failed to read current video src.') + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key='video_src_read_failed', + message=f'Intro fingerprint listening waiting for video source for {intro_fingerprint_key}.', + ) return False if not current_src_value: - logging.debug('Intro fingerprint auto-match: empty video src while listening.') + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key='video_src_empty', + message=f'Intro fingerprint listening has empty video source for {intro_fingerprint_key}.', + ) return False cache_key: str = f"{intro_fingerprint_key}|{current_src_value}|{fingerprint_value[:120]}" @@ -457,9 +477,16 @@ def try_match_current_video_fingerprint( timeout=12, ) if result.returncode != 0: + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key='fpcalc_failed', + message=( + f'Intro fingerprint listening fpcalc failed for {intro_fingerprint_key} ' + f'(code {result.returncode}).' + ), + ) logging.debug( - 'Intro fingerprint auto-match fpcalc failed (%s): %s', - result.returncode, + 'Intro fingerprint auto-match fpcalc stderr: %s', (result.stderr or '').strip()[:300], ) INTRO_AUTO_MATCH_CACHE[cache_key] = False @@ -473,12 +500,18 @@ def try_match_current_video_fingerprint( ) INTRO_AUTO_MATCH_CACHE[cache_key] = is_match if is_match: + INTRO_AUTO_REASON_LOGGED.pop(intro_fingerprint_key, None) logging.info( 'Automatic intro fingerprint match succeeded for %s.', intro_fingerprint_key, ) return is_match except Exception as e: + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key='probe_exception', + message=f'Intro fingerprint listening probe error for {intro_fingerprint_key}.', + ) logging.debug('Automatic intro fingerprint match failed: %s', e) INTRO_AUTO_MATCH_CACHE[cache_key] = False return False @@ -509,22 +542,38 @@ def maybe_apply_intro_skip( return True entry = get_intro_fingerprint_entry(series, season) + intro_fingerprint_key: str = build_intro_fingerprint_key(series, season) if not entry: + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key='missing_entry', + message=f'Intro listening skipped for {intro_fingerprint_key}: no fingerprint entry found.', + ) return intro_skip_applied intro_duration_raw: int = int(entry.get("fullIntroDurationSeconds", 0) or 0) intro_duration_seconds: int = max(0, intro_duration_raw) if intro_duration_seconds <= 0: + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key='missing_intro_duration', + message=f'Intro listening skipped for {intro_fingerprint_key}: intro duration is 0.', + ) return intro_skip_applied fingerprint_value: str = str(entry.get("fingerprint", "") or "").strip() if not fingerprint_value: + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key='duration_only_mode', + message=f'Intro listening for {intro_fingerprint_key}: fingerprint empty, using duration skip only.', + ) seek_to_position(driver, intro_duration_seconds) return True - intro_fingerprint_key: str = build_intro_fingerprint_key(series, season) matched_key = read_intro_fingerprint_match(driver) if matched_key == intro_fingerprint_key: + INTRO_AUTO_REASON_LOGGED.pop(intro_fingerprint_key, None) current_time_value: float = float( driver.execute_script( "return document.querySelector('video')?.currentTime || 0;" @@ -581,6 +630,7 @@ def maybe_apply_intro_skip( return intro_skip_applied if current_time_value >= fallback_trigger_seconds: + INTRO_AUTO_REASON_LOGGED.pop(intro_fingerprint_key, None) logging.info( "Intro fingerprint match timeout for %s. Falling back to intro duration skip.", intro_fingerprint_key, From adefe5c3d31abe58acaa18e6fbe93387cc2bb384 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:22:05 +0100 Subject: [PATCH 21/26] Allow intro listening on near-zero resume positions --- SerienJunkie/s.toBot.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index bbfd289..b58ec53 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -1387,8 +1387,25 @@ def play_episodes_loop( apply_media_settings(driver, rate, vol) if position and position > 0: - seek_to_position(driver, position) - intro_skip_applied = True + resume_position_seconds: int = int(position) + if resume_position_seconds > 3: + logging.info( + "Intro listening disabled for this episode because resume position is %ss.", + resume_position_seconds, + ) + seek_to_position(driver, resume_position_seconds) + intro_skip_applied = True + else: + logging.info( + "Resume position %ss is too small, treating as fresh start for intro listening.", + resume_position_seconds, + ) + intro_skip_applied = maybe_apply_intro_skip( + driver=driver, + series=series, + season=current_season, + intro_skip_applied=intro_skip_applied, + ) else: intro_skip_applied = maybe_apply_intro_skip( driver=driver, From a799f3111f8fece8dcaeb1a025434fc1fc302f3f Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Sat, 7 Feb 2026 13:07:05 +0100 Subject: [PATCH 22/26] Wait for video source before intro fingerprint probe --- SerienJunkie/s.toBot.py | 53 ++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index b58ec53..6df2176 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -40,6 +40,7 @@ INTRO_AUTO_MATCH_CACHE: Dict[str, bool] = {} INTRO_AUTO_LISTEN_LOGGED: Dict[str, bool] = {} INTRO_AUTO_REASON_LOGGED: Dict[str, str] = {} +INTRO_VIDEO_SRC_LOGGED: Dict[str, bool] = {} # === STREAMING PROVIDERS === STREAMING_PROVIDERS = { @@ -390,6 +391,30 @@ def _log_intro_reason_once(intro_fingerprint_key: str, reason_key: str, message: logging.info(message) +def _wait_for_video_source(driver: webdriver.Firefox, timeout_seconds: int = 6) -> str: + deadline_value: float = time.time() + max(1, timeout_seconds) + while time.time() < deadline_value: + try: + src_value: str = str( + driver.execute_script( + "return document.querySelector('video')?.currentSrc || document.querySelector('video')?.src || '';" + ) + or "" + ).strip() + ready_state_value: int = int( + driver.execute_script( + "return document.querySelector('video')?.readyState || 0;" + ) + or 0 + ) + if src_value and ready_state_value >= 3: + return src_value + except Exception: + pass + time.sleep(0.25) + return "" + + def _compare_fingerprint_prefix(stored_fingerprint: str, candidate_fingerprint: str) -> bool: stored_value: str = str(stored_fingerprint or '').strip() candidate_value: str = str(candidate_fingerprint or '').strip() @@ -436,26 +461,20 @@ def try_match_current_video_fingerprint( return False try: - current_src_value: str = str( - driver.execute_script( - "return document.querySelector('video')?.currentSrc || document.querySelector('video')?.src || '';" - ) - or '' - ).strip() + current_src_value: str = _wait_for_video_source(driver) except Exception: - _log_intro_reason_once( - intro_fingerprint_key=intro_fingerprint_key, - reason_key='video_src_read_failed', - message=f'Intro fingerprint listening waiting for video source for {intro_fingerprint_key}.', - ) - return False + current_src_value = "" if not current_src_value: - _log_intro_reason_once( - intro_fingerprint_key=intro_fingerprint_key, - reason_key='video_src_empty', - message=f'Intro fingerprint listening has empty video source for {intro_fingerprint_key}.', - ) + if not INTRO_VIDEO_SRC_LOGGED.get(intro_fingerprint_key): + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key='video_src_wait_timeout', + message=( + f'Intro fingerprint listening waiting for video source for {intro_fingerprint_key}.' + ), + ) + INTRO_VIDEO_SRC_LOGGED[intro_fingerprint_key] = True return False cache_key: str = f"{intro_fingerprint_key}|{current_src_value}|{fingerprint_value[:120]}" From 31d8d02a387c4fc590e5a67ef3609b04a44d7d9d Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:53:43 +0100 Subject: [PATCH 23/26] Fallback to duration skip when fingerprint probe fails --- SerienJunkie/s.toBot.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index 6df2176..27335ee 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -7,7 +7,7 @@ import subprocess import tempfile import time -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Set from urllib.parse import unquote from selenium import webdriver @@ -657,6 +657,25 @@ def maybe_apply_intro_skip( seek_to_position(driver, intro_duration_seconds) return True + reason_value: Optional[str] = INTRO_AUTO_REASON_LOGGED.get(intro_fingerprint_key) + early_fallback_reasons: Set[str] = { + "missing_fpcalc", + "video_src_wait_timeout", + "fpcalc_failed", + "probe_exception", + } + if reason_value in early_fallback_reasons and current_time_value >= min( + 6, + intro_duration_seconds, + ): + INTRO_AUTO_REASON_LOGGED.pop(intro_fingerprint_key, None) + logging.info( + "Intro fingerprint probe unavailable for %s. Applying duration skip.", + intro_fingerprint_key, + ) + seek_to_position(driver, intro_duration_seconds) + return True + logging.debug( 'Intro fingerprint not matched yet for %s (currentTime=%.2f, fallbackAt=%ss).', intro_fingerprint_key, From 2e5116eb17c24eb11fa59d188b381304bf36113a Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Sat, 7 Feb 2026 18:57:16 +0100 Subject: [PATCH 24/26] Wait for video context before intro fingerprint probe --- SerienJunkie/s.toBot.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index 27335ee..cd32a4a 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -460,6 +460,26 @@ def try_match_current_video_fingerprint( ) return False + try: + if not ensure_video_context(driver): + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key="video_context_missing", + message=( + f"Intro fingerprint listening waiting for video context for {intro_fingerprint_key}." + ), + ) + return False + except Exception: + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key="video_context_missing", + message=( + f"Intro fingerprint listening waiting for video context for {intro_fingerprint_key}." + ), + ) + return False + try: current_src_value: str = _wait_for_video_source(driver) except Exception: @@ -660,6 +680,7 @@ def maybe_apply_intro_skip( reason_value: Optional[str] = INTRO_AUTO_REASON_LOGGED.get(intro_fingerprint_key) early_fallback_reasons: Set[str] = { "missing_fpcalc", + "video_context_missing", "video_src_wait_timeout", "fpcalc_failed", "probe_exception", From 913670a4ad859b38d00119f82ec3ecd21e2fd89e Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:31:05 +0100 Subject: [PATCH 25/26] Ensure video context before reading currentTime for intro skip --- SerienJunkie/s.toBot.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index cd32a4a..8749bda 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -613,6 +613,15 @@ def maybe_apply_intro_skip( matched_key = read_intro_fingerprint_match(driver) if matched_key == intro_fingerprint_key: INTRO_AUTO_REASON_LOGGED.pop(intro_fingerprint_key, None) + if not ensure_video_context(driver): + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key="video_context_missing", + message=( + f"Intro fingerprint listening waiting for video context for {intro_fingerprint_key}." + ), + ) + return intro_skip_applied current_time_value: float = float( driver.execute_script( "return document.querySelector('video')?.currentTime || 0;" @@ -638,6 +647,15 @@ def maybe_apply_intro_skip( season=season, fingerprint_value=fingerprint_value, ): + if not ensure_video_context(driver): + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key="video_context_missing", + message=( + f"Intro fingerprint listening waiting for video context for {intro_fingerprint_key}." + ), + ) + return intro_skip_applied current_time_value: float = float( driver.execute_script( "return document.querySelector('video')?.currentTime || 0;" @@ -659,6 +677,15 @@ def maybe_apply_intro_skip( ) try: + if not ensure_video_context(driver): + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key="video_context_missing", + message=( + f"Intro fingerprint listening waiting for video context for {intro_fingerprint_key}." + ), + ) + return intro_skip_applied current_time_value: float = float( driver.execute_script( "return document.querySelector('video')?.currentTime || 0;" From 355473abc29f28f80d9d775b14c78df145e15f62 Mon Sep 17 00:00:00 2001 From: Moritz Nonnemann <44306276+FieteGM@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:48:18 +0100 Subject: [PATCH 26/26] Skip fpcalc probe for non-audio video sources --- SerienJunkie/s.toBot.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/SerienJunkie/s.toBot.py b/SerienJunkie/s.toBot.py index 8749bda..4116589 100644 --- a/SerienJunkie/s.toBot.py +++ b/SerienJunkie/s.toBot.py @@ -497,6 +497,19 @@ def try_match_current_video_fingerprint( INTRO_VIDEO_SRC_LOGGED[intro_fingerprint_key] = True return False + lower_src_value: str = current_src_value.lower() + if lower_src_value.startswith("http") and not lower_src_value.endswith( + (".mp3", ".wav", ".flac", ".m4a", ".aac") + ): + _log_intro_reason_once( + intro_fingerprint_key=intro_fingerprint_key, + reason_key="video_src_unsupported", + message=( + f"Intro fingerprint listening cannot probe non-audio source for {intro_fingerprint_key}." + ), + ) + return False + cache_key: str = f"{intro_fingerprint_key}|{current_src_value}|{fingerprint_value[:120]}" cached_result: Optional[bool] = INTRO_AUTO_MATCH_CACHE.get(cache_key) if cached_result is not None: @@ -709,6 +722,7 @@ def maybe_apply_intro_skip( "missing_fpcalc", "video_context_missing", "video_src_wait_timeout", + "video_src_unsupported", "fpcalc_failed", "probe_exception", }