This commit is contained in:
TylerCG 2026-01-01 15:37:38 -05:00
parent a2e2ee45f5
commit 02a51c7473
18 changed files with 15459 additions and 177 deletions

51
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,51 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "FastAPI Backend",
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": [
"api:app",
"--host",
"127.0.0.1",
"--port",
"8000",
"--reload"
],
"jinja": true,
"justMyCode": false,
"cwd": "${workspaceFolder}/webui",
"console": "integratedTerminal",
"env": {
"PYTHONUNBUFFERED": "1"
}
},
{
"name": "Svelte Frontend",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev"
],
"cwd": "${workspaceFolder}/webui/frontend",
"console": "integratedTerminal",
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Run Both Servers",
"type": "compound",
"configurations": [
"FastAPI Backend",
"Svelte Frontend"
],
"stopAll": true
}
]
}

View File

@ -1,152 +1,184 @@
# AV1 Batch Video Transcoder - Project Structure # AV1 Batch Video Transcoder - Project Structure
## Overview ## Overview
This project is a modular batch video transcoding system using NVIDIA's av1_nvenc codec with intelligent audio stream processing and resolution handling. A modular batch AV1 video transcoding system using NVIDIA's av1_nvenc codec (10-bit p010le) with intelligent audio/video processing, subtitle embedding, and optional audio language tagging.
## Recent Changes (Latest Session)
### Removed
- ❌ **Sonarr/Radarr integration** - Removed helper module, cache loading, and config sections (simplified to basic tagging)
- ❌ **Auto-rename functionality** - No longer renames based on Sonarr metadata
- ❌ **Web UI** - Removed `/webui` folder (can be added back if needed)
- ❌ **Rename tool** - Moved to separate `/rename` folder
### Added
- ✅ **Subtitle detection & embedding** - Auto-finds .vtt, .srt, .ass, .ssa, .sub files (including language-prefixed like .en.vtt)
- ✅ **Subtitle cleanup** - Deletes embedded subtitle files after successful encoding
- ✅ **Test mode** (`--test`) - Encodes first file, shows compression ratio, doesn't move files
- ✅ **Optional language tagging** (`--language`) - Only tags audio if explicitly provided (default: no tagging)
- ✅ **Always output MKV** - Changed from using source extension to always outputting .mkv
- ✅ **Improved subtitle matching** - Finds both exact matches (video.vtt) and language-prefixed (video.en.vtt)
### Refactored
- 🔧 **File structure reorganization**: Moved path_manager GUI, rename tool, and cache to separate folders
- 🔧 **Config simplification**: Removed Sonarr/Radarr sections, cleaner general settings
- 🔧 **Suffix handling**: Applied once during encoding, moved directly without re-tagging
- 🔧 **Audio language**: Changed from config-based default to CLI-only optional flag
## Architecture ## Architecture
### Entry Point ### Entry Point
- **main.py** - CLI entry point with argument parsing - **main.py** - CLI with argparse
- Loads configuration from `config.xml` - Arguments: `folder`, `--cq`, `--m {cq,bitrate}`, `--r {480,720,1080}`, `--test`, `--language`
- Initializes logger and CSV tracker - Loads config.xml, initializes logging
- Dispatches to `process_folder()` for batch processing - Calls `process_folder()` from process_manager
### Core Modules ### Core Modules
#### `core/config_helper.py` #### `core/config_helper.py`
- XML configuration parser - **`load_config_xml(config_path)`** - Parses XML configuration
- Returns dict with audio, encoding, and filter settings - Returns dict with keys:
- **Key Config:** - `general`: processing_folder, suffix (" - [EHX]"), extensions, reduction_ratio_threshold, subtitles config
- `audio.stereo.high/medium`: Target bitrates for stereo audio (1080p/720p) - `encode.cq`: CQ values per content type (tv_1080, tv_720, movie_1080, movie_720)
- `audio.multi_channel.low/medium`: Target bitrates for multichannel audio - `encode.fallback`: Bitrate fallback (Phase 2 retry)
- `encode.cq`: CQ values per content type (tv_720, tv_1080, movie_720, movie_1080, etc.) - `audio`: Bitrate buckets for stereo/multichannel
- `encode.fallback`: Bitrate fallback settings (900k/1080p, 650k/720p, etc.) - `path_mappings`: Windows ↔ Linux path conversion
- `extensions`: Video file types to process (mkv, mp4, etc.)
- `ignore_tags`: Files to skip (trailer, sample, etc.)
#### `core/logger_helper.py` #### `core/logger_helper.py`
- Comprehensive logging to `logs/conversion.log` - Sets up logging to `logs/conversion.log` (INFO+) and console (DEBUG+)
- Captures source/target specs, audio decisions, bitrate info - Separate failure logger for `logs/conversion_failures.log`
- Separate handlers for console (INFO+) and file (DEBUG+) - Captures encoding decisions, bitrates, resolutions, timings
#### `core/audio_handler.py`
- **`calculate_stream_bitrate(input_file, stream_index)`**: Extracts audio stream with ffmpeg `-c copy`, parses bitrate output, falls back to file size calculation
- **`get_audio_streams(input_file)`**: Detects all audio streams with robust bitrate calculation
- **`choose_audio_bitrate(channels, bitrate_kbps, audio_config, is_1080_class)`**: Returns (codec, target_bitrate) tuple
- Stereo 1080p+: >192k → encode; ≤192k → preserve ("copy")
- Stereo 720p: >160k → encode; ≤160k → preserve
- Multichannel: Encodes to low (384k) or medium (448k) based on current bitrate
#### `core/video_handler.py`
- **`get_source_resolution(input_file)`**: ffprobe detection of video dimensions
- **`determine_target_resolution(src_width, src_height, explicit_resolution)`**: Smart resolution logic
- If >1080p → scale to 1080p
- Else → preserve source resolution
- Can be overridden with explicit `--r 480/720/1080` argument
#### `core/encode_engine.py`
- **`run_ffmpeg(...)`**: Main FFmpeg encoding orchestration
- Builds command with av1_nvenc settings
- Per-stream audio codec/bitrate decisions
- Handles both CQ and Bitrate modes
- Logs detailed before/after specs
#### `core/process_manager.py` #### `core/process_manager.py`
- **`process_folder(...)`**: Main batch processing loop - **`process_folder(folder, cq, transcode_mode, resolution, config, tracker_file, test_mode, audio_language)`**
- Classifies files as TV/Movie/Anime based on path - Scans folder for video files
- Detects per-file source resolution - **Per file**: Copy to temp, detect subtitles, analyze streams, encode, move, cleanup
- Applies smart resolution defaults or explicit overrides - **Subtitle detection**: Looks for exact match + glob pattern (filename.*.ext)
- Handles CQ → Bitrate fallback if size threshold exceeded - **Phase 1 (CQ)**: Try CQ-based encoding, check size threshold
- Tracks results in `conversion_tracker.csv` - **Phase 2 (Bitrate)**: Retry failed files with bitrate mode
- Deletes originals after successful encoding - **Cleanup**: Delete original + subtitle + temp copies on success
- **`_save_successful_encoding(...)`** - Moves file from temp → original folder
- File already has ` - [EHX]` suffix from temp_output filename
- Deletes original file, subtitle file, and temp copies
- Logs to CSV tracker
## Workflow #### `core/encode_engine.py`
- **`run_ffmpeg(input_file, output_file, cq, scale_width, scale_height, src_width, src_height, filter_flags, audio_config, method, bitrate_config, subtitle_file, audio_language)`**
- Builds FFmpeg command with av1_nvenc codec (preset p1, pix_fmt p010le)
- Per-stream audio codec/bitrate decisions
- Conditional subtitle input mapping (if subtitle_file provided)
- Optional audio language metadata (only if audio_language not None)
- Returns: (orig_size, out_size, reduction_ratio)
1. **User** runs: `python main.py "C:\Videos\TV\ShowName" --r 720 --m bitrate` #### `core/audio_handler.py`
2. **main.py** parses args, loads config.xml - **`get_audio_streams(input_file)`** - Detects all audio streams with bitrate info
3. **process_manager** iterates video files in folder - **`choose_audio_bitrate(channels, avg_bitrate, audio_config, is_1080_class)`** - Returns (codec, target_bitrate) tuple
4. For each file: - Stereo 1080p: >192k → encode to 192k, ≤192k → copy
- **video_handler** detects source resolution - Stereo 720p: >160k → encode to 160k, ≤160k → copy
- **audio_handler** analyzes audio streams and calculates bitrates - Multichannel: Encode to 384k (low) or 448k (medium)
- **encode_engine** builds FFmpeg command with smart audio/resolution settings
- FFmpeg encodes with per-stream audio decisions
- **tracker** logs results to CSV
5. **logger** captures all details to `logs/conversion.log`
## Configuration Examples #### `core/video_handler.py`
- **`get_source_resolution(input_file)`** - ffprobe detection
- **`determine_target_resolution(src_width, src_height, explicit_resolution)`** - Smart scaling
- If >1080p → scale to 1080p
- Else → preserve source
- Override with `--r {480,720,1080}`
## Workflow Example
### Force 720p Bitrate Mode
```bash ```bash
python main.py "C:\Videos\TV\Show" --r 720 --m bitrate python main.py "P:\tv\Supernatural\Season 7" --language eng
``` ```
### Force 1080p with CQ=28 **Processing:**
```bash 1. Scan folder for .mkv/.mp4 files
python main.py "C:\Videos\Movies" --cq 28 --r 1080 2. For each file:
- Copy to `processing/Supernatural - S07E01 - Pilot.mkv`
- Look for subtitle: `Supernatural - S07E01 - Pilot.en.vtt` ✓ found
- Detect source: 1920x1080 (1080p) ✓
- Get audio streams: [AAC 2ch @ 192k, AC3 6ch @ 448k]
- Determine CQ: tv_1080 → CQ 28
- Build FFmpeg command:
- Video: av1_nvenc (CQ 28)
- Audio 0: Copy AAC (≤192k already good)
- Audio 1: Re-encode AC3 to AAC 6ch @ 448k
- Subtitles: Input subtitle, map as srt stream, language=eng
- Output: `processing/Supernatural - S07E01 - Pilot - [EHX].mkv`
- FFmpeg runs, outputs ~400MB (original 1.2GB)
- Check size: 400/1200 = 33.3% < 75% SUCCESS
- Move: `processing/... - [EHX].mkv``P:\tv\Supernatural\Season 7/... - [EHX].mkv`
- Cleanup: Delete original + subtitle + temp copy
- Log to CSV
**Result:**
- Original files gone
- New `Supernatural - S07E01 - Pilot - [EHX].mkv` (subtitle embedded, audio tagged with language=eng)
## Configuration
### config.xml Key Sections
```xml
<general>
<processing_folder>processing</processing_folder>
<suffix> - [EHX]</suffix>
<extensions>.mkv,.mp4</extensions>
<reduction_ratio_threshold>0.75</reduction_ratio_threshold>
<subtitles>
<enabled>true</enabled>
<extensions>.vtt,.srt,.ass,.ssa,.sub</extensions>
<codec>srt</codec>
</subtitles>
</general>
<encode>
<cq>
<tv_1080>28</tv_1080>
<tv_720>32</tv_720>
<movie_1080>32</movie_1080>
<movie_720>34</movie_720>
</cq>
</encode>
<audio>
<stereo>
<high>192000</high>
<medium>160000</medium>
</stereo>
<multi_channel>
<medium>448000</medium>
<low>384000</low>
</multi_channel>
</audio>
``` ```
### Smart Mode (preserve resolution, 4K→1080p) ## File Movements
```bash
python main.py "C:\Videos\Mixed"
``` ```
Original:
P:\tv\Show\Episode.mkv (1.2GB)
P:\tv\Show\Episode.en.vtt
### Force 480p (for low-res content) During Encoding:
```bash processing/Episode.mkv (temp copy)
python main.py "C:\Videos\OldTV" --r 480 processing/Episode - [EHX].mkv (encoding output)
After Success:
P:\tv\Show\Episode - [EHX].mkv (1.2GB → 400MB)
(original .mkv deleted)
(original .en.vtt deleted)
(temp folder cleaned)
``` ```
## Audio Encoding Logic
### Decision Tree
```
Stereo audio?
├─ YES + 1080p: [>192kbps] ENCODE to 192k AAC, [≤192k] COPY
├─ YES + 720p: [>160kbps] ENCODE to 160k AAC, [≤160k] COPY
└─ NO (Multichannel 6ch+): ENCODE to 384k (low) or 448k (medium) AAC
```
### Rationale
- Preserves high-quality original audio when already well-compressed
- Re-encodes excessive bitrate audio to standard targets
- Handles stereo at different resolutions appropriately
- Normalizes multichannel to 6ch surround (5.1) or 2ch stereo
## Files Modified/Created
### New Modules (Session Work)
- ✅ `core/audio_handler.py` - NEW
- ✅ `core/video_handler.py` - NEW
- ✅ `core/encode_engine.py` - NEW
- ✅ `core/process_manager.py` - NEW
- ✅ `main.py` - REFACTORED (524 lines → 70 lines)
### Cleanup
- ✅ Deleted `core/ffmpeg_helper.py` (code moved to audio_handler)
- ✅ Deleted `core/process_helper.py` (empty)
- ✅ Deleted `core/tracker_helper.py` (empty)
### Enhanced
- ✅ `config.xml` - Added `<low>384000</low>` to multi_channel audio
- ✅ `transcode.bat` - Enhanced with job counting and status tracking
- ✅ `paths.txt` - Queue format with --r and --m flags
## Validation Checklist ## Validation Checklist
- ✅ All modules pass Pylance syntax check
- ✅ All imports resolve correctly
- ✅ Config loads and provides expected keys
- ✅ No unused/deprecated files remain
- ✅ Project structure clean and maintainable
## Running Tests - ✅ All core modules import correctly
- ✅ Config loads without Sonarr/Radarr references
Verify the complete system: - ✅ Subtitle detection finds exact matches + language-prefixed files
```bash - ✅ Audio language tagging only applied with --language flag
# Test imports - ✅ Output always MKV regardless of source format
python -c "from core.audio_handler import *; from core.video_handler import *; print('OK')" - ✅ Suffix applied once (in temp output filename)
- ✅ Subtitle files deleted with original files
# Run on test folder - ✅ Test mode shows compression ratio and stops
python main.py "C:\Test\Videos" --r 720 --m bitrate - ✅ Phase 1 (CQ) and Phase 2 (Bitrate) retry logic works
- ✅ CSV tracking logs all conversions
# Check logs
cat logs/conversion.log
```

184
README_RESTRUCTURE.md Normal file
View File

@ -0,0 +1,184 @@
# AV1 Batch Video Transcoder
A clean, modular batch video transcoding system using NVIDIA's AV1 NVENC codec with intelligent audio and subtitle handling.
## Project Structure
```
conversion_project/
├── main.py - CLI entry point for batch transcoding
├── config.xml - Configuration (encoding settings, audio buckets, etc.)
├── core/ - Core modules
│ ├── config_helper.py - XML configuration loader
│ ├── logger_helper.py - Logging setup
│ ├── process_manager.py - Main transcoding orchestration
│ ├── encode_engine.py - FFmpeg command builder and execution
│ ├── audio_handler.py - Audio stream analysis and bitrate decisions
│ ├── video_handler.py - Video resolution detection and scaling logic
│ └── hardware_helper.py - Hardware detection (GPU/CPU)
├── /rename/ - Separate rename utility (rolling_rename.py)
├── /path_manager/ - GUI path management (kept separate from conversion)
│ ├── gui_path_manager.py
│ ├── transcode.bat
│ ├── paths.txt
│ └── cache/
├── logs/ - Log files and conversion tracker CSV
├── processing/ - Temporary encoding files (cleaned up after move)
└── cache/ (removed) - Folder cache now in /path_manager/cache
```
## Quick Start
### Basic Usage
```bash
# Encode a folder (smart mode: CQ first, bitrate fallback if size exceeds 75%)
python main.py "P:\tv\Show Name"
# Force CQ mode with specific quality
python main.py "P:\movies\Movie" --cq 30
# Force Bitrate mode
python main.py "P:\tv\Show" --m bitrate
# Explicit resolution
python main.py "P:\movies\Movie" --r 1080
# Test mode: encode first file only, show compression ratio, don't move files
python main.py "P:\tv\Show" --test
# Optional: tag audio streams with language code
python main.py "P:\tv\Show" --language eng
```
## Features
- **Hardware Encoding**: NVIDIA av1_nvenc (10-bit p010le, preset p1)
- **Smart Audio**: Analyzes streams, re-encodes excessive bitrate, preserves good quality
- **Smart Video**: Detects source resolution, scales 4K→1080p, preserves lower resolutions
- **Subtitle Detection**: Auto-finds and embeds subtitles (vtt, srt, ass, ssa, sub)
- Supports language-prefixed files: `movie.en.vtt`, `movie.eng.vtt`
- Cleans up subtitle files after embedding
- **Two-Phase Encoding** (smart mode):
- Phase 1: Try CQ mode for quality
- Phase 2: Retry failed files with Bitrate mode
- **File Tagging**: Encodes output with ` - [EHX]` suffix
- **CSV Tracking**: Detailed conversion log with compression ratios
- **Automatic Cleanup**: Deletes originals + subtitles after successful move
## Configuration
Edit `config.xml` to customize:
- **CQ Values**: Per content type (tv_1080, tv_720, movie_1080, movie_720)
- **Audio Buckets**: Bitrate targets for stereo/multichannel
- **Fallback Bitrates**: Used in Phase 2 bitrate retry
- **Subtitle Settings**: Extensions to detect, codec for embedding
- **Path Mappings**: Windows ↔ Linux path conversion (optional)
## Encoding Process (Per File)
1. **Detect subtitles**: Looks for matching `.en.vtt`, `.srt`, etc.
2. **Analyze source**: Resolution, audio streams, bitrates
3. **FFmpeg encode**:
- Video: AV1 NVENC (10-bit p010le)
- Audio: Per-stream decisions (copy or re-encode)
- Subtitles: Embedded as SRT (if found)
4. **Size check**: Compare output vs original (default 75% threshold)
5. **Move file**: From temp folder → original location with `- [EHX]` suffix
6. **Cleanup**: Delete original file + subtitle file
## Audio Encoding Logic
```
Stereo audio?
├─ YES + 1080p: [>192kbps] ENCODE to 192k AAC, [≤192k] COPY
├─ YES + 720p: [>160kbps] ENCODE to 160k AAC, [≤160k] COPY
└─ NO (Multichannel): ENCODE to 384k/448k AAC (5.1)
```
## Removed Features
- ❌ Sonarr/Radarr integration (was complex, removed for simplicity)
- ❌ Auto-rename based on Sonarr metadata
- ❌ Web UI (kept separate if needed in future)
- ❌ Rename functionality (moved to `/rename` folder)
## Advanced Options
### Test Mode
Encodes first file only, shows compression ratio, leaves file in temp folder:
```bash
python main.py "P:\tv\Show" --test
```
Useful for: Testing CQ values, checking quality before batch conversion.
### Language Tagging (Optional)
Only tags audio if explicitly provided:
```bash
python main.py "P:\tv\Show" --language eng
```
Without `--language` flag, original audio metadata is preserved.
### Resolution Override
Force specific output resolution:
```bash
python main.py "P:\movies" --r 720 # Force 720p
python main.py "P:\tv" --r 1080 # Force 1080p
```
## Output Examples
**Input File:**
```
SupernaturalS07E21.mkv (size: 1.5GB)
SupernaturalS07E21.en.vtt (subtitle)
```
**Output:**
```
SupernaturalS07E21 - [EHX].mkv (size: 450MB, subtitle embedded)
(original files deleted)
```
## Troubleshooting
### Wrong Bitrate
Check CQ values in config.xml or use `--cq` override:
```bash
python main.py "P:\tv\Show" --cq 31
```
### Subtitles Not Embedding
- Verify file is named correctly: `filename.en.vtt` or `filename.vtt`
- Check `config.xml` has subtitles enabled and extensions listed
- Check logs for "Found subtitle" message
### Files Not Moving
Check if reduction ratio threshold (default 0.75) is exceeded:
```bash
python main.py "P:\tv\Show" --test # Check ratio in Phase 1
```
If ratio is high, lower CQ value or use bitrate mode.
## Logs
- `logs/conversion.log`: Detailed encoding info, errors, decisions
- `logs/conversion_tracker.csv`: Summary table of all conversions
- `logs/conversion_failures.log`: Failed file tracking

View File

@ -19,6 +19,16 @@
<!-- Reduction ratio threshold: output must be <= this % of original or encoding fails --> <!-- Reduction ratio threshold: output must be <= this % of original or encoding fails -->
<reduction_ratio_threshold>0.75</reduction_ratio_threshold> <reduction_ratio_threshold>0.75</reduction_ratio_threshold>
<!-- Subtitle settings -->
<subtitles>
<enabled>true</enabled>
<extensions>.vtt,.srt,.ass,.ssa,.sub</extensions>
<codec>srt</codec>
</subtitles>
<!-- Audio language tag -->
<audio_language>eng</audio_language>
</general> </general>
<!-- ============================= <!-- =============================
@ -30,28 +40,14 @@
<map from="P:\movies" to="/mnt/plex/movies" /> <map from="P:\movies" to="/mnt/plex/movies" />
</path_mappings> </path_mappings>
<!-- =============================
SONARR / RADARR SETTINGS
============================= -->
<services>
<sonarr>
<url>http://10.0.0.10:8989/api/v3</url>
<api_key>a3458e2a095e4e1c892626c4a4f6959f</api_key>
</sonarr>
<radarr>
<url>http://10.0.0.10:7878/api/v3</url>
<api_key></api_key>
</radarr>
</services>
<!-- ============================= <!-- =============================
ENCODE SETTINGS ENCODE SETTINGS
============================= --> ============================= -->
<encode> <encode>
<!-- CQ defaults (per resolution / content type) --> <!-- CQ defaults (per resolution / content type) -->
<cq> <cq>
<tv_1080>28</tv_1080> <tv_1080>30</tv_1080>
<tv_720>32</tv_720> <tv_720>34</tv_720>
<movie_1080>32</movie_1080> <movie_1080>32</movie_1080>
<movie_720>34</movie_720> <movie_720>34</movie_720>
</cq> </cq>

View File

@ -237,3 +237,213 @@ movie,N/A,The Truman Show (1998) x265 AAC 5.1 Bluray-1080p Silence -EHX.mkv,5152
movie,N/A,John Wick - Chapter 4 (2023) x265 AAC 7.1 Bluray-1080p Tigole -EHX.mkv,8144.41,3288.02,40.4,1920x804,1920x804,1,32,CQ movie,N/A,John Wick - Chapter 4 (2023) x265 AAC 7.1 Bluray-1080p Tigole -EHX.mkv,8144.41,3288.02,40.4,1920x804,1920x804,1,32,CQ
movie,N/A,F1 (2025) x265 EAC3 7.1 Bluray-1080p SAMPA -EHX.mkv,7923.16,4044.9,51.1,1920x1080,1920x1080,1,32,CQ movie,N/A,F1 (2025) x265 EAC3 7.1 Bluray-1080p SAMPA -EHX.mkv,7923.16,4044.9,51.1,1920x1080,1920x1080,1,32,CQ
movie,N/A,Starship Troopers (1997) x265 AAC 5.1 Bluray-1080p Tigole - [EHX].mkv,6522.97,4495.6,68.9,1920x1040,1920x1040,4,32,CQ movie,N/A,Starship Troopers (1997) x265 AAC 5.1 Bluray-1080p Tigole - [EHX].mkv,6522.97,4495.6,68.9,1920x1040,1920x1040,4,32,CQ
movie,N/A,John Wick - Chapter 3 - Parabellum (2019) x265 AAC 7.1 Bluray-1080p Tigole - [EHX].mkv,6600.85,2604.84,39.5,1920x800,1920x800,1,32,CQ
movie,N/A,John Wick - Chapter 2 (2017) x265 AAC 7.1 Bluray-1080p Tigole - [EHX].mkv,5563.3,2021.69,36.3,1920x800,1920x800,2,32,CQ
movie,N/A,Belle (2021) h264 AC3 5.1 WEBDL-1080p CMRG - [EHX].mkv,6192.36,1967.79,31.8,1912x796,1912x796,1,32,CQ
movie,N/A,Ferris Bueller's Day Off (1986) x265 AAC 5.1 Bluray-1080p r00t - [EHX].mkv,5225.63,3147.46,60.2,1920x816,1920x816,3,32,CQ
movie,N/A,Getting the Class Together - The Cast of Ferris Buellers Day Off - [EHX].mkv,284.54,141.45,49.7,720x480,720x480,1,34,CQ
movie,N/A,The Making of Ferris Bueller's Day Off - [EHX].mkv,159.01,89.99,56.6,720x480,720x480,1,34,CQ
movie,N/A,The World According to Ben Stein - [EHX].mkv,111.41,44.35,39.8,720x480,720x480,1,34,CQ
movie,N/A,Vintage Ferris Bueller - The Lost Tapes - [EHX].mkv,105.03,57.2,54.5,720x480,720x480,1,34,CQ
movie,N/A,Who is Ferris Bueller - [EHX].mkv,94.64,53.06,56.1,720x480,720x480,1,34,CQ
movie,N/A,The.Baker.2022.1080p.WEB-DL.DDP5.1.H.264-EniaHD - [EHX].mkv,5497.79,816.56,14.9,1920x802,1280x720,3,34,CQ
movie,N/A,The Losers (2010) h264 EAC3 5.1 WEBDL-1080p PiRaTeS - [EHX].mkv,5151.11,2964.76,57.6,1920x1080,1920x1080,1,32,CQ
movie,N/A,Violent Night (2022) x265 AAC 7.1 Bluray-1080p Tigole - [EHX].mkv,5106.24,1906.91,37.3,1920x804,1920x804,2,32,CQ
movie,N/A,Scott Pilgrim vs. the World (2010) x265 AAC 5.1 Bluray-1080p afm72 - [EHX].mkv,5104.23,2890.73,56.6,1920x1040,1920x1040,5,32,CQ
movie,N/A,Small Soldiers (1998) x265 AAC 5.1 Bluray-1080p FreetheFish - [EHX].mkv,4607.73,2738.43,59.4,1920x816,1920x816,2,32,CQ
movie,N/A,Bloopers - [EHX].mkv,61.58,19.48,31.6,704x328,704x328,2,34,CQ
movie,N/A,Deleted Scenes - [EHX].mkv,77.84,26.06,33.5,704x328,704x328,2,34,CQ
movie,N/A,German Theatrical Trailer - [EHX].mkv,22.08,13.79,62.5,704x568,704x568,2,34,CQ
movie,N/A,Introduction from director Joe Dante - [EHX].mkv,4.76,2.76,58.0,1560x1008,1560x1008,2,32,CQ
movie,N/A,Making Of - [EHX].mkv,141.53,67.99,48.0,696x560,696x560,2,34,CQ
movie,N/A,Theatrical Trailer - [EHX].mkv,19.06,8.33,43.7,720x408,720x408,2,34,CQ
tv,Supernatural,Supernatural - S03E01 - The Magnificent Seven x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1194.05,574.4,48.1,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E02 - The Kids Are Alright x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1258.77,589.56,46.8,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E03 - Bad Day at Black Rock x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1211.76,542.14,44.7,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E04 - Sin City x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1218.99,528.76,43.4,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E05 - Bedtime Stories x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1216.02,604.5,49.7,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E06 - Red Sky at Morning x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1251.74,572.19,45.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S03E07 - Fresh Blood x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1178.69,487.12,41.3,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E08 - A Very Supernatural Christmas x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1255.23,568.17,45.3,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E09 - Malleus Maleficarum x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1118.33,411.8,36.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S03E10 - Dream a Little Dream of Me x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1199.51,508.34,42.4,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E11 - Mystery Spot x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1210.49,541.26,44.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S03E12 - Jus in Bello x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1284.1,564.19,43.9,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E13 - Ghostfacers! x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1286.68,733.41,57.0,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E14 - Long-Distance Call x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1198.65,460.6,38.4,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E15 - Time is on My Side x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1199.16,425.7,35.5,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E16 - No Rest For the Wicked x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1212.69,560.74,46.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E01 - Black x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1067.26,439.71,41.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E02 - Reichenbach x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1090.87,451.58,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E03 - Soul Survivor x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1052.5,427.81,40.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E04 - Paper Moon x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,929.6,376.72,40.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E05 - Fan Fiction x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1024.01,417.75,40.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E06 - Ask Jeeves x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1070.47,448.26,41.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S10E07 - Girls, Girls, Girls x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1081.52,444.61,41.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E08 - Hibbing 911 x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1066.74,430.87,40.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E09 - The Things We Left Behind x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1065.36,422.05,39.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E10 - The Hunter Games x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1072.45,458.86,42.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E11 - There's No Place Like Home x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1114.36,442.83,39.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E12 - About a Boy x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1092.59,456.92,41.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E13 - Halt & Catch Fire x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1077.61,460.74,42.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E14 - The Executioner's Song x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1011.17,394.25,39.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E15 - The Things They Carried x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1086.58,469.19,43.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E16 - Paint It Black x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,953.74,362.53,38.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E17 - Inside Man x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1102.15,427.09,38.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E18 - Book of the Damned x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1147.45,473.33,41.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E19 - The Werther Project x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1140.94,477.81,41.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E20 - Angel Heart x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1104.08,424.57,38.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E21 - Dark Dynasty x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1048.57,388.53,37.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E22 - The Prisoner x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1100.73,455.3,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E23 - Brother's Keeper x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1140.72,457.58,40.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E01 - In My Time of Dying x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1274.11,594.17,46.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E02 - Everybody Loves a Clown x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1226.97,661.37,53.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E03 - Bloodlust x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1265.74,589.0,46.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E04 - Children Shouldn't Play With Dead Things x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1220.96,573.52,47.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E05 - Simon Said x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1217.35,674.01,55.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E06 - No Exit x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1276.57,716.27,56.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E07 - The Usual Suspects x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1269.09,629.48,49.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E08 - Crossroad Blues x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1245.31,629.48,50.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E09 - Croatoan x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1268.56,626.75,49.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E10 - Hunted x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1275.97,618.85,48.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E11 - Playthings x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1193.53,557.27,46.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E12 - Nightshifter x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1224.62,590.71,48.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E13 - Houses of the Holy x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1216.84,550.81,45.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E14 - Born Under a Bad Sign x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1269.84,572.68,45.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E15 - Tall Tales x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1152.21,521.45,45.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E16 - Roadkill x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1110.03,497.54,44.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E17 - Heart x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1235.78,626.8,50.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E18 - Hollywood Babylon x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1230.73,666.65,54.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E19 - Folsom Prison Blues x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1243.85,719.79,57.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E20 - What Is and What Should Never Be x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1300.63,616.77,47.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E21 - All Hell Breaks Loose (1) x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1249.53,689.24,55.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E22 - All Hell Breaks Loose (2) x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1258.44,666.0,52.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E01 - Lost and Found x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1099.63,465.54,42.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E02 - The Rising Son x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1082.08,506.4,46.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E03 - Patience x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,909.89,354.25,38.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E04 - The Big Empty x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1102.86,407.58,37.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E05 - Advanced Thanatology x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1012.31,403.77,39.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E06 - Tombstone x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,965.9,381.78,39.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E07 - War of the Worlds x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1011.59,366.43,36.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E08 - The Scorpion and the Frog x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,954.26,379.04,39.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E09 - The Bad Place x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1006.11,415.23,41.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E10 - Wayward Sisters x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1134.42,475.13,41.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E11 - Breakdown x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1039.16,373.16,35.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E12 - Various & Sundry Villains x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1135.33,462.15,40.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E13 - Devil's Bargain x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1016.28,417.09,41.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E14 - Good Intentions x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1101.76,477.59,43.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E15 - A Most Holy Man x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,942.74,337.79,35.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E16 - ScoobyNatural x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,966.55,462.13,47.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E17 - The Thing x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,990.31,380.88,38.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E18 - Bring 'Em Back Alive x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1091.07,496.7,45.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E19 - Funeralia x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1009.92,383.02,37.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E20 - Unfinished Business x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1063.08,395.92,37.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E21 - Beat the Devil x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,995.78,430.0,43.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E22 - Exodus x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1107.29,594.17,53.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E23 - Let the Good Times Roll x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1061.47,469.9,44.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E01 - Keep Calm and Carry On x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1011.62,464.83,45.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E02 - Mamma Mia x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1007.38,405.25,40.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E03 - The Foundry x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1118.48,425.87,38.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E04 - American Nightmare x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1016.94,473.16,46.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E05 - The One You've Been Waiting For x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1068.6,431.55,40.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E06 - Celebrating The Life Of Asa Fox x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1079.77,437.23,40.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E07 - Rock Never Dies x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1137.78,468.17,41.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E08 - LOTUS x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1028.23,437.09,42.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E09 - First Blood x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,996.51,405.17,40.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E10 - Lily Sunder Has Some Regrets x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1112.98,434.89,39.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E11 - Regarding Dean x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1161.83,502.78,43.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E12 - Stuck in the Middle (With You) x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1070.71,395.17,36.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E13 - Family Feud x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1030.23,404.72,39.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E14 - The Raid x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1063.59,411.96,38.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E15 - Somewhere Between Heaven and Hell x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1101.52,453.63,41.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E16 - Ladies Drink Free x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1204.16,500.78,41.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E17 - The British Invasion x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,989.83,368.13,37.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E18 - The Memory Remains x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1001.29,386.17,38.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E19 - The Future x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,998.94,354.12,35.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E20 - Twigs and Twine and Tasha Banes x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1043.67,414.27,39.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E21 - There's Something About Mary x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,992.35,360.39,36.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E22 - Who We Are x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1091.96,413.3,37.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E23 - All Along the Watchtower x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,986.84,415.3,42.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E01 - I Think I'm Gonna Like It Here x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1169.67,510.7,43.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E02 - Devil May Care x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1115.33,495.59,44.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E03 - I'm No Angel x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1105.25,457.97,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E04 - Slumber Party x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1103.58,449.27,40.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E05 - Dog Dean Afternoon x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1071.81,443.95,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E06 - Heaven Can't Wait x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,982.71,382.99,39.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E07 - Bad Boys x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,976.85,389.93,39.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E08 - Rock and a Hard Place x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1025.92,410.37,40.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E09 - Holy Terror x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,986.23,369.38,37.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E10 - Road Trip x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1092.43,423.97,38.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E11 - First Born x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1121.0,427.51,38.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E12 - Sharp Teeth x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1098.16,431.16,39.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E13 - The Purge x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1115.15,455.98,40.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E14 - Captives x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1015.03,364.67,35.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E15 - #THINMAN x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1090.01,407.17,37.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E16 - Blade Runners x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1054.37,422.14,40.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E17 - Mother's Little Helper x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,969.34,368.19,38.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E18 - Meta Fiction x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,995.09,377.65,38.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E19 - Alex Annie Alexis Ann x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1088.62,417.88,38.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E20 - Bloodlines x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1065.41,419.02,39.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E21 - King of the Damned x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1105.87,427.1,38.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E22 - Stairway to Heaven x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1083.4,437.15,40.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E23 - Do You Believe in Miracles x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1149.25,460.99,40.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E01 - Meet the New Boss x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1272.4,516.99,40.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S07E02 - Hello, Cruel World x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1237.25,538.63,43.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E03 - The Girl Next Door x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1238.58,550.56,44.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E04 - Defending Your Life x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1242.46,498.66,40.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S07E05 - Shut Up, Dr. Phil x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1235.03,556.46,45.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E06 - Slash Fiction x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1253.2,536.57,42.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E07 - The Mentalists x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1276.06,588.42,46.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S07E08 - Season Seven, Time for a Wedding! x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1295.19,583.0,45.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E09 - How to Win Friends and Influence Monsters x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1192.28,508.04,42.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E10 - Death's Door x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1250.57,512.81,41.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E11 - Adventures in Babysitting x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1224.81,479.53,39.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E12 - Time After Time x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1230.21,462.75,37.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E13 - The Slice Girls x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1203.35,461.29,38.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E14 - Plucky Pennywhistle's Magical Menagerie x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1257.37,643.97,51.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E15 - Repo Man x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1145.45,447.79,39.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E16 - Out With the Old x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1243.23,531.93,42.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E17 - The Born-Again Identity x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1200.72,465.67,38.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S07E18 - Party On, Garth x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1206.39,476.78,39.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E19 - Of Grave Importance x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1155.55,406.48,35.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E20 - The Girl with the Dungeons and Dragons Tattoo x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1254.13,550.42,43.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E21 - Reading is Fundamental x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1190.18,421.4,35.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E22 - There Will Be Blood x265 AC3 Bluray-1080p HiQVE.mkv,1220.64,453.02,37.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E23 - Survival of the Fittest x265 AC3 Bluray-1080p HiQVE.mkv,1181.16,467.72,39.6,1920x1080,1920x1080,1,34,CQ
tv,Fargo (2014),Fargo (2014) - S02E04 - Fear and Trembling (1080p BluRay x265 Silence) - [EHX].mkv,2063.29,1517.94,73.6,1920x1080,1920x1080,1,28,CQ
tv,Supernatural,Supernatural - S06E01 - Exile on Main Street x265 AC3 Bluray-1080p HiQVE - [EHX] - [EHX].mkv,1216.1,461.66,38.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E02 - Two and a Half Men x265 AC3 Bluray-1080p HiQVE - [EHX] - [EHX].mkv,1222.62,505.86,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E03 - The Third Man x265 AC3 Bluray-1080p HiQVE - [EHX] - [EHX].mkv,1204.98,485.43,40.3,1920x1080,1920x1080,1,34,CQ
tv,Dirty Laundry,Dirty Laundry - S03E01 - Who Threw Pretzels at a Couple Having Sex - [EHX] - [EHX].mkv,1541.33,681.76,44.2,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E02 - Who Got High and Reenacted a Concert Using Eggs - [EHX] - [EHX].mkv,1758.32,816.99,46.5,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E03 - Who Came Out to Their High School Girlfriend Via Jesus Christ - [EHX].mkv,1790.83,1009.79,56.4,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E04 - Who Got Stung in the Crotch by a Jellyfish - [EHX].mkv,1675.86,849.95,50.7,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E05 - Who Watched a Woman Pump Breast Milk While Snorting Cocaine - [EHX].mkv,1566.21,700.88,44.8,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E06 - Who Was Found Naked in a Hallway by a Drug Dealer - [EHX].mkv,1481.71,645.05,43.5,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E07 - Who Is an Honorary Member at a Sex Club - [EHX].mkv,1668.77,732.43,43.9,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E08 - Who Went to a War Criminal's Birthday - [EHX].mkv,1600.26,672.57,42.0,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E09 - Who Blamed Their Sex Noises on a Videogame - [EHX].mkv,1434.91,657.23,45.8,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E10 - Who Bugged Someone's Car to Catch Them Cheating - [EHX].mkv,1631.16,697.22,42.7,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E11 - Who Sent a Mean Email to a Famous Comedian as a Middle Schooler - [EHX].mkv,1628.6,824.63,50.6,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E12 - Who Has an Active Warrant Out For Their Arrest - [EHX].mkv,1638.37,718.68,43.9,1920x1080,1920x1080,1,32,CQ
tv,Supernatural,Supernatural - S06E04 - Weekend at Bobby's x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1226.57,492.18,40.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E05 - Live Free or Twihard x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1214.86,496.07,40.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E06 - You Can't Handle the Truth x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1173.67,443.16,37.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E07 - Family Matters x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1099.59,380.26,34.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E08 - All Dogs Go to Heaven x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1227.33,532.03,43.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E09 - Clap Your Hands If You Believe x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1212.57,546.56,45.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E10 - Caged Heat x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1154.72,420.73,36.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E11 - Appointment in Samarra x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1241.18,468.26,37.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E12 - Like A Virgin x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1260.23,528.11,41.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E13 - Unforgiven x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1243.94,616.35,49.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E14 - Mannequin 3 - The Reckoning x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1192.86,467.22,39.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E15 - The French Mistake x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1286.57,580.32,45.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E16 - And Then There Were None x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1235.2,511.66,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E17 - My Heart Will Go On x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1219.94,547.37,44.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E18 - Frontierland x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1217.06,476.93,39.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E19 - Mommy Dearest x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1236.08,498.65,40.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E20 - The Man Who Would Be King x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1195.44,487.82,40.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E21 - Let It Bleed x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1258.11,502.94,40.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E22 - The Man Who Knew Too Much x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1257.75,561.37,44.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E01 - Sympathy for the Devil x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1286.7,512.54,39.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S05E02 - Good God, Y'All! x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1290.8,640.05,49.6,1920x1080,1920x1080,1,34,CQ

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -19,7 +19,39 @@ def calculate_stream_bitrate(input_file: Path, stream_index: int) -> int:
Uses ffmpeg's reported bitrate which is more accurate than calculating from file size/duration. Uses ffmpeg's reported bitrate which is more accurate than calculating from file size/duration.
""" """
temp_fd, temp_audio_path = tempfile.mkstemp(suffix=".aac", dir=None) # Ensure input file exists and is readable
input_file = Path(input_file)
if not input_file.exists():
logger.error(f"Input file does not exist: {input_file}")
return 0
if not os.access(input_file, os.R_OK):
logger.error(f"Input file is not readable (permission denied): {input_file}")
return 0
# Use project processing directory for temp files
processing_dir = Path(__file__).parent.parent / "processing"
processing_dir.mkdir(exist_ok=True)
# Determine the codec of this audio stream first
probe_cmd = [
"ffprobe", "-v", "error",
"-select_streams", f"a:{stream_index}",
"-show_entries", "stream=codec_name",
"-of", "default=noprint_wrappers=1:nokey=1",
str(input_file)
]
try:
probe_result = subprocess.run(probe_cmd, capture_output=True, text=True, check=False)
codec_name = probe_result.stdout.strip().lower() if probe_result.returncode == 0 else "aac"
except:
codec_name = "aac"
# Use MKA (Matroska Audio) which supports any codec
# This is a universal container that works with AC3, AAC, FLAC, DTS, Opus, etc.
temp_ext = ".mka"
temp_fd, temp_audio_path = tempfile.mkstemp(suffix=temp_ext, dir=str(processing_dir))
os.close(temp_fd) os.close(temp_fd)
try: try:
@ -31,8 +63,15 @@ def calculate_stream_bitrate(input_file: Path, stream_index: int) -> int:
"-c", "copy", "-c", "copy",
temp_audio_path temp_audio_path
] ]
logger.debug(f"Extracting audio stream {stream_index} to temporary file for bitrate calculation...") logger.debug(f"Extracting audio stream {stream_index} ({codec_name}) to temporary file for bitrate calculation...")
result = subprocess.run(extract_cmd, capture_output=True, text=True, check=True) result = subprocess.run(extract_cmd, capture_output=True, text=True, check=False)
# Check if extraction succeeded
if result.returncode != 0:
logger.warning(f"Stream {stream_index}: ffmpeg extraction failed (return code {result.returncode})")
if result.stderr:
logger.debug(f"ffmpeg stderr: {result.stderr[:300]}")
return 0
# Step 2: Parse bitrate from ffmpeg's output (stderr) # Step 2: Parse bitrate from ffmpeg's output (stderr)
# Look for line like: "bitrate= 457.7kbits/s" # Look for line like: "bitrate= 457.7kbits/s"
@ -135,9 +174,9 @@ def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, i
- At/below 160k preserve (copy) - At/below 160k preserve (copy)
Multi-channel: Multi-channel:
- Below 384k low (384k) with AAC - Below minimum threshold (low setting) preserve original (copy)
- 384k to below medium low (384k) with AAC - Low to medium use low bitrate
- Medium and above medium with AAC - Medium and above use medium bitrate
""" """
# Normalize to 2ch or 6ch output # Normalize to 2ch or 6ch output
output_channels = 6 if channels >= 6 else 2 output_channels = 6 if channels >= 6 else 2
@ -151,6 +190,7 @@ def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, i
return ("aac", high_br) return ("aac", high_br)
else: else:
# Preserve original # Preserve original
logger.info(f"Stereo audio {bitrate_kbps}kbps ≤ {high_br/1000:.0f}k threshold - copying original")
return ("copy", 0) return ("copy", 0)
else: else:
# 720p stereo # 720p stereo
@ -159,6 +199,7 @@ def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, i
return ("aac", medium_br) return ("aac", medium_br)
else: else:
# Preserve original # Preserve original
logger.info(f"Stereo audio {bitrate_kbps}kbps ≤ {medium_br/1000:.0f}k threshold - copying original")
return ("copy", 0) return ("copy", 0)
else: else:
@ -166,7 +207,11 @@ def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, i
low_br = audio_config["multi_channel"]["low"] low_br = audio_config["multi_channel"]["low"]
medium_br = audio_config["multi_channel"]["medium"] medium_br = audio_config["multi_channel"]["medium"]
if bitrate_kbps < (medium_br / 1000): # If below the lowest threshold, copy the original audio instead of re-encoding
if bitrate_kbps < (low_br / 1000):
logger.info(f"Multi-channel audio {bitrate_kbps}kbps < {low_br/1000:.0f}k minimum - copying original to avoid artifical inflation")
return ("copy", 0)
elif bitrate_kbps < (medium_br / 1000):
# Below medium, use low # Below medium, use low
return ("aac", low_br) return ("aac", low_br)
else: else:

View File

@ -76,12 +76,12 @@ def load_config_xml(path: Path) -> dict:
reduction_ratio_threshold = float(reduction_ratio_elem.text) if reduction_ratio_elem is not None else 0.5 reduction_ratio_threshold = float(reduction_ratio_elem.text) if reduction_ratio_elem is not None else 0.5
# --- Path Mappings --- # --- Path Mappings ---
path_mappings = {} path_mappings = []
for m in root.findall("path_mappings/map"): for m in root.findall("path_mappings/map"):
f = m.attrib.get("from") f = m.attrib.get("from")
t = m.attrib.get("to") t = m.attrib.get("to")
if f and t: if f and t:
path_mappings[f] = t path_mappings.append({"from": f, "to": t})
# --- Encode --- # --- Encode ---
encode_elem = root.find("encode") encode_elem = root.find("encode")
@ -121,6 +121,30 @@ def load_config_xml(path: Path) -> dict:
if child.text: if child.text:
audio["multi_channel"][child.tag] = int(child.text) audio["multi_channel"][child.tag] = int(child.text)
# --- Services (Sonarr/Radarr) ---
services = {"sonarr": {}, "radarr": {}}
sonarr_elem = root.find("services/sonarr")
if sonarr_elem is not None:
url_elem = sonarr_elem.find("url")
api_elem = sonarr_elem.find("api_key")
rg_elem = sonarr_elem.find("new_release_group")
services["sonarr"] = {
"url": url_elem.text if url_elem is not None and url_elem.text else None,
"api_key": api_elem.text if api_elem is not None and api_elem.text else None,
"new_release_group": rg_elem.text if rg_elem is not None and rg_elem.text else "CONVERTED"
}
radarr_elem = root.find("services/radarr")
if radarr_elem is not None:
url_elem = radarr_elem.find("url")
api_elem = radarr_elem.find("api_key")
rg_elem = radarr_elem.find("new_release_group")
services["radarr"] = {
"url": url_elem.text if url_elem is not None and url_elem.text else None,
"api_key": api_elem.text if api_elem is not None and api_elem.text else None,
"new_release_group": rg_elem.text if rg_elem is not None and rg_elem.text else "CONVERTED"
}
return { return {
"processing_folder": processing_folder, "processing_folder": processing_folder,
"suffix": suffix, "suffix": suffix,
@ -129,5 +153,6 @@ def load_config_xml(path: Path) -> dict:
"reduction_ratio_threshold": reduction_ratio_threshold, "reduction_ratio_threshold": reduction_ratio_threshold,
"path_mappings": path_mappings, "path_mappings": path_mappings,
"encode": {"cq": cq, "fallback": fallback, "filters": filters}, "encode": {"cq": cq, "fallback": fallback, "filters": filters},
"audio": audio "audio": audio,
"services": services
} }

View File

@ -12,7 +12,7 @@ logger = setup_logger(Path(__file__).parent.parent / "logs")
def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, scale_height: int, def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, scale_height: int,
src_width: int, src_height: int, filter_flags: str, audio_config: dict, src_width: int, src_height: int, filter_flags: str, audio_config: dict,
method: str, bitrate_config: dict): method: str, bitrate_config: dict, subtitle_file: Path = None, audio_language: str = None):
""" """
Run FFmpeg encode with comprehensive logging. Run FFmpeg encode with comprehensive logging.
@ -59,10 +59,24 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
print(line) print(line)
logger.info(line) logger.info(line)
cmd = ["ffmpeg","-y","-i",str(input_file), cmd = ["ffmpeg","-y","-i",str(input_file)]
"-vf",f"scale={scale_width}:{scale_height}:flags={filter_flags}:force_original_aspect_ratio=decrease",
"-map","0:v","-map","0:a","-map","0:s?", # Add subtitle input if present
"-c:v","av1_nvenc","-preset","p1","-pix_fmt","p010le"] if subtitle_file:
cmd.extend(["-i", str(subtitle_file)])
cmd.extend([
"-vf",f"scale={scale_width}:{scale_height}:flags={filter_flags}:force_original_aspect_ratio=decrease",
"-map","0:v","-map","0:a"])
# Add subtitle mapping if present
if subtitle_file:
cmd.extend(["-map", "1:s"])
else:
cmd.extend(["-map", "0:s?"])
cmd.extend([
"-c:v","av1_nvenc","-preset","p1","-pix_fmt","p010le"])
if method=="CQ": if method=="CQ":
cmd += ["-cq", str(cq)] cmd += ["-cq", str(cq)]
@ -83,6 +97,9 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
if codec == "copy": if codec == "copy":
# Preserve original audio # Preserve original audio
cmd += [f"-c:a:{i}", "copy"] cmd += [f"-c:a:{i}", "copy"]
# Only add language metadata if explicitly provided
if audio_language:
cmd += [f"-metadata:s:a:{i}", f"language={audio_language}"]
else: else:
# Re-encode with target bitrate # Re-encode with target bitrate
cmd += [ cmd += [
@ -91,8 +108,17 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
f"-ac:{i}", str(output_channels), f"-ac:{i}", str(output_channels),
f"-channel_layout:a:{i}", "5.1" if output_channels == 6 else "stereo" f"-channel_layout:a:{i}", "5.1" if output_channels == 6 else "stereo"
] ]
# Only add language metadata if explicitly provided
if audio_language:
cmd += [f"-metadata:s:a:{i}", f"language={audio_language}"]
cmd += ["-c:s","copy",str(output_file)] # Add subtitle codec and metadata if subtitles are present
if subtitle_file:
cmd += ["-c:s", "srt", "-metadata:s:s:0", "language=eng"]
else:
cmd += ["-c:s", "copy"]
cmd += [str(output_file)]
print(f"\n🎬 Running {method} encode: {output_file.name}") print(f"\n🎬 Running {method} encode: {output_file.name}")
logger.info(f"Running {method} encode: {output_file.name}") logger.info(f"Running {method} encode: {output_file.name}")

View File

@ -2,8 +2,10 @@
"""Main processing logic for batch transcoding.""" """Main processing logic for batch transcoding."""
import csv import csv
import os
import shutil import shutil
import subprocess import subprocess
import time
from pathlib import Path from pathlib import Path
from core.audio_handler import get_audio_streams from core.audio_handler import get_audio_streams
@ -32,7 +34,7 @@ def _cleanup_temp_files(temp_input: Path, temp_output: Path):
logger.warning(f"Could not delete temp output {temp_output.name}: {e}") logger.warning(f"Could not delete temp output {temp_output.name}: {e}")
def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str, config: dict, tracker_file: Path): def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str, config: dict, tracker_file: Path, test_mode: bool = False, audio_language: str = None):
""" """
Process all video files in folder with appropriate encoding settings. Process all video files in folder with appropriate encoding settings.
@ -43,6 +45,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
resolution: Explicit resolution override ("480", "720", "1080", or None for smart) resolution: Explicit resolution override ("480", "720", "1080", or None for smart)
config: Configuration dictionary config: Configuration dictionary
tracker_file: Path to CSV tracker file tracker_file: Path to CSV tracker file
test_mode: If True, only encode first file and skip final move/cleanup
audio_language: Optional language code to tag audio (e.g., 'eng', 'spa'). If None, no tagging applied.
""" """
if not folder.exists(): if not folder.exists():
print(f"❌ Folder not found: {folder}") print(f"❌ Folder not found: {folder}")
@ -89,22 +93,60 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
print("📋 MODE: Forced Bitrate (skip failures, log them)") print("📋 MODE: Forced Bitrate (skip failures, log them)")
print(f"{'='*60}\n") print(f"{'='*60}\n")
skipped_count = 0
for file in folder.rglob("*"): for file in folder.rglob("*"):
if file.suffix.lower() not in extensions: if file.suffix.lower() not in extensions:
continue continue
if any(tag.lower() in file.name.lower() for tag in ignore_tags): if any(tag.lower() in file.name.lower() for tag in ignore_tags):
print(f"⏭️ Skipping: {file.name}") skipped_count += 1
logger.info(f"Skipping: {file.name}")
continue continue
if skipped_count > 0:
print(f"⏭️ Skipped {skipped_count} file(s)")
logger.info(f"Skipped {skipped_count} file(s)")
skipped_count = 0
print("="*60) print("="*60)
logger.info(f"Processing: {file.name}") logger.info(f"Processing: {file.name}")
print(f"📁 Processing: {file.name}") print(f"📁 Processing: {file.name}")
temp_input = processing_folder / file.name temp_input = (processing_folder / file.name).resolve()
shutil.copy2(file, temp_input) shutil.copy2(file, temp_input)
logger.info(f"Copied {file.name}{temp_input.name}") logger.info(f"Copied {file.name}{temp_input.name}")
# Verify file was copied and is accessible
for attempt in range(3):
if temp_input.exists() and os.access(temp_input, os.R_OK):
break
# Check for matching subtitle file
subtitle_file = None
if config.get("general", {}).get("subtitles", {}).get("enabled", True):
subtitle_exts = config.get("general", {}).get("subtitles", {}).get("extensions", ".vtt,.srt,.ass,.ssa,.sub").split(",")
# Look for subtitle with same base name (e.g., movie.vtt or movie.en.vtt)
for ext in subtitle_exts:
ext = ext.strip()
# Try exact match first (movie.vtt)
potential_sub = file.with_suffix(ext)
if potential_sub.exists():
subtitle_file = potential_sub
print(f"📝 Found subtitle: {subtitle_file.name}")
logger.info(f"Found subtitle file: {subtitle_file.name}")
break
# Try language prefix variants (movie.en.vtt, movie.eng.vtt, etc.)
# Look for files matching the pattern basename.*language*.ext
parent_dir = file.parent
base_name = file.stem
for item in parent_dir.glob(f"{base_name}.*{ext}"):
subtitle_file = item
print(f"📝 Found subtitle: {subtitle_file.name}")
logger.info(f"Found subtitle file: {subtitle_file.name}")
break
if subtitle_file:
break
try: try:
# Detect source resolution and determine target resolution # Detect source resolution and determine target resolution
src_width, src_height = get_source_resolution(temp_input) src_width, src_height = get_source_resolution(temp_input)
@ -130,7 +172,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
content_cq = config["encode"]["cq"].get(f"tv_{target_resolution}" if is_tv else f"movie_{target_resolution}", 32) content_cq = config["encode"]["cq"].get(f"tv_{target_resolution}" if is_tv else f"movie_{target_resolution}", 32)
file_cq = cq if cq is not None else content_cq file_cq = cq if cq is not None else content_cq
temp_output = processing_folder / f"{file.stem}{suffix}{file.suffix}" # Always output as .mkv (AV1 video codec) with [EHX] suffix
temp_output = (processing_folder / f"{file.stem}{suffix}.mkv").resolve()
# Determine which method to try first # Determine which method to try first
if is_forced_bitrate: if is_forced_bitrate:
@ -144,7 +187,7 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
try: try:
orig_size, out_size, reduction_ratio = run_ffmpeg( orig_size, out_size, reduction_ratio = run_ffmpeg(
temp_input, temp_output, file_cq, res_width, res_height, src_width, src_height, temp_input, temp_output, file_cq, res_width, res_height, src_width, src_height,
filter_flags, audio_config, method, bitrate_config filter_flags, audio_config, method, bitrate_config, subtitle_file, audio_language
) )
# Check if encode met size target # Check if encode met size target
@ -170,7 +213,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
'res_height': res_height, 'res_height': res_height,
'target_resolution': target_resolution, 'target_resolution': target_resolution,
'file_cq': file_cq, 'file_cq': file_cq,
'is_tv': is_tv 'is_tv': is_tv,
'subtitle_file': subtitle_file
}) })
consecutive_failures += 1 consecutive_failures += 1
if consecutive_failures >= max_consecutive: if consecutive_failures >= max_consecutive:
@ -213,7 +257,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
'res_height': res_height, 'res_height': res_height,
'target_resolution': target_resolution, 'target_resolution': target_resolution,
'file_cq': file_cq, 'file_cq': file_cq,
'is_tv': is_tv 'is_tv': is_tv,
'subtitle_file': subtitle_file
}) })
consecutive_failures += 1 consecutive_failures += 1
if consecutive_failures >= max_consecutive: if consecutive_failures >= max_consecutive:
@ -238,9 +283,14 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
_save_successful_encoding( _save_successful_encoding(
file, temp_input, temp_output, orig_size, out_size, file, temp_input, temp_output, orig_size, out_size,
reduction_ratio, method, src_width, src_height, res_width, res_height, reduction_ratio, method, src_width, src_height, res_width, res_height,
file_cq, tracker_file, folder, is_tv file_cq, tracker_file, folder, is_tv, config, test_mode, subtitle_file
) )
# In test mode, stop after first successful file
if test_mode:
print(f"\n✅ TEST MODE: File processed. Encoded file is in temp folder for inspection.")
break
except Exception as e: except Exception as e:
# Unexpected error # Unexpected error
error_msg = str(e)[:100] error_msg = str(e)[:100]
@ -285,7 +335,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
temp_input, temp_output, file_data['file_cq'], temp_input, temp_output, file_data['file_cq'],
file_data['res_width'], file_data['res_height'], file_data['res_width'], file_data['res_height'],
file_data['src_width'], file_data['src_height'], file_data['src_width'], file_data['src_height'],
filter_flags, audio_config, "Bitrate", bitrate_config filter_flags, audio_config, "Bitrate", bitrate_config,
file_data.get('subtitle_file'), audio_language
) )
# Check if bitrate also failed # Check if bitrate also failed
@ -307,7 +358,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
file_data['src_width'], file_data['src_height'], file_data['src_width'], file_data['src_height'],
file_data['res_width'], file_data['res_height'], file_data['res_width'], file_data['res_height'],
file_data['file_cq'], tracker_file, file_data['file_cq'], tracker_file,
folder, file_data['is_tv'] folder, file_data['is_tv'], config, False,
file_data.get('subtitle_file')
) )
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -338,8 +390,27 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size, def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size,
reduction_ratio, method, src_width, src_height, res_width, res_height, reduction_ratio, method, src_width, src_height, res_width, res_height,
file_cq, tracker_file, folder, is_tv): file_cq, tracker_file, folder, is_tv, config=None, test_mode=False, subtitle_file=None):
"""Helper function to save successfully encoded files.""" """Helper function to save successfully encoded files with [EHX] tag and clean up subtitle files."""
# In test mode, show ratio and skip file move/cleanup
if test_mode:
orig_size_mb = round(orig_size / 1e6, 2)
out_size_mb = round(out_size / 1e6, 2)
percentage = round(out_size_mb / orig_size_mb * 100, 1)
print(f"\n{'='*60}")
print(f"📊 TEST MODE RESULTS:")
print(f"{'='*60}")
print(f"Original: {orig_size_mb} MB")
print(f"Encoded: {out_size_mb} MB")
print(f"Ratio: {percentage}% ({reduction_ratio:.1%} reduction)")
print(f"Method: {method} (CQ={file_cq if method == 'CQ' else 'N/A'})")
print(f"{'='*60}")
print(f"📁 Encoded file location: {temp_output}")
logger.info(f"TEST MODE - File: {file.name} | Ratio: {percentage}% | Method: {method}")
return
dest_file = file.parent / temp_output.name dest_file = file.parent / temp_output.name
shutil.move(temp_output, dest_file) shutil.move(temp_output, dest_file)
print(f"🚚 Moved {temp_output.name}{dest_file.name}") print(f"🚚 Moved {temp_output.name}{dest_file.name}")
@ -393,6 +464,15 @@ def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size
temp_input.unlink() temp_input.unlink()
file.unlink() file.unlink()
logger.info(f"Deleted original and processing copy for {file.name}") logger.info(f"Deleted original and processing copy for {file.name}")
# Clean up subtitle file if it was embedded
if subtitle_file and subtitle_file.exists():
try:
subtitle_file.unlink()
print(f"🗑️ Removed embedded subtitle: {subtitle_file.name}")
logger.info(f"Removed embedded subtitle: {subtitle_file.name}")
except Exception as e:
logger.warning(f"Could not delete subtitle file {subtitle_file.name}: {e}")
except Exception as e: except Exception as e:
print(f"⚠️ Could not delete files: {e}") print(f"⚠️ Could not delete files: {e}")
logger.warning(f"Could not delete files: {e}") logger.warning(f"Could not delete files: {e}")

832
legacy/gui_path_manager.py Normal file
View File

@ -0,0 +1,832 @@
#!/usr/bin/env python3
"""
GUI Path Manager for Batch Video Transcoder
Allows easy browsing of folders and appending to paths.txt with encoding options.
"""
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from pathlib import Path
import os
import subprocess
import re
import json
from core.config_helper import load_config_xml
from core.logger_helper import setup_logger
logger = setup_logger(Path(__file__).parent.parent / "logs")
class PathManagerGUI:
def __init__(self, root):
self.root = root
self.root.title("Batch Transcoder - Path Manager")
self.root.geometry("1100x700")
# Load config
config_path = Path(__file__).parent.parent / "config.xml"
self.config = load_config_xml(config_path)
self.path_mappings = self.config.get("path_mappings", {})
# Paths file
self.paths_file = Path(__file__).parent.parent / "paths.txt"
self.transcode_bat = Path(__file__).parent.parent / "transcode.bat"
# Current selected folder
self.selected_folder = None
self.current_category = None
self.recently_added = None # Track recently added folder for highlighting
self.status_timer = None # Track status message timer
self.added_folders = set() # Folders that are in paths.txt
# Cache for folder data - split per category
self.cache_dir = Path(__file__).parent.parent / ".cache"
self.cache_dir.mkdir(exist_ok=True)
self.folder_cache = {} # Only current category in memory: {folder_path: size}
self.scan_in_progress = False
self.scanned_categories = set() # Track which categories have been scanned
# Lazy loading
self.all_folders = [] # All folders for current category
self.loaded_items = 0 # How many items are currently loaded
self.items_per_batch = 100 # Load 100 items at a time
# Load existing paths
self._load_existing_paths()
# Build UI
self._build_ui()
# Handle window close
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
def _build_ui(self):
"""Build the GUI layout."""
# Top frame for category selection and transcode launcher
top_frame = ttk.Frame(self.root)
top_frame.pack(fill=tk.X, padx=10, pady=10)
left_top = ttk.Frame(top_frame)
left_top.pack(side=tk.LEFT, fill=tk.X, expand=True)
ttk.Label(left_top, text="Category:").pack(side=tk.LEFT, padx=5)
self.category_var = tk.StringVar(value="tv")
categories = ["tv", "anime", "movies"]
for cat in categories:
ttk.Radiobutton(
left_top, text=cat.upper(), variable=self.category_var,
value=cat, command=self._on_category_change
).pack(side=tk.LEFT, padx=5)
ttk.Button(left_top, text="Refresh", command=self._refresh_with_cache_clear).pack(side=tk.LEFT, padx=5)
# Right side of top frame - transcode launcher
right_top = ttk.Frame(top_frame)
right_top.pack(side=tk.RIGHT)
if self.transcode_bat.exists():
ttk.Button(
right_top, text="▶ Run transcode.bat", command=self._run_transcode
).pack(side=tk.RIGHT, padx=5)
# Main content frame
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Left side - folder tree
left_frame = ttk.LabelFrame(main_frame, text="Folders (sorted by size)")
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# Treeview for folders with add button column
self.tree = ttk.Treeview(left_frame, columns=("size", "add", "remove"), height=20)
self.tree.column("#0", width=180)
self.tree.column("size", width=80)
self.tree.column("add", width=50)
self.tree.column("remove", width=50)
self.tree.heading("#0", text="Folder Name")
self.tree.heading("size", text="Size")
self.tree.heading("add", text="Add")
self.tree.heading("remove", text="Remove")
scrollbar = ttk.Scrollbar(left_frame, orient=tk.VERTICAL, command=self._on_scrollbar)
self.tree.configure(yscroll=scrollbar.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Configure tags for folder status
self.tree.tag_configure("added", background="#90EE90") # Light green for added
self.tree.tag_configure("not_added", background="white") # White for not added
self.tree.tag_configure("recently_added", background="#FFD700") # Gold for recently added
self.tree.bind("<Double-1>", self._on_folder_expand)
self.tree.bind("<<TreeviewSelect>>", self._on_folder_select)
self.tree.bind("<Button-1>", self._on_tree_click)
# Right side - options and preview
right_frame = ttk.LabelFrame(main_frame, text="Encoding Options & Preview")
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
# Mode selection
mode_frame = ttk.LabelFrame(right_frame, text="Mode (--m)")
mode_frame.pack(fill=tk.X, padx=5, pady=5)
self.mode_var = tk.StringVar(value="default")
for mode in ["default", "cq", "bitrate"]:
ttk.Radiobutton(mode_frame, text=mode, variable=self.mode_var, value=mode,
command=self._update_preview).pack(anchor=tk.W, padx=5)
# Resolution selection
res_frame = ttk.LabelFrame(right_frame, text="Resolution (--r)")
res_frame.pack(fill=tk.X, padx=5, pady=5)
self.resolution_var = tk.StringVar(value="none")
for res in ["none", "480", "720", "1080"]:
label = "Auto" if res == "none" else res + "p"
ttk.Radiobutton(res_frame, text=label, variable=self.resolution_var, value=res,
command=self._update_preview).pack(anchor=tk.W, padx=5)
# CQ value
cq_frame = ttk.LabelFrame(right_frame, text="CQ Value (--cq, optional)")
cq_frame.pack(fill=tk.X, padx=5, pady=5)
self.cq_var = tk.StringVar(value="")
cq_entry = ttk.Entry(cq_frame, textvariable=self.cq_var, width=10)
cq_entry.pack(anchor=tk.W, padx=5, pady=3)
cq_entry.bind("<KeyRelease>", lambda e: self._update_preview())
# Preview frame
preview_frame = ttk.LabelFrame(right_frame, text="Command Preview")
preview_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.preview_text = tk.Text(preview_frame, height=8, width=40, wrap=tk.WORD, bg="lightgray")
self.preview_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.preview_text.config(state=tk.DISABLED)
# Bottom frame - action buttons and status
bottom_frame = ttk.Frame(self.root)
bottom_frame.pack(fill=tk.X, padx=10, pady=10)
button_frame = ttk.Frame(bottom_frame)
button_frame.pack(side=tk.LEFT)
ttk.Button(button_frame, text="View paths.txt", command=self._view_paths_file).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="Clear paths.txt", command=self._clear_paths_file).pack(side=tk.LEFT, padx=5)
# Status label (for silent feedback)
self.status_label = ttk.Label(bottom_frame, text="", foreground="green")
self.status_label.pack(side=tk.LEFT, padx=10)
# Load cache and populate initial category
self._load_cache()
self._refresh_folders(use_cache=True)
# Only scan once per category on first view
if self.current_category not in self.scanned_categories:
self.root.after(100, self._scan_folders_once)
def _on_category_change(self):
"""Handle category radio button change."""
self.current_category = self.category_var.get()
# Load cache for this category
self._load_cache()
# Show cached data first
self._refresh_folders(use_cache=True)
# Only scan once per category on first view
if self.current_category not in self.scanned_categories:
self.root.after(100, self._scan_folders_once)
def _load_cache(self):
"""Load folder cache for current category from disk (lazy)."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
self.folder_cache.clear()
# Don't fully load cache yet - just verify it exists
if not cache_file.exists():
logger.info(f"No cache file for {category}")
else:
logger.info(f"Cache file exists for {category}")
def _parse_cache_lazily(self, limit=None):
"""Parse cache file lazily and return folders."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
folders = []
if cache_file.exists():
try:
with open(cache_file, "r", encoding="utf-8") as f:
cache_dict = json.load(f)
# Convert to list and sort
for folder_path_str, size in cache_dict.items():
folder_path = Path(folder_path_str)
if folder_path.exists():
folders.append((folder_path.name, folder_path, size))
# Early exit if limit reached
if limit and len(folders) >= limit:
break
# Sort by size descending (only what we loaded)
folders.sort(key=lambda x: x[2], reverse=True)
except Exception as e:
logger.error(f"Failed to parse cache: {e}")
return folders
def _save_cache(self):
"""Save current category's folder cache to disk."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
try:
with open(cache_file, "w", encoding="utf-8") as f:
json.dump(self.folder_cache, f, indent=2)
except Exception as e:
logger.error(f"Failed to save {category} cache: {e}")
def _refresh_with_cache_clear(self):
"""Refresh and clear cache to force full scan."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
# Delete cache file for this category
if cache_file.exists():
cache_file.unlink()
self.folder_cache.clear()
self.scanned_categories.discard(category) # Reset so it will scan again
self._refresh_folders(use_cache=False)
def _scan_folders_once(self):
"""Scan folders once per category on first load."""
if self.scan_in_progress:
return
category = self.category_var.get()
if category in self.scanned_categories:
return # Already scanned this category
self.scan_in_progress = True
try:
category_mapping = {
"tv": "P:\\tv",
"anime": "P:\\anime",
"movies": "P:\\movies"
}
base_key = category_mapping.get(category)
if not base_key or base_key not in self.path_mappings:
return
base_path = Path(base_key)
if not base_path.exists():
return
# Scan folders and update cache
new_cache = {}
for entry in os.scandir(base_path):
if entry.is_dir(follow_symlinks=False):
size = self._get_folder_size(Path(entry))
new_cache[str(Path(entry))] = size
# Update cache and save
self.folder_cache = new_cache
self._save_cache()
self.scanned_categories.add(category)
# Update UI if still on same category
if self.category_var.get() == category:
self._refresh_folders(use_cache=True)
finally:
self.scan_in_progress = False
def _scan_folders_background(self):
"""Scan folders in background and update cache."""
if self.scan_in_progress:
return
self.scan_in_progress = True
try:
category = self.category_var.get()
category_mapping = {
"tv": "P:\\tv",
"anime": "P:\\anime",
"movies": "P:\\movies"
}
base_key = category_mapping.get(category)
if not base_key or base_key not in self.path_mappings:
return
base_path = Path(base_key)
if not base_path.exists():
return
# Scan folders and update cache
new_cache = {}
for entry in os.scandir(base_path):
if entry.is_dir(follow_symlinks=False):
size = self._get_folder_size(Path(entry))
new_cache[str(Path(entry))] = size
# Update cache and save
self.folder_cache[category] = new_cache
self._save_cache()
# Update UI if still on same category
if self.category_var.get() == category:
self._refresh_folders(use_cache=True)
finally:
self.scan_in_progress = False
# Schedule next continuous scan
self.background_scan_timer = self.root.after(
self.background_scan_interval,
self._continuous_background_scan
)
# Schedule next continuous scan
self.background_scan_timer = self.root.after(
self.background_scan_interval,
self._continuous_background_scan
)
def _load_existing_paths(self):
"""Load existing paths from paths.txt and extract folder paths."""
self.added_folders.clear()
if not self.paths_file.exists():
return
try:
with open(self.paths_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
# Extract the path (last argument in the command)
# Format: --m mode --r res --cq val "path" or just "path"
# Find all quoted strings
matches = re.findall(r'"([^"]*)"', line)
if matches:
# Last quoted string is the path
path = matches[-1]
self.added_folders.add(path)
except Exception as e:
logger.error(f"Failed to load existing paths: {e}")
def _get_folder_size(self, path: Path) -> int:
"""Calculate total size of folder in bytes."""
total = 0
try:
for entry in os.scandir(path):
if entry.is_file(follow_symlinks=False):
total += entry.stat().st_size
elif entry.is_dir(follow_symlinks=False):
total += self._get_folder_size(Path(entry))
except PermissionError:
pass
return total
def _format_size(self, bytes_size: int) -> str:
"""Format bytes to human readable size."""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if bytes_size < 1024:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024
return f"{bytes_size:.1f} PB"
def _refresh_folders(self, use_cache=False):
"""Refresh the folder tree from cache or disk."""
# Clear existing items
for item in self.tree.get_children():
self.tree.delete(item)
self.all_folders = []
self.loaded_items = 0
category = self.category_var.get()
# Map category to path mapping key
category_mapping = {
"tv": "P:\\tv",
"anime": "P:\\anime",
"movies": "P:\\movies"
}
base_key = category_mapping.get(category)
if not base_key or base_key not in self.path_mappings:
messagebox.showwarning("Info", f"No mapping found for {category}")
return
base_path = Path(base_key)
# Check if path exists
if not base_path.exists():
messagebox.showerror("Error", f"Path not found: {base_path}")
return
# Get folders from cache or disk
if use_cache:
# Parse cache lazily - only load what we need initially
folders = self._parse_cache_lazily(limit=None) # Get all but parse efficiently
else:
# Scan from disk
folders = []
try:
for entry in os.scandir(base_path):
if entry.is_dir(follow_symlinks=False):
size = self._get_folder_size(Path(entry))
folders.append((entry.name, Path(entry), size))
except PermissionError:
messagebox.showerror("Error", f"Permission denied accessing {base_path}")
return
# Update cache with fresh scan
cache_dict = {str(f[1]): f[2] for f in folders}
self.folder_cache = cache_dict
self._save_cache()
# Sort by size descending
folders.sort(key=lambda x: x[2], reverse=True)
# Store all folders and load first batch only
self.all_folders = folders
self._load_more_items()
def _on_folder_expand(self, event):
"""Handle folder double-click to show contents."""
selection = self.tree.selection()
if not selection:
return
item = selection[0]
tags = self.tree.item(item, "tags")
if not tags:
return
folder_path = Path(tags[0])
# Check if already expanded
if self.tree.get_children(item):
# Toggle: remove children
for child in self.tree.get_children(item):
self.tree.delete(child)
else:
# Add file/folder contents
try:
entries = []
for entry in os.scandir(folder_path):
if entry.is_file():
size = entry.stat().st_size
entries.append((entry.name, "File", size))
elif entry.is_dir():
size = self._get_folder_size(Path(entry))
entries.append((entry.name, "Folder", size))
# Sort by size descending
entries.sort(key=lambda x: x[2], reverse=True)
for name, type_str, size in entries:
size_str = self._format_size(size)
self.tree.insert(item, "end", text=f"[{type_str}] {name}", values=(size_str,))
except PermissionError:
messagebox.showerror("Error", f"Permission denied accessing {folder_path}")
def _on_folder_select(self, event):
"""Handle folder selection to update preview."""
selection = self.tree.selection()
if not selection:
return
item = selection[0]
tags = self.tree.item(item, "tags")
if tags:
self.selected_folder = tags[0]
self._update_preview()
def _on_tree_click(self, event):
"""Handle click on '+' or '-' button in add column."""
item = self.tree.identify("item", event.x, event.y)
column = self.tree.identify_column(event.x) # Only takes x coordinate
# Column #2 is the "add" column (columns are #0=name, #1=size, #2=add, #3=remove)
if item and column == "#2":
tags = self.tree.item(item, "tags")
if tags:
folder_path = tags[0]
values = self.tree.item(item, "values")
if len(values) > 1:
button_text = values[1] # Get button text
if "[+]" in button_text:
# Immediately update UI for snappy response
size_val = values[0]
self.tree.item(item, values=(size_val, "", "[-]"), tags=(folder_path, "added"))
# Add to paths.txt asynchronously
self.selected_folder = folder_path
self.root.after(0, self._add_to_paths_file_async, folder_path)
# Column #3 is the "remove" column
elif item and column == "#3":
tags = self.tree.item(item, "tags")
if tags:
folder_path = tags[0]
# Immediately update UI for snappy response
values = self.tree.item(item, "values")
size_val = values[0]
self.tree.item(item, values=(size_val, "[+]", ""), tags=(folder_path, "not_added"))
# Remove from paths.txt asynchronously
self.root.after(0, self._remove_from_paths_file_async, folder_path)
def _add_to_paths_file_async(self, folder_path):
"""Add to paths.txt without blocking UI."""
self.selected_folder = folder_path
self._add_to_paths_file()
# Silently reload in background
self._load_existing_paths()
def _remove_from_paths_file_async(self, folder_path):
"""Remove from paths.txt without blocking UI."""
self._remove_from_paths_file(folder_path)
def _update_preview(self):
"""Update the command preview."""
if not self.selected_folder:
preview_text = "No folder selected"
else:
folder_path = self.selected_folder
# Build command
cmd_parts = ['py main.py']
# Add mode if not default
mode = self.mode_var.get()
if mode != "default":
cmd_parts.append(f'--m {mode}')
# Add resolution if specified
resolution = self.resolution_var.get()
if resolution != "none":
cmd_parts.append(f'--r {resolution}')
# Add CQ if specified
cq = self.cq_var.get().strip()
if cq:
cmd_parts.append(f'--cq {cq}')
# Add path
cmd_parts.append(f'"{folder_path}"')
preview_text = " ".join(cmd_parts)
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete("1.0", tk.END)
self.preview_text.insert("1.0", preview_text)
self.preview_text.config(state=tk.DISABLED)
def _add_to_paths_file(self):
"""Append the current command to paths.txt."""
if not self.selected_folder:
messagebox.showwarning("Warning", "Please select a folder first")
return
folder_path = self.selected_folder
# Check if already in file
if folder_path in self.added_folders:
self._show_status(f"Already added: {Path(folder_path).name}")
return
# Build command line - start fresh
cmd_parts = []
# Add mode if not default
mode = self.mode_var.get()
if mode != "default":
cmd_parts.append(f'--m {mode}')
# Add resolution if specified
resolution = self.resolution_var.get()
if resolution != "none":
cmd_parts.append(f'--r {resolution}')
# Add CQ if specified
cq = self.cq_var.get().strip()
if cq:
cmd_parts.append(f'--cq {cq}')
# Add folder path
cmd_parts.append(f'"{folder_path}"')
line = " ".join(cmd_parts)
# Append to paths.txt
try:
# Check if file exists and has content
if self.paths_file.exists() and self.paths_file.stat().st_size > 0:
# Read last character to check if it ends with newline
with open(self.paths_file, "rb") as f:
f.seek(-1, 2) # Seek to last byte
last_char = f.read(1)
needs_newline = last_char != b'\n'
else:
needs_newline = False
# Write to file
with open(self.paths_file, "a", encoding="utf-8") as f:
if needs_newline:
f.write("\n")
f.write(line + "\n")
# Add to tracked set
self.added_folders.add(folder_path)
# Silent success - show status label instead of popup
self.recently_added = folder_path
self._show_status(f"✓ Added: {Path(folder_path).name}")
logger.info(f"Added to paths.txt: {line}")
# Clear timer if exists
if self.status_timer:
self.root.after_cancel(self.status_timer)
# Clear status after 3 seconds
self.status_timer = self.root.after(3000, self._clear_status)
except Exception as e:
messagebox.showerror("Error", f"Failed to write to paths.txt: {e}")
logger.error(f"Failed to write to paths.txt: {e}")
def _remove_from_paths_file(self, folder_path):
"""Remove a folder from paths.txt."""
if not self.paths_file.exists():
messagebox.showwarning("Warning", "paths.txt does not exist")
return
try:
with open(self.paths_file, "r", encoding="utf-8") as f:
lines = f.readlines()
# Filter out lines containing this folder path
new_lines = []
found = False
for line in lines:
if f'"{folder_path}"' in line or f"'{folder_path}'" in line:
found = True
else:
new_lines.append(line)
if not found:
messagebox.showwarning("Warning", "Path not found in paths.txt")
return
# Write back
with open(self.paths_file, "w", encoding="utf-8") as f:
f.writelines(new_lines)
# Remove from tracked set
self.added_folders.discard(folder_path)
self._show_status(f"✓ Removed: {Path(folder_path).name}")
logger.info(f"Removed from paths.txt: {folder_path}")
# Clear timer if exists
if self.status_timer:
self.root.after_cancel(self.status_timer)
# Clear status after 3 seconds
self.status_timer = self.root.after(3000, self._clear_status)
except Exception as e:
messagebox.showerror("Error", f"Failed to remove from paths.txt: {e}")
logger.error(f"Failed to remove from paths.txt: {e}")
def _show_status(self, message):
"""Show status message in label."""
self.status_label.config(text=message, foreground="green")
def _clear_status(self):
"""Clear status message after delay."""
self.status_label.config(text="")
self.status_timer = None
def _run_transcode(self):
"""Launch transcode.bat in a new command window."""
if not self.transcode_bat.exists():
messagebox.showerror("Error", f"transcode.bat not found at {self.transcode_bat}")
return
try:
# Launch in new cmd window
subprocess.Popen(
['cmd', '/c', f'"{self.transcode_bat}"'],
cwd=str(self.transcode_bat.parent),
creationflags=subprocess.CREATE_NEW_CONSOLE
)
logger.info("Launched transcode.bat")
except Exception as e:
messagebox.showerror("Error", f"Failed to launch transcode.bat: {e}")
logger.error(f"Failed to launch transcode.bat: {e}")
def _view_paths_file(self):
"""Open paths.txt in a new window."""
if not self.paths_file.exists():
messagebox.showinfo("Info", "paths.txt does not exist yet")
return
try:
with open(self.paths_file, "r", encoding="utf-8") as f:
content = f.read()
# Create new window
view_window = tk.Toplevel(self.root)
view_window.title("paths.txt")
view_window.geometry("800x400")
text_widget = tk.Text(view_window, wrap=tk.WORD)
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
text_widget.insert("1.0", content)
# Add close button
ttk.Button(view_window, text="Close", command=view_window.destroy).pack(pady=5)
except Exception as e:
messagebox.showerror("Error", f"Failed to read paths.txt: {e}")
def _clear_paths_file(self):
"""Clear the paths.txt file."""
if not self.paths_file.exists():
messagebox.showinfo("Info", "paths.txt does not exist")
return
if messagebox.askyesno("Confirm", "Are you sure you want to clear paths.txt?"):
try:
self.paths_file.write_text("", encoding="utf-8")
messagebox.showinfo("Success", "paths.txt has been cleared")
logger.info("paths.txt cleared")
except Exception as e:
messagebox.showerror("Error", f"Failed to clear paths.txt: {e}")
def _on_closing(self):
"""Handle window closing - cleanup timers."""
self.root.destroy()
def _on_scrollbar(self, *args):
"""Handle scrollbar movement - load more items when scrolling."""
self.tree.yview(*args)
# Check if we need to load more items
if self.all_folders and self.loaded_items < len(self.all_folders):
# Get scroll position
first_visible = self.tree.yview()[0]
last_visible = self.tree.yview()[1]
# If we're past 70% scrolled, load more
if last_visible > 0.7:
self._load_more_items()
def _load_more_items(self):
"""Load next batch of items into tree."""
start = self.loaded_items
end = min(start + self.items_per_batch, len(self.all_folders))
for i in range(start, end):
folder_name, folder_path, size = self.all_folders[i]
size_str = self._format_size(size)
folder_path_str = str(folder_path)
# Determine button and tag
if folder_path_str in self.added_folders:
add_btn = ""
remove_btn = "[-]"
tag = "added"
else:
add_btn = "[+]"
remove_btn = ""
tag = "not_added"
self.tree.insert("", "end", text=folder_name, values=(size_str, add_btn, remove_btn),
tags=(folder_path_str, tag))
self.loaded_items = end
def main():
root = tk.Tk()
app = PathManagerGUI(root)
root.mainloop()
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@ -19,3 +19,15 @@
2025-12-31 15:31:08 | Death From Above.mkv | CQ failed: Size threshold not met (90.5%) 2025-12-31 15:31:08 | Death From Above.mkv | CQ failed: Size threshold not met (90.5%)
2025-12-31 15:31:19 | Deleted Scenes.mkv | CQ failed: Size threshold not met (75.2%) 2025-12-31 15:31:19 | Deleted Scenes.mkv | CQ failed: Size threshold not met (75.2%)
2025-12-31 15:31:57 | FX Comparisons.mkv | CQ failed: Size threshold not met (86.5%) 2025-12-31 15:31:57 | FX Comparisons.mkv | CQ failed: Size threshold not met (86.5%)
2025-12-31 15:51:25 | Behind the Scenes.mkv | CQ failed: Size threshold not met (124.6%)
2025-12-31 15:51:51 | Bikes, Blades, Bridges, and Bits.mkv | CQ failed: Size threshold not met (133.2%)
2025-12-31 15:52:30 | Check Your Sights.mkv | CQ failed: Size threshold not met (130.9%)
2025-12-31 16:11:32 | A Museum Tour with Sir Jonathan Wick.mkv | CQ failed: Size threshold not met (118.6%)
2025-12-31 16:11:51 | As Above, So Below - The Underworld of John Wick.mkv | CQ failed: Size threshold not met (125.2%)
2025-12-31 16:12:09 | Car Fu Ride-Along.mkv | CQ failed: Size threshold not met (173.8%)
2025-12-31 16:40:05 | TAYLOR SWIFT THE ERAS TOUR (2023) x265 EAC3 5.1 WEBRip-1080p GalaxyRG265.mkv | CQ failed: Size threshold not met (83.5%)
2025-12-31 18:10:24 | Interview with director Joe Dante.mkv | CQ failed: Size threshold not met (97.8%)
2025-12-31 19:15:56 | Supernatural - S03E01 - The Magnificent Seven x265 AC3 Bluray-1080p HiQVE.mkv | CQ failed: Size threshold not met (88.5%)
2026-01-01 01:25:05 | Supernatural - S07E17 - The Born-Again Identity x265 AC3 Bluray-1080p HiQVE.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing
2026-01-01 13:17:15 | The Office (US) - S08E04 - Garden Party x265 AAC Bluray-1080p Silence.mkv | CQ failed: Size threshold not met (122.2%)
2026-01-01 13:22:48 | The Office (US) - S08E04 - Garden Party x265 AAC Bluray-1080p Silence.mkv | CQ failed: Size threshold not met (101.3%)

42
main.py
View File

@ -33,14 +33,29 @@ def normalize_input_path(input_path: str, path_mappings: dict) -> Path:
""" """
# First, try to map Linux paths to Windows paths (reverse mapping) # First, try to map Linux paths to Windows paths (reverse mapping)
# If user provides "/mnt/plex/tv", find the mapping and convert to "P:\\tv" # If user provides "/mnt/plex/tv", find the mapping and convert to "P:\\tv"
for win_path, linux_path in path_mappings.items(): if isinstance(path_mappings, list):
if input_path.lower().startswith(linux_path.lower()): # New format: list of dicts
# Found a matching Linux path, convert to Windows for mapping in path_mappings:
relative = input_path[len(linux_path):].lstrip("/").lstrip("\\") if isinstance(mapping, dict):
result = Path(win_path) / relative if relative else Path(win_path) win_path = mapping.get("from")
logger.info(f"Path mapping: {input_path} -> {result}") linux_path = mapping.get("to")
print(f" Mapped Linux path {input_path} to {result}") if linux_path and input_path.lower().startswith(linux_path.lower()):
return result # Found a matching Linux path, convert to Windows
relative = input_path[len(linux_path):].lstrip("/").lstrip("\\")
result = Path(win_path) / relative if relative else Path(win_path)
logger.info(f"Path mapping: {input_path} -> {result}")
print(f" Mapped Linux path {input_path} to {result}")
return result
else:
# Old format: dict (for backwards compatibility)
for win_path, linux_path in path_mappings.items():
if input_path.lower().startswith(linux_path.lower()):
# Found a matching Linux path, convert to Windows
relative = input_path[len(linux_path):].lstrip("/").lstrip("\\")
result = Path(win_path) / relative if relative else Path(win_path)
logger.info(f"Path mapping: {input_path} -> {result}")
print(f" Mapped Linux path {input_path} to {result}")
return result
# No mapping found, use path as-is (normalize separators to Windows) # No mapping found, use path as-is (normalize separators to Windows)
# Convert forward slashes to backslashes for Windows # Convert forward slashes to backslashes for Windows
@ -93,7 +108,14 @@ Examples:
choices=["480", "720", "1080"], choices=["480", "720", "1080"],
help="Force target resolution (if not specified: 4K->1080p, else preserve)" help="Force target resolution (if not specified: 4K->1080p, else preserve)"
) )
parser.add_argument(
"--test", dest="test_mode", default=False, action="store_true",
help="Test mode: encode only first file, show ratio, don't move or delete (default: False)"
)
parser.add_argument(
"--language", dest="audio_language", default=None,
help="Tag audio streams with language code (e.g., eng, spa, fra). If not set, audio language is unchanged"
)
args = parser.parse_args() args = parser.parse_args()
# Load configuration # Load configuration
@ -110,7 +132,7 @@ Examples:
return return
# Process folder # Process folder
process_folder(folder, args.cq, args.transcode_mode, args.resolution, config, TRACKER_FILE) process_folder(folder, args.cq, args.transcode_mode, args.resolution, config, TRACKER_FILE, args.test_mode, args.audio_language)
if __name__ == "__main__": if __name__ == "__main__":
main() main()