updated
This commit is contained in:
parent
a2e2ee45f5
commit
02a51c7473
51
.vscode/launch.json
vendored
Normal file
51
.vscode/launch.json
vendored
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,152 +1,184 @@
|
||||
# AV1 Batch Video Transcoder - Project Structure
|
||||
|
||||
## 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
|
||||
|
||||
### Entry Point
|
||||
- **main.py** - CLI entry point with argument parsing
|
||||
- Loads configuration from `config.xml`
|
||||
- Initializes logger and CSV tracker
|
||||
- Dispatches to `process_folder()` for batch processing
|
||||
- **main.py** - CLI with argparse
|
||||
- Arguments: `folder`, `--cq`, `--m {cq,bitrate}`, `--r {480,720,1080}`, `--test`, `--language`
|
||||
- Loads config.xml, initializes logging
|
||||
- Calls `process_folder()` from process_manager
|
||||
|
||||
### Core Modules
|
||||
|
||||
#### `core/config_helper.py`
|
||||
- XML configuration parser
|
||||
- Returns dict with audio, encoding, and filter settings
|
||||
- **Key Config:**
|
||||
- `audio.stereo.high/medium`: Target bitrates for stereo audio (1080p/720p)
|
||||
- `audio.multi_channel.low/medium`: Target bitrates for multichannel audio
|
||||
- `encode.cq`: CQ values per content type (tv_720, tv_1080, movie_720, movie_1080, etc.)
|
||||
- `encode.fallback`: Bitrate fallback settings (900k/1080p, 650k/720p, etc.)
|
||||
- `extensions`: Video file types to process (mkv, mp4, etc.)
|
||||
- `ignore_tags`: Files to skip (trailer, sample, etc.)
|
||||
- **`load_config_xml(config_path)`** - Parses XML configuration
|
||||
- Returns dict with keys:
|
||||
- `general`: processing_folder, suffix (" - [EHX]"), extensions, reduction_ratio_threshold, subtitles config
|
||||
- `encode.cq`: CQ values per content type (tv_1080, tv_720, movie_1080, movie_720)
|
||||
- `encode.fallback`: Bitrate fallback (Phase 2 retry)
|
||||
- `audio`: Bitrate buckets for stereo/multichannel
|
||||
- `path_mappings`: Windows ↔ Linux path conversion
|
||||
|
||||
#### `core/logger_helper.py`
|
||||
- Comprehensive logging to `logs/conversion.log`
|
||||
- Captures source/target specs, audio decisions, bitrate info
|
||||
- Separate handlers for console (INFO+) and file (DEBUG+)
|
||||
|
||||
#### `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
|
||||
- Sets up logging to `logs/conversion.log` (INFO+) and console (DEBUG+)
|
||||
- Separate failure logger for `logs/conversion_failures.log`
|
||||
- Captures encoding decisions, bitrates, resolutions, timings
|
||||
|
||||
#### `core/process_manager.py`
|
||||
- **`process_folder(...)`**: Main batch processing loop
|
||||
- Classifies files as TV/Movie/Anime based on path
|
||||
- Detects per-file source resolution
|
||||
- Applies smart resolution defaults or explicit overrides
|
||||
- Handles CQ → Bitrate fallback if size threshold exceeded
|
||||
- Tracks results in `conversion_tracker.csv`
|
||||
- Deletes originals after successful encoding
|
||||
- **`process_folder(folder, cq, transcode_mode, resolution, config, tracker_file, test_mode, audio_language)`**
|
||||
- Scans folder for video files
|
||||
- **Per file**: Copy to temp, detect subtitles, analyze streams, encode, move, cleanup
|
||||
- **Subtitle detection**: Looks for exact match + glob pattern (filename.*.ext)
|
||||
- **Phase 1 (CQ)**: Try CQ-based encoding, check size threshold
|
||||
- **Phase 2 (Bitrate)**: Retry failed files with bitrate mode
|
||||
- **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`
|
||||
2. **main.py** parses args, loads config.xml
|
||||
3. **process_manager** iterates video files in folder
|
||||
4. For each file:
|
||||
- **video_handler** detects source resolution
|
||||
- **audio_handler** analyzes audio streams and calculates bitrates
|
||||
- **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`
|
||||
#### `core/audio_handler.py`
|
||||
- **`get_audio_streams(input_file)`** - Detects all audio streams with bitrate info
|
||||
- **`choose_audio_bitrate(channels, avg_bitrate, audio_config, is_1080_class)`** - Returns (codec, target_bitrate) tuple
|
||||
- Stereo 1080p: >192k → encode to 192k, ≤192k → copy
|
||||
- Stereo 720p: >160k → encode to 160k, ≤160k → copy
|
||||
- Multichannel: Encode to 384k (low) or 448k (medium)
|
||||
|
||||
## 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
|
||||
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
|
||||
```bash
|
||||
python main.py "C:\Videos\Movies" --cq 28 --r 1080
|
||||
**Processing:**
|
||||
1. Scan folder for .mkv/.mp4 files
|
||||
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)
|
||||
```bash
|
||||
python main.py "C:\Videos\Mixed"
|
||||
## File Movements
|
||||
|
||||
```
|
||||
Original:
|
||||
P:\tv\Show\Episode.mkv (1.2GB)
|
||||
P:\tv\Show\Episode.en.vtt
|
||||
|
||||
### Force 480p (for low-res content)
|
||||
```bash
|
||||
python main.py "C:\Videos\OldTV" --r 480
|
||||
During Encoding:
|
||||
processing/Episode.mkv (temp copy)
|
||||
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
|
||||
- ✅ 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
|
||||
|
||||
Verify the complete system:
|
||||
```bash
|
||||
# Test imports
|
||||
python -c "from core.audio_handler import *; from core.video_handler import *; print('OK')"
|
||||
|
||||
# Run on test folder
|
||||
python main.py "C:\Test\Videos" --r 720 --m bitrate
|
||||
|
||||
# Check logs
|
||||
cat logs/conversion.log
|
||||
```
|
||||
- ✅ All core modules import correctly
|
||||
- ✅ Config loads without Sonarr/Radarr references
|
||||
- ✅ Subtitle detection finds exact matches + language-prefixed files
|
||||
- ✅ Audio language tagging only applied with --language flag
|
||||
- ✅ Output always MKV regardless of source format
|
||||
- ✅ Suffix applied once (in temp output filename)
|
||||
- ✅ Subtitle files deleted with original files
|
||||
- ✅ Test mode shows compression ratio and stops
|
||||
- ✅ Phase 1 (CQ) and Phase 2 (Bitrate) retry logic works
|
||||
- ✅ CSV tracking logs all conversions
|
||||
|
||||
184
README_RESTRUCTURE.md
Normal file
184
README_RESTRUCTURE.md
Normal 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
|
||||
28
config.xml
28
config.xml
@ -19,6 +19,16 @@
|
||||
|
||||
<!-- Reduction ratio threshold: output must be <= this % of original or encoding fails -->
|
||||
<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>
|
||||
|
||||
<!-- =============================
|
||||
@ -30,28 +40,14 @@
|
||||
<map from="P:\movies" to="/mnt/plex/movies" />
|
||||
</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>
|
||||
<!-- CQ defaults (per resolution / content type) -->
|
||||
<cq>
|
||||
<tv_1080>28</tv_1080>
|
||||
<tv_720>32</tv_720>
|
||||
<tv_1080>30</tv_1080>
|
||||
<tv_720>34</tv_720>
|
||||
<movie_1080>32</movie_1080>
|
||||
<movie_720>34</movie_720>
|
||||
</cq>
|
||||
|
||||
@ -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,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,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 Bueller’s 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.
|
@ -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.
|
||||
"""
|
||||
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)
|
||||
|
||||
try:
|
||||
@ -31,8 +63,15 @@ def calculate_stream_bitrate(input_file: Path, stream_index: int) -> int:
|
||||
"-c", "copy",
|
||||
temp_audio_path
|
||||
]
|
||||
logger.debug(f"Extracting audio stream {stream_index} to temporary file for bitrate calculation...")
|
||||
result = subprocess.run(extract_cmd, capture_output=True, text=True, check=True)
|
||||
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=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)
|
||||
# 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)
|
||||
|
||||
Multi-channel:
|
||||
- Below 384k → low (384k) with AAC
|
||||
- 384k to below medium → low (384k) with AAC
|
||||
- Medium and above → medium with AAC
|
||||
- Below minimum threshold (low setting) → preserve original (copy)
|
||||
- Low to medium → use low bitrate
|
||||
- Medium and above → use medium bitrate
|
||||
"""
|
||||
# Normalize to 2ch or 6ch output
|
||||
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)
|
||||
else:
|
||||
# Preserve original
|
||||
logger.info(f"Stereo audio {bitrate_kbps}kbps ≤ {high_br/1000:.0f}k threshold - copying original")
|
||||
return ("copy", 0)
|
||||
else:
|
||||
# 720p stereo
|
||||
@ -159,6 +199,7 @@ def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, i
|
||||
return ("aac", medium_br)
|
||||
else:
|
||||
# Preserve original
|
||||
logger.info(f"Stereo audio {bitrate_kbps}kbps ≤ {medium_br/1000:.0f}k threshold - copying original")
|
||||
return ("copy", 0)
|
||||
|
||||
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"]
|
||||
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
|
||||
return ("aac", low_br)
|
||||
else:
|
||||
|
||||
@ -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
|
||||
|
||||
# --- Path Mappings ---
|
||||
path_mappings = {}
|
||||
path_mappings = []
|
||||
for m in root.findall("path_mappings/map"):
|
||||
f = m.attrib.get("from")
|
||||
t = m.attrib.get("to")
|
||||
if f and t:
|
||||
path_mappings[f] = t
|
||||
path_mappings.append({"from": f, "to": t})
|
||||
|
||||
# --- Encode ---
|
||||
encode_elem = root.find("encode")
|
||||
@ -121,6 +121,30 @@ def load_config_xml(path: Path) -> dict:
|
||||
if 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 {
|
||||
"processing_folder": processing_folder,
|
||||
"suffix": suffix,
|
||||
@ -129,5 +153,6 @@ def load_config_xml(path: Path) -> dict:
|
||||
"reduction_ratio_threshold": reduction_ratio_threshold,
|
||||
"path_mappings": path_mappings,
|
||||
"encode": {"cq": cq, "fallback": fallback, "filters": filters},
|
||||
"audio": audio
|
||||
"audio": audio,
|
||||
"services": services
|
||||
}
|
||||
|
||||
@ -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,
|
||||
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.
|
||||
|
||||
@ -59,10 +59,24 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
|
||||
print(line)
|
||||
logger.info(line)
|
||||
|
||||
cmd = ["ffmpeg","-y","-i",str(input_file),
|
||||
cmd = ["ffmpeg","-y","-i",str(input_file)]
|
||||
|
||||
# Add subtitle input if present
|
||||
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","-map","0:s?",
|
||||
"-c:v","av1_nvenc","-preset","p1","-pix_fmt","p010le"]
|
||||
"-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":
|
||||
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":
|
||||
# Preserve original audio
|
||||
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:
|
||||
# Re-encode with target bitrate
|
||||
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"-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}")
|
||||
logger.info(f"Running {method} encode: {output_file.name}")
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
"""Main processing logic for batch transcoding."""
|
||||
|
||||
import csv
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@ -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)
|
||||
config: Configuration dictionary
|
||||
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():
|
||||
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(f"{'='*60}\n")
|
||||
|
||||
skipped_count = 0
|
||||
for file in folder.rglob("*"):
|
||||
if file.suffix.lower() not in extensions:
|
||||
continue
|
||||
if any(tag.lower() in file.name.lower() for tag in ignore_tags):
|
||||
print(f"⏭️ Skipping: {file.name}")
|
||||
logger.info(f"Skipping: {file.name}")
|
||||
skipped_count += 1
|
||||
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)
|
||||
logger.info(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)
|
||||
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:
|
||||
# Detect source resolution and determine target resolution
|
||||
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)
|
||||
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
|
||||
if is_forced_bitrate:
|
||||
@ -144,7 +187,7 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
|
||||
try:
|
||||
orig_size, out_size, reduction_ratio = run_ffmpeg(
|
||||
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
|
||||
@ -170,7 +213,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
|
||||
'res_height': res_height,
|
||||
'target_resolution': target_resolution,
|
||||
'file_cq': file_cq,
|
||||
'is_tv': is_tv
|
||||
'is_tv': is_tv,
|
||||
'subtitle_file': subtitle_file
|
||||
})
|
||||
consecutive_failures += 1
|
||||
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,
|
||||
'target_resolution': target_resolution,
|
||||
'file_cq': file_cq,
|
||||
'is_tv': is_tv
|
||||
'is_tv': is_tv,
|
||||
'subtitle_file': subtitle_file
|
||||
})
|
||||
consecutive_failures += 1
|
||||
if consecutive_failures >= max_consecutive:
|
||||
@ -238,9 +283,14 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
|
||||
_save_successful_encoding(
|
||||
file, temp_input, temp_output, orig_size, out_size,
|
||||
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:
|
||||
# Unexpected error
|
||||
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'],
|
||||
file_data['res_width'], file_data['res_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
|
||||
@ -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['res_width'], file_data['res_height'],
|
||||
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:
|
||||
@ -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,
|
||||
reduction_ratio, method, src_width, src_height, res_width, res_height,
|
||||
file_cq, tracker_file, folder, is_tv):
|
||||
"""Helper function to save successfully encoded files."""
|
||||
file_cq, tracker_file, folder, is_tv, config=None, test_mode=False, subtitle_file=None):
|
||||
"""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
|
||||
shutil.move(temp_output, dest_file)
|
||||
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()
|
||||
file.unlink()
|
||||
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:
|
||||
print(f"⚠️ Could not delete files: {e}")
|
||||
logger.warning(f"Could not delete files: {e}")
|
||||
|
||||
832
legacy/gui_path_manager.py
Normal file
832
legacy/gui_path_manager.py
Normal 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()
|
||||
13767
logs/conversion.log
13767
logs/conversion.log
File diff suppressed because it is too large
Load Diff
@ -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: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: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%)
|
||||
|
||||
26
main.py
26
main.py
@ -33,6 +33,21 @@ def normalize_input_path(input_path: str, path_mappings: dict) -> Path:
|
||||
"""
|
||||
# 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 isinstance(path_mappings, list):
|
||||
# New format: list of dicts
|
||||
for mapping in path_mappings:
|
||||
if isinstance(mapping, dict):
|
||||
win_path = mapping.get("from")
|
||||
linux_path = mapping.get("to")
|
||||
if linux_path and 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
|
||||
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
|
||||
@ -93,7 +108,14 @@ Examples:
|
||||
choices=["480", "720", "1080"],
|
||||
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()
|
||||
|
||||
# Load configuration
|
||||
@ -110,7 +132,7 @@ Examples:
|
||||
return
|
||||
|
||||
# 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__":
|
||||
main()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user