v1.02a
added message cleanup and some better logic
This commit is contained in:
parent
13912636ea
commit
8d3aa03d72
101
LOGGING_STRUCTURE.md
Normal file
101
LOGGING_STRUCTURE.md
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# Structured Logging with Media Context
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The conversion system now uses **structured JSON logging** to enable organized analysis and filtering of conversion results by media type, show name, season, and episode.
|
||||||
|
|
||||||
|
## Terminal vs Log Output
|
||||||
|
|
||||||
|
- **Terminal Output**: Clean, human-readable print statements (VIDEO/AUDIO/PROGRESS sections)
|
||||||
|
- **Log Output**: Rich structured JSON with full media context for programmatic analysis
|
||||||
|
|
||||||
|
## Media Context Fields
|
||||||
|
|
||||||
|
Extracted automatically from file path structure:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"video_filename": "episode01.mkv",
|
||||||
|
"media_type": "tv", # "tv", "anime", "movie", or "other"
|
||||||
|
"show_name": "Breaking Bad",
|
||||||
|
"season": "01", # Optional (TV/anime only)
|
||||||
|
"episode": "01" # Optional (TV/anime only)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Path Structure Recognition
|
||||||
|
|
||||||
|
**TV Show**:
|
||||||
|
```
|
||||||
|
P:\tv\Breaking Bad\season01\episode01.mkv
|
||||||
|
→ media_type: "tv", show_name: "Breaking Bad", season: "01", episode: "01"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Anime**:
|
||||||
|
```
|
||||||
|
P:\anime\Demon Slayer\season02\e12.mkv
|
||||||
|
→ media_type: "anime", show_name: "Demon Slayer", season: "02", episode: "12"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Movie**:
|
||||||
|
```
|
||||||
|
P:\movies\Inception.mkv
|
||||||
|
→ media_type: "movie", show_name: "Inception"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Log Output Format
|
||||||
|
|
||||||
|
JSON logs contain both the message and media context:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-22 15:30:45",
|
||||||
|
"level": "INFO",
|
||||||
|
"message": "✅ CONVERSION COMPLETE: episode01[EHX].mkv",
|
||||||
|
"video_filename": "episode01.mkv",
|
||||||
|
"media_type": "tv",
|
||||||
|
"show_name": "Breaking Bad",
|
||||||
|
"season": "01",
|
||||||
|
"episode": "01",
|
||||||
|
"method": "CQ",
|
||||||
|
"original_size_mb": 4096.5,
|
||||||
|
"output_size_mb": 1843.2,
|
||||||
|
"reduction_pct": 55.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filtering Logs Later
|
||||||
|
|
||||||
|
You can parse the JSON logs to group by show/season/episode:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Filter all Breaking Bad conversions
|
||||||
|
with open("logs/conversion.log") as f:
|
||||||
|
for line in f:
|
||||||
|
entry = json.loads(line)
|
||||||
|
if entry.get("show_name") == "Breaking Bad":
|
||||||
|
print(f"S{entry['season']}E{entry['episode']}: {entry['reduction_pct']}% reduction")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current Implementation
|
||||||
|
|
||||||
|
**Files Updated**:
|
||||||
|
- `core/process_manager.py`:
|
||||||
|
- Added `get_media_context()` function to parse file paths
|
||||||
|
- Extracts media context once per file processing
|
||||||
|
- Passes context to all logging calls via `extra={}` parameter
|
||||||
|
|
||||||
|
- `core/logger_helper.py`:
|
||||||
|
- JsonFormatter automatically includes all extra fields in output
|
||||||
|
- Added `log_event()` helper for consistent structured logging
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Always call `get_media_context()` once per file
|
||||||
|
2. Pass result to all logging calls for that file: `logger.info(msg, extra=media_context)`
|
||||||
|
3. For additional context: `logger.info(msg, extra={**media_context, "custom_field": value})`
|
||||||
|
4. Parse logs with JSON reader for reliable data extraction
|
||||||
@ -1645,3 +1645,111 @@ tv,Taskmaster,Taskmaster - S00E54 - Taskmaster’s New Year Treat h265 AAC WEBRi
|
|||||||
tv,Taskmaster,Taskmaster - S00E73 - Taskmaster’s New Year Treat 2022 - Basic Recipe 28 h265 AAC WEBRip-1080p EHX.mkv,695.65,695.65,100.0,1920x1080,1920x1080,1,28,CQ
|
tv,Taskmaster,Taskmaster - S00E73 - Taskmaster’s New Year Treat 2022 - Basic Recipe 28 h265 AAC WEBRip-1080p EHX.mkv,695.65,695.65,100.0,1920x1080,1920x1080,1,28,CQ
|
||||||
tv,Taskmaster,Taskmaster - S00E85 - Taskmaster's New Year Treat 2023 - That's a Swizz h265 AAC WEBRip-1080p EHX.mkv,642.96,642.96,100.0,1920x1080,1920x1080,1,28,CQ
|
tv,Taskmaster,Taskmaster - S00E85 - Taskmaster's New Year Treat 2023 - That's a Swizz h265 AAC WEBRip-1080p EHX.mkv,642.96,642.96,100.0,1920x1080,1920x1080,1,28,CQ
|
||||||
tv,Taskmaster,Taskmaster - S00E98 - Taskmaster's New Year Treat 2024 - Huh h265 AAC WEBRip-1080p EHX.mkv,707.51,707.51,100.0,1920x1080,1920x1080,1,28,CQ
|
tv,Taskmaster,Taskmaster - S00E98 - Taskmaster's New Year Treat 2024 - Huh h265 AAC WEBRip-1080p EHX.mkv,707.51,707.51,100.0,1920x1080,1920x1080,1,28,CQ
|
||||||
|
tv,Tulsa King,"Tulsa King - S01E01 - Go West, Old Man x265 EAC3 Bluray-1080p t3nzin - [EHX].mkv",1814.72,488.3,26.9,1920x960,1920x960,1,28,CQ
|
||||||
|
tv,Tulsa King,Tulsa King - S01E02 - Center of the Universe x265 EAC3 Bluray-1080p t3nzin - [EHX].mkv,1626.91,447.12,27.5,1920x804,1920x804,1,28,CQ
|
||||||
|
tv,Tulsa King,Tulsa King - S01E03 - Caprice x265 EAC3 Bluray-1080p t3nzin - [EHX].mkv,1703.8,511.73,30.0,1920x804,1920x804,1,28,CQ
|
||||||
|
tv,Tulsa King,Tulsa King - S01E04 - Visitation Place x265 EAC3 Bluray-1080p t3nzin - [EHX].mkv,1713.64,457.66,26.7,1920x804,1920x804,1,28,CQ
|
||||||
|
tv,Tulsa King,Tulsa King - S01E05 - Token Joe x265 EAC3 Bluray-1080p t3nzin - [EHX].mkv,1584.63,428.51,27.0,1920x960,1920x960,1,28,CQ
|
||||||
|
tv,Tulsa King,Tulsa King - S01E06 - Stable x265 EAC3 Bluray-1080p t3nzin - [EHX].mkv,1730.31,432.13,25.0,1920x956,1920x956,1,28,CQ
|
||||||
|
tv,Tulsa King,Tulsa King - S01E07 - Warr Acres x265 EAC3 Bluray-1080p t3nzin - [EHX].mkv,1856.49,450.09,24.2,1920x960,1920x960,1,28,CQ
|
||||||
|
tv,Tulsa King,Tulsa King - S01E08 - Adobe Walls x265 EAC3 Bluray-1080p t3nzin - [EHX].mkv,1569.86,439.54,28.0,1920x804,1920x804,1,28,CQ
|
||||||
|
tv,Tulsa King,Tulsa King - S01E09 - Happy Trails x265 EAC3 Bluray-1080p t3nzin - [EHX].mkv,1608.93,502.05,31.2,1920x804,1920x804,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 01 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2133.69,340.32,15.9,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 02 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2000.63,316.87,15.8,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 03 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2388.71,327.7,13.7,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 04 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2298.9,358.96,15.6,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 05 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2224.88,276.85,12.4,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 06 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1879.17,311.66,16.6,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 07 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2029.98,337.99,16.6,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 08 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2027.96,306.39,15.1,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 09 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1916.23,324.79,16.9,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 10 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2184.08,328.82,15.1,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 11 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2481.42,418.22,16.9,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 12 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2305.83,326.33,14.2,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 01 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1977.67,287.94,14.6,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 02 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1981.07,285.77,14.4,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 03 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2227.27,355.11,15.9,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 04 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2304.24,342.7,14.9,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 05 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2718.84,419.84,15.4,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 06 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2364.74,342.99,14.5,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 07 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1992.49,308.29,15.5,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 08 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1986.14,316.75,15.9,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 09 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1980.58,297.66,15.0,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 10 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1903.95,282.62,14.8,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 11 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2365.17,387.04,16.4,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 12 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2371.71,389.02,16.4,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 01 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2153.09,344.79,16.0,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 02 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1975.28,274.59,13.9,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 03 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2157.15,307.58,14.3,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 04 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2446.97,387.79,15.8,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 05 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2075.37,291.7,14.1,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 06 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1917.51,292.27,15.2,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 07 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2159.91,335.62,15.5,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 08 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2525.01,394.22,15.6,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 09 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2511.14,427.56,17.0,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 10 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1981.0,275.71,13.9,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 11 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2376.41,402.42,16.9,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 12 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2268.85,375.29,16.5,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 00 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2144.67,333.13,15.5,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 01 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1965.41,307.32,15.6,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 02 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1859.7,319.66,17.2,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 03 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1948.02,311.6,16.0,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 04 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2097.75,320.2,15.3,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 05 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2531.16,367.06,14.5,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 06 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2165.79,312.71,14.4,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 07 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1698.21,279.77,16.5,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 08 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1908.73,284.91,14.9,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 09 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1781.66,300.93,16.9,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 10 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2280.0,372.33,16.3,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 11 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2314.99,370.49,16.0,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - 12 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1825.25,333.69,18.3,1920x1080,1920x1080,2,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - NCED (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,203.33,39.91,19.6,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - NCOP 01 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,190.59,25.6,13.4,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD HERO - NCOP 02 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,193.2,26.99,14.0,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - 13 (OVA) (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2131.25,309.54,14.5,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - Special 01 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,238.41,25.52,10.7,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - Special 02 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,239.71,24.76,10.3,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - Special 03 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,275.15,30.81,11.2,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - Special 04 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,235.34,29.8,12.7,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - Special 05 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,312.28,37.7,12.1,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - Special 06 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,247.67,27.08,10.9,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - NCED (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,136.96,17.2,12.6,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - NCOP 01 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,157.29,26.65,16.9,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD BorN - NCOP 02 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,157.18,26.73,17.0,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - 13 (OVA) (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1186.27,238.43,20.1,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - NCED 01 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,112.28,13.52,12.0,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - NCED 02 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,92.92,23.07,24.8,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - NCOP 01 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,187.62,30.65,16.3,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD NEW - NCOP 02 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,190.33,31.29,16.4,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 13 (OVA) (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,1558.46,250.68,16.1,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - 14 (OVA) (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,2244.67,237.94,10.6,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - Special 01 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,218.25,35.79,16.4,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - Special 02 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,250.69,36.44,14.5,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - Special 03 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,182.05,34.82,19.1,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - Special 04 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,171.06,26.42,15.4,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - Special 05 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,263.3,48.39,18.4,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - Special 06 (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,156.18,22.79,14.6,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - NCED (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,181.09,47.83,26.4,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,High School D×D (2012),[IK] High School DxD - NCOP (BD 1920x1080 Hi10 FLAC) - [EHX].mkv,194.96,29.79,15.3,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S01E01 - Sheep Costume x265 FLAC Bluray-1080p YURASUKA - [EHX].mkv,753.08,160.08,21.3,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S01E02 - How to Make Delicious Hot Cocoa x265 FLAC Bluray-1080p YURASUKA - [EHX].mkv,782.89,147.31,18.8,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S01E03 - Humpty Dumpty x265 FLAC Bluray-1080p YURASUKA - [EHX].mkv,706.75,146.46,20.7,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S01E04 - Mind of a Lone Wolf x265 FLAC Bluray-1080p YURASUKA - [EHX].mkv,684.77,139.25,20.3,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S01E05 - Berliner Mystery x265 FLAC Bluray-1080p YURASUKA - [EHX].mkv,762.44,135.78,17.8,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S01E06 - But I Get to Keep Charlotte x265 FLAC Bluray-1080p YURASUKA - [EHX].mkv,769.93,149.39,19.4,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S01E07 - Shake Half x265 FLAC Bluray-1080p YURASUKA - [EHX].mkv,738.83,144.63,19.6,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,"SHOSHIMIN - How to Become Ordinary - S01E08 - C'mere, You Want Some Free Candy x265 FLAC Bluray-1080p YURASUKA - [EHX].mkv",743.89,142.98,19.2,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S01E09 - Sweet Memory (Part 1) x265 FLAC Bluray-1080p YURASUKA - [EHX].mkv,659.52,134.14,20.3,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S01E10 - Sweet Memory (Part 2) x265 FLAC Bluray-1080p YURASUKA - [EHX].mkv,729.55,144.02,19.7,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E01 - A Warm Winter (Part 1) x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1259.13,150.42,11.9,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E02 - A Warm Winter (Part 2) x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1081.2,125.75,11.6,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E03 - Hesitant Spring x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1051.26,126.74,12.1,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E04 - Suspicious Summer (Part 1) x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1164.91,143.01,12.3,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E05 - Suspicious Summer (Part 2) x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1209.37,140.26,11.6,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E06 - Midsummer Night x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1145.83,128.41,11.2,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E07 - Autumn Returns x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1187.86,135.27,11.4,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E08 - Should We Really Have Met x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1189.66,131.28,11.0,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E09 - Kobato-kun and Osanai-san x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1243.46,141.37,11.4,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E10 - Please Water the Dried Flowers x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1171.12,134.31,11.5,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E11 - The End of What Seemed Like a Golden Age x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1084.98,123.48,11.4,1920x1080,1920x1080,1,28,CQ
|
||||||
|
anime,SHOSHIMIN - How to Become Ordinary,SHOSHIMIN - How to Become Ordinary - S02E12 - Just Deserts x265 Opus Bluray-1080p YURASUKA - [EHX].mkv,1031.82,124.04,12.0,1920x1080,1920x1080,1,28,CQ
|
||||||
|
|||||||
|
Can't render this file because it has a wrong number of fields in line 14.
|
@ -15,16 +15,30 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
|
|||||||
method: str, bitrate_config: dict, encoder: str = "nvenc", subtitle_files: list = None, audio_language: str = None,
|
method: str, bitrate_config: dict, encoder: str = "nvenc", subtitle_files: list = None, audio_language: str = None,
|
||||||
audio_filter_config: dict = None, test_mode: bool = False, strip_all_titles: bool = False, src_bit_depth: int = None, unforce_subs: bool = False, no_encode: bool = False):
|
audio_filter_config: dict = None, test_mode: bool = False, strip_all_titles: bool = False, src_bit_depth: int = None, unforce_subs: bool = False, no_encode: bool = False):
|
||||||
"""
|
"""
|
||||||
Run FFmpeg encode with comprehensive logging.
|
Execute FFmpeg encoding/re-muxing with structured console output.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
src_bit_depth: Source video bit depth (8, 10, or 12). If provided, encoder will be auto-selected:
|
input_file: Path to source video file
|
||||||
10+ bit → HEVC NVENC, 8-bit → AV1 NVENC
|
output_file: Path for encoded output file
|
||||||
strip_all_titles: If True, strip all title metadata from all audio tracks
|
cq: Quality value (0-63, lower=better) for CQ mode
|
||||||
unforce_subs: If True, remove forced flag from all subtitle tracks
|
scale_width/height: Target resolution dimensions
|
||||||
no_encode: If True, copy video/audio streams without encoding (only re-mux with subtitle processing)
|
src_width/height: Source resolution dimensions
|
||||||
|
filter_flags: Scaling filter algorithm (lanczos, bicubic, etc)
|
||||||
|
audio_config: Audio bitrate configuration dict
|
||||||
|
method: Encoding method - "CQ" or "Bitrate"
|
||||||
|
bitrate_config: Bitrate/maxrate/bufsize configuration dict
|
||||||
|
encoder: Video codec - "hevc", "av1", or "nvenc"
|
||||||
|
subtitle_files: List of external subtitle file paths (if any)
|
||||||
|
audio_language: ISO 639-2 language code to tag audio (e.g., "eng", "spa")
|
||||||
|
audio_filter_config: Audio filtering/selection configuration
|
||||||
|
test_mode: If True, only encode first 15 minutes, don't move files
|
||||||
|
strip_all_titles: If True, strip title metadata from all audio tracks
|
||||||
|
src_bit_depth: Source bit depth (8/10/12) for encoder auto-selection
|
||||||
|
unforce_subs: If True, remove forced flag from subtitle tracks
|
||||||
|
no_encode: If True, copy video/audio (re-mux only, skip encoding)
|
||||||
|
|
||||||
Returns tuple: (orig_size, out_size, reduction_ratio)
|
Returns:
|
||||||
|
tuple: (orig_size_bytes, output_size_bytes, reduction_ratio)
|
||||||
"""
|
"""
|
||||||
streams = get_audio_streams(input_file)
|
streams = get_audio_streams(input_file)
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from datetime import datetime
|
|||||||
class JsonFormatter(logging.Formatter):
|
class JsonFormatter(logging.Formatter):
|
||||||
"""
|
"""
|
||||||
Custom JSON log formatter for structured logging.
|
Custom JSON log formatter for structured logging.
|
||||||
|
Outputs rich JSON objects with context for programmatic parsing and analysis.
|
||||||
"""
|
"""
|
||||||
def format(self, record: logging.LogRecord) -> str:
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
log_object = {
|
log_object = {
|
||||||
@ -14,15 +15,16 @@ class JsonFormatter(logging.Formatter):
|
|||||||
"level": record.levelname,
|
"level": record.levelname,
|
||||||
"message": record.getMessage(),
|
"message": record.getMessage(),
|
||||||
"module": record.module,
|
"module": record.module,
|
||||||
"funcName": record.funcName,
|
"function": record.funcName,
|
||||||
"line": record.lineno,
|
"line": record.lineno,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Include any extra fields added via logger.info("msg", extra={...})
|
# Include any extra fields added via logger.info("msg", extra={...})
|
||||||
|
# This allows passing structured context: logger.info("msg", extra={"file": "video.mkv", "size": 1024})
|
||||||
if hasattr(record, "extra") and isinstance(record.extra, dict):
|
if hasattr(record, "extra") and isinstance(record.extra, dict):
|
||||||
log_object.update(record.extra)
|
log_object.update(record.extra)
|
||||||
|
|
||||||
# Include exception info if present
|
# Include exception info if present (for error tracking)
|
||||||
if record.exc_info:
|
if record.exc_info:
|
||||||
log_object["exception"] = self.formatException(record.exc_info)
|
log_object["exception"] = self.formatException(record.exc_info)
|
||||||
|
|
||||||
@ -30,7 +32,18 @@ class JsonFormatter(logging.Formatter):
|
|||||||
|
|
||||||
def setup_logger(log_folder: Path, log_file_name: str = "conversion.log", level=logging.INFO) -> logging.Logger:
|
def setup_logger(log_folder: Path, log_file_name: str = "conversion.log", level=logging.INFO) -> logging.Logger:
|
||||||
"""
|
"""
|
||||||
Sets up a logger that prints to console and writes to a rotating JSON log file.
|
Setup logger with structured JSON file output and disabled console output.
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- File (logs/conversion.log): JSON format with full context for programmatic parsing
|
||||||
|
- Console: Disabled (all user output handled via print() for clean terminal UI)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
logger.info("Processing complete", extra={
|
||||||
|
"file": "video.mkv",
|
||||||
|
"size_mb": 1024,
|
||||||
|
"duration_sec": 3600
|
||||||
|
})
|
||||||
"""
|
"""
|
||||||
log_folder.mkdir(parents=True, exist_ok=True)
|
log_folder.mkdir(parents=True, exist_ok=True)
|
||||||
log_file = log_folder / log_file_name
|
log_file = log_folder / log_file_name
|
||||||
@ -46,10 +59,11 @@ def setup_logger(log_folder: Path, log_file_name: str = "conversion.log", level=
|
|||||||
)
|
)
|
||||||
json_formatter = JsonFormatter()
|
json_formatter = JsonFormatter()
|
||||||
|
|
||||||
# Console handler (human-readable)
|
# Console handler (disabled - use print() for user-facing output)
|
||||||
|
# This prevents duplicate/ugly output mixing with terminal UI
|
||||||
console_handler = logging.StreamHandler()
|
console_handler = logging.StreamHandler()
|
||||||
console_handler.setFormatter(text_formatter)
|
console_handler.setFormatter(text_formatter)
|
||||||
console_handler.setLevel(level)
|
console_handler.setLevel(logging.CRITICAL + 1) # Effectively disable (above CRITICAL)
|
||||||
|
|
||||||
# File handler (JSON logs)
|
# File handler (JSON logs)
|
||||||
file_handler = RotatingFileHandler(log_file, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8")
|
file_handler = RotatingFileHandler(log_file, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8")
|
||||||
@ -66,8 +80,14 @@ def setup_logger(log_folder: Path, log_file_name: str = "conversion.log", level=
|
|||||||
|
|
||||||
def setup_failure_logger(log_folder: Path) -> logging.Logger:
|
def setup_failure_logger(log_folder: Path) -> logging.Logger:
|
||||||
"""
|
"""
|
||||||
Setup a dedicated logger for encoding failures.
|
Setup dedicated failure logger for encoding/processing failures.
|
||||||
Returns a logger that writes to logs/failure.log
|
|
||||||
|
Output:
|
||||||
|
- File (logs/failure.log): Simple text format with timestamp and failure message
|
||||||
|
- Use this for tracking files that failed processing for later analysis
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
failure_logger.warning(f"{file.name} | CQ mode failed: size threshold not met (95%)")
|
||||||
"""
|
"""
|
||||||
log_folder.mkdir(parents=True, exist_ok=True)
|
log_folder.mkdir(parents=True, exist_ok=True)
|
||||||
log_file = log_folder / "failure.log"
|
log_file = log_folder / "failure.log"
|
||||||
@ -94,3 +114,21 @@ def setup_failure_logger(log_folder: Path) -> logging.Logger:
|
|||||||
logger.propagate = False
|
logger.propagate = False
|
||||||
|
|
||||||
return logger
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
def log_event(logger: logging.Logger, level: str, message: str, **context):
|
||||||
|
"""
|
||||||
|
Log a structured event with context fields.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
logger: Logger instance
|
||||||
|
level: Log level ("debug", "info", "warning", "error")
|
||||||
|
message: Main message text
|
||||||
|
**context: Additional context fields (file, size, duration, etc)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
log_event(logger, "info", "Encoding complete",
|
||||||
|
file="video.mkv", size_mb=1024, method="CQ", reduction_pct=45)
|
||||||
|
"""
|
||||||
|
log_func = getattr(logger, level.lower(), logger.info)
|
||||||
|
log_func(message, extra=context)
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import csv
|
import csv
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
@ -48,6 +49,82 @@ def get_default_cq(folder: Path, config: dict, resolution: str, encoder: str = "
|
|||||||
return cq_config.get(key, 28) # Default fallback to 28
|
return cq_config.get(key, 28) # Default fallback to 28
|
||||||
|
|
||||||
|
|
||||||
|
def get_media_context(file: Path, root_folder: Path) -> dict:
|
||||||
|
"""
|
||||||
|
Extract media context from file path for structured logging.
|
||||||
|
|
||||||
|
Parses directory structure to identify show name, media type (TV/Movie),
|
||||||
|
season/episode numbers for grouping logs later.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file: File path to analyze
|
||||||
|
root_folder: Root processing folder to use as reference
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with keys: media_type, show_name, season (optional), episode (optional), video_filename
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
P:\\tv\\Breaking Bad\\season01\\episode01.mkv
|
||||||
|
→ {"media_type": "tv", "show_name": "Breaking Bad", "season": "01", "episode": "01"}
|
||||||
|
|
||||||
|
P:\\movies\\Inception.mkv
|
||||||
|
→ {"media_type": "movie", "show_name": "Inception"}
|
||||||
|
"""
|
||||||
|
parts = file.parts
|
||||||
|
root_parts = root_folder.parts
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"video_filename": file.name,
|
||||||
|
"media_type": None,
|
||||||
|
"show_name": None,
|
||||||
|
"season": None,
|
||||||
|
"episode": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find where media type (tv/movie/anime) appears in path
|
||||||
|
path_lower = str(file).lower()
|
||||||
|
|
||||||
|
if "\\tv\\" in path_lower or "/tv/" in path_lower:
|
||||||
|
context["media_type"] = "tv"
|
||||||
|
elif "\\anime\\" in path_lower or "/anime/" in path_lower:
|
||||||
|
context["media_type"] = "anime"
|
||||||
|
elif "\\movies\\" in path_lower or "/movies/" in path_lower:
|
||||||
|
context["media_type"] = "movie"
|
||||||
|
else:
|
||||||
|
# Default to movie if path structure unclear
|
||||||
|
context["media_type"] = "other"
|
||||||
|
|
||||||
|
# Extract show name (directory immediately after media type)
|
||||||
|
try:
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
part_lower = part.lower()
|
||||||
|
if part_lower in ("tv", "anime", "movies"):
|
||||||
|
# Next part is show name
|
||||||
|
if i + 1 < len(parts):
|
||||||
|
context["show_name"] = parts[i + 1]
|
||||||
|
|
||||||
|
# For TV/anime, check if there's a season folder
|
||||||
|
if context["media_type"] in ("tv", "anime") and i + 2 < len(parts):
|
||||||
|
season_part = parts[i + 2].lower()
|
||||||
|
# Pattern: "season01", "s01", "season 1", etc.
|
||||||
|
import re
|
||||||
|
season_match = re.search(r's(?:eason)?\s*(\d+)', season_part)
|
||||||
|
if season_match:
|
||||||
|
context["season"] = season_match.group(1).zfill(2)
|
||||||
|
|
||||||
|
# Extract episode from filename
|
||||||
|
# Pattern: "e01", "episode01", "01", etc.
|
||||||
|
filename_lower = file.stem.lower()
|
||||||
|
ep_match = re.search(r'e(?:pisode)?\s*(\d+)', filename_lower)
|
||||||
|
if ep_match:
|
||||||
|
context["episode"] = ep_match.group(1).zfill(2)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not parse media context from {file}: {e}")
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
def _cleanup_temp_files(temp_input: Path, temp_output: Path):
|
def _cleanup_temp_files(temp_input: Path, temp_output: Path):
|
||||||
"""Helper function to clean up temporary input and output files."""
|
"""Helper function to clean up temporary input and output files."""
|
||||||
try:
|
try:
|
||||||
@ -65,6 +142,39 @@ def _cleanup_temp_files(temp_input: Path, temp_output: Path):
|
|||||||
logger.warning(f"Could not delete temp output {temp_output.name}: {e}")
|
logger.warning(f"Could not delete temp output {temp_output.name}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def should_skip_file(file: Path, no_encode: bool, unforce_subs: bool, force_process: bool, ignore_tags: list, travel_output_folder: Path) -> tuple:
|
||||||
|
"""
|
||||||
|
Determine if a file should be skipped from processing based on multiple criteria.
|
||||||
|
|
||||||
|
Skip conditions (in order):
|
||||||
|
1. If --no-encode + --unforce-subs: skip if file has no forced subtitles
|
||||||
|
2. If --force-process NOT set: skip if filename contains any ignore_tags (e.g., [EHX])
|
||||||
|
3. Travel mode always processes files (overrides ignore tags)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file: File path to check
|
||||||
|
no_encode: True if --no-encode flag is set
|
||||||
|
unforce_subs: True if --unforce-subs flag is set
|
||||||
|
force_process: True if --force-process flag is set (bypass ignore_tags)
|
||||||
|
ignore_tags: List of filename tags to skip (from config)
|
||||||
|
travel_output_folder: If set, travel mode is active (process all files)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (should_skip: bool, reason: str or None)
|
||||||
|
"""
|
||||||
|
# Check for forced subtitles if using --no-encode + --unforce-subs
|
||||||
|
if no_encode and unforce_subs:
|
||||||
|
if not has_forced_subtitles(file):
|
||||||
|
return True, "no forced subtitles found (--no-encode + --unforce-subs)"
|
||||||
|
|
||||||
|
# Skip files with ignore tags (unless force_process is enabled)
|
||||||
|
# In travel mode, don't skip files based on tags
|
||||||
|
if not force_process and not travel_output_folder and any(tag.lower() in file.name.lower() for tag in ignore_tags):
|
||||||
|
return True, "matches ignore tags"
|
||||||
|
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
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, filter_audio: bool = None, audio_select: str = None, encoder: str = "hevc", strip_all_titles: bool = False, travel_output_folder: Path = None, unforce_subs: bool = False, no_encode: bool = False, force_process: bool = False, replace_file: bool = False, wait_seconds: int = 0):
|
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, filter_audio: bool = None, audio_select: str = None, encoder: str = "hevc", strip_all_titles: bool = False, travel_output_folder: Path = None, unforce_subs: bool = False, no_encode: bool = False, force_process: bool = False, replace_file: bool = False, wait_seconds: int = 0):
|
||||||
"""
|
"""
|
||||||
Process all video files in folder with appropriate encoding settings.
|
Process all video files in folder with appropriate encoding settings.
|
||||||
@ -139,20 +249,18 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
|
|||||||
|
|
||||||
skipped_count = 0
|
skipped_count = 0
|
||||||
for file in folder.rglob("*"):
|
for file in folder.rglob("*"):
|
||||||
|
# Skip hidden files/directories (starting with . or ._)
|
||||||
|
if file.name.startswith('.') or file.name.startswith('._'):
|
||||||
|
continue
|
||||||
|
|
||||||
if file.suffix.lower() not in extensions:
|
if file.suffix.lower() not in extensions:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if using --no-encode with --unforce-subs: skip files with no forced subs
|
# Check if file should be skipped
|
||||||
if no_encode and unforce_subs:
|
should_skip, skip_reason = should_skip_file(file, no_encode, unforce_subs, force_process, ignore_tags, travel_output_folder)
|
||||||
if not has_forced_subtitles(file):
|
if should_skip:
|
||||||
logger.info(f"Skipping {file.name}: no forced subtitles found (--no-encode + --unforce-subs)")
|
logger.info(f"Skipping {file.name}: {skip_reason}")
|
||||||
print(f"⏭️ Skipping {file.name}: no forced subtitles found")
|
print(f"⏭️ Skipping {file.name}: {skip_reason}")
|
||||||
skipped_count += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip files with ignore tags (unless force_process is enabled)
|
|
||||||
# In travel mode, don't skip files based on tags - we process everything
|
|
||||||
if not force_process and not travel_output_folder and any(tag.lower() in file.name.lower() for tag in ignore_tags):
|
|
||||||
skipped_count += 1
|
skipped_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -161,8 +269,11 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
|
|||||||
logger.info(f"Skipped {skipped_count} file(s)")
|
logger.info(f"Skipped {skipped_count} file(s)")
|
||||||
skipped_count = 0
|
skipped_count = 0
|
||||||
|
|
||||||
|
# Extract media context for structured logging
|
||||||
|
media_context = get_media_context(file, folder)
|
||||||
|
|
||||||
print("="*60)
|
print("="*60)
|
||||||
logger.info(f"Processing: {file.name}")
|
logger.info(f"Processing: {file.name}", extra=media_context)
|
||||||
print(f"📁 Processing: {file.name}")
|
print(f"📁 Processing: {file.name}")
|
||||||
|
|
||||||
temp_input = (processing_folder / file.name).resolve()
|
temp_input = (processing_folder / file.name).resolve()
|
||||||
@ -265,7 +376,7 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
|
|||||||
content_cq = encoder_cq_config.get(cq_key, 32)
|
content_cq = encoder_cq_config.get(cq_key, 32)
|
||||||
file_cq = cq if cq is not None else content_cq
|
file_cq = cq if cq is not None else content_cq
|
||||||
|
|
||||||
# Always output as .mkv (AV1 video codec) with [EHX] suffix
|
# Output file with suffix in processing folder (always .mkv container)
|
||||||
temp_output = (processing_folder / f"{file.stem}{suffix}.mkv").resolve()
|
temp_output = (processing_folder / f"{file.stem}{suffix}.mkv").resolve()
|
||||||
|
|
||||||
# Determine which method to try first
|
# Determine which method to try first
|
||||||
@ -336,7 +447,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
|
|||||||
'is_tv': is_tv,
|
'is_tv': is_tv,
|
||||||
'subtitle_file': subtitle_file,
|
'subtitle_file': subtitle_file,
|
||||||
'src_bit_depth': src_bit_depth,
|
'src_bit_depth': src_bit_depth,
|
||||||
'encoder': actual_encoder
|
'encoder': actual_encoder,
|
||||||
|
'media_context': media_context
|
||||||
})
|
})
|
||||||
consecutive_failures += 1
|
consecutive_failures += 1
|
||||||
if consecutive_failures >= max_consecutive:
|
if consecutive_failures >= max_consecutive:
|
||||||
@ -387,7 +499,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
|
|||||||
'target_resolution': target_resolution,
|
'target_resolution': target_resolution,
|
||||||
'file_cq': file_cq,
|
'file_cq': file_cq,
|
||||||
'is_tv': is_tv,
|
'is_tv': is_tv,
|
||||||
'subtitle_file': subtitle_file
|
'subtitle_file': subtitle_file,
|
||||||
|
'media_context': media_context
|
||||||
})
|
})
|
||||||
consecutive_failures += 1
|
consecutive_failures += 1
|
||||||
if consecutive_failures >= max_consecutive:
|
if consecutive_failures >= max_consecutive:
|
||||||
@ -420,7 +533,7 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
|
|||||||
_save_successful_encoding(
|
_save_successful_encoding(
|
||||||
file, temp_input, temp_output, orig_size, out_size,
|
file, temp_input, temp_output, orig_size, out_size,
|
||||||
reduction_ratio, method, src_width, src_height, res_width, res_height,
|
reduction_ratio, method, src_width, src_height, res_width, res_height,
|
||||||
file_cq, tracker_file, folder, is_tv, suffix, config, test_mode, subtitle_file, travel_output_folder, replace_file, wait_seconds
|
file_cq, tracker_file, folder, is_tv, suffix, config, test_mode, subtitle_file, travel_output_folder, replace_file, wait_seconds, media_context
|
||||||
)
|
)
|
||||||
|
|
||||||
# In test mode, stop after first successful file
|
# In test mode, stop after first successful file
|
||||||
@ -498,7 +611,8 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
|
|||||||
file_data['res_width'], file_data['res_height'],
|
file_data['res_width'], file_data['res_height'],
|
||||||
file_data['file_cq'], tracker_file,
|
file_data['file_cq'], tracker_file,
|
||||||
folder, file_data['is_tv'], suffix, config, False,
|
folder, file_data['is_tv'], suffix, config, False,
|
||||||
file_data.get('subtitle_file'), travel_output_folder, replace_file, wait_seconds
|
file_data.get('subtitle_file'), travel_output_folder, replace_file, wait_seconds,
|
||||||
|
file_data.get('media_context')
|
||||||
)
|
)
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
@ -547,9 +661,12 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
|
|||||||
|
|
||||||
def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size,
|
def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size,
|
||||||
reduction_ratio, method, src_width, src_height, res_width, res_height,
|
reduction_ratio, method, src_width, src_height, res_width, res_height,
|
||||||
file_cq, tracker_file, folder, is_tv, suffix, config=None, test_mode=False, subtitle_file=None, travel_output_folder=None, replace_file: bool = False, wait_seconds: int = 0):
|
file_cq, tracker_file, folder, is_tv, suffix, config=None, test_mode=False, subtitle_file=None, travel_output_folder=None, replace_file: bool = False, wait_seconds: int = 0, media_context: dict = None):
|
||||||
"""Helper function to save successfully encoded files with [EHX] tag and clean up subtitle files."""
|
"""Helper function to save successfully encoded files with [EHX] tag and clean up subtitle files."""
|
||||||
|
|
||||||
|
if media_context is None:
|
||||||
|
media_context = {}
|
||||||
|
|
||||||
# In test mode, show ratio and skip file move/cleanup
|
# In test mode, show ratio and skip file move/cleanup
|
||||||
if test_mode:
|
if test_mode:
|
||||||
orig_size_mb = round(orig_size / 1e6, 2)
|
orig_size_mb = round(orig_size / 1e6, 2)
|
||||||
@ -565,7 +682,7 @@ def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size
|
|||||||
print(f"Method: {method} (CQ={file_cq if method == 'CQ' else 'N/A'})")
|
print(f"Method: {method} (CQ={file_cq if method == 'CQ' else 'N/A'})")
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
print(f"📁 Encoded file location: {temp_output}")
|
print(f"📁 Encoded file location: {temp_output}")
|
||||||
logger.info(f"TEST MODE - File: {file.name} | Ratio: {percentage}% | Method: {method}")
|
logger.info(f"TEST MODE - File: {file.name} | Ratio: {percentage}% | Method: {method}", extra=media_context)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if file is in a Featurettes folder - if so, remove suffix from destination filename
|
# Check if file is in a Featurettes folder - if so, remove suffix from destination filename
|
||||||
@ -592,11 +709,11 @@ def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size
|
|||||||
travel_dest_dir.mkdir(parents=True, exist_ok=True)
|
travel_dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
dest_file = travel_dest_dir / temp_output.name
|
dest_file = travel_dest_dir / temp_output.name
|
||||||
print(f"🧳 Travel mode: Moving to {dest_file}")
|
print(f"🧳 Travel mode: Moving to {dest_file}")
|
||||||
logger.info(f"Travel mode destination: {dest_file}")
|
logger.info(f"Travel mode destination: {dest_file}", extra=media_context)
|
||||||
|
|
||||||
shutil.move(temp_output, dest_file)
|
shutil.move(temp_output, dest_file)
|
||||||
print(f"🚚 Moved {temp_output.name} → {dest_file.name}")
|
print(f"🚚 Moved {temp_output.name} → {dest_file.name}")
|
||||||
logger.info(f"Moved {temp_output.name} → {dest_file.name}")
|
logger.info(f"Moved {temp_output.name} → {dest_file.name}", extra=media_context)
|
||||||
|
|
||||||
# Classify file type based on folder (folder_parts already defined earlier)
|
# Classify file type based on folder (folder_parts already defined earlier)
|
||||||
if "tv" in folder_parts:
|
if "tv" in folder_parts:
|
||||||
@ -635,10 +752,11 @@ def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size
|
|||||||
])
|
])
|
||||||
|
|
||||||
# Enhanced logging with all conversion details
|
# Enhanced logging with all conversion details
|
||||||
logger.info(f"\n✅ CONVERSION COMPLETE: {dest_file.name}")
|
log_context = {**media_context, "method": method, "original_size_mb": orig_size_mb, "output_size_mb": proc_size_mb, "reduction_pct": 100 - percentage}
|
||||||
logger.info(f" Type: {f_type.upper()} | Show: {show}")
|
logger.info(f"✅ CONVERSION COMPLETE: {dest_file.name}", extra=log_context)
|
||||||
logger.info(f" Size: {orig_size_mb}MB → {proc_size_mb}MB ({percentage}% of original, {100-percentage:.1f}% reduction)")
|
logger.info(f" Type: {f_type.upper()} | Show: {show}", extra=log_context)
|
||||||
logger.info(f" Method: {method} | Status: SUCCESS")
|
logger.info(f" Size: {orig_size_mb}MB → {proc_size_mb}MB ({percentage}% of original, {100-percentage:.1f}% reduction)", extra=log_context)
|
||||||
|
logger.info(f" Method: {method} | Status: SUCCESS", extra=log_context)
|
||||||
print(f"📝 Logged conversion: {dest_file.name} ({percentage}%), method={method}")
|
print(f"📝 Logged conversion: {dest_file.name} ({percentage}%), method={method}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -646,9 +764,9 @@ def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size
|
|||||||
|
|
||||||
# Keep original file if in travel mode, replace mode, or if in Featurettes folder
|
# Keep original file if in travel mode, replace mode, or if in Featurettes folder
|
||||||
if travel_output_folder:
|
if travel_output_folder:
|
||||||
logger.info(f"Travel mode: Kept original file {file.name}")
|
logger.info(f"Travel mode: Kept original file {file.name}", extra=media_context)
|
||||||
elif replace_file:
|
elif replace_file:
|
||||||
logger.info(f"Replace mode: Original file has been replaced with processed version at {file.name}")
|
logger.info(f"Replace mode: Original file has been replaced with processed version at {file.name}", extra=media_context)
|
||||||
elif not is_featurette:
|
elif not is_featurette:
|
||||||
file.unlink()
|
file.unlink()
|
||||||
logger.info(f"Deleted original and processing copy for {file.name}")
|
logger.info(f"Deleted original and processing copy for {file.name}")
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
# core/video_handler.py
|
# core/video_handler.py
|
||||||
"""Video resolution detection and encoding logic."""
|
"""Video resolution detection and encoding logic."""
|
||||||
|
|
||||||
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -190,8 +191,6 @@ def has_forced_subtitles(input_file: Path) -> bool:
|
|||||||
Returns True if at least one subtitle stream has forced=1 disposition.
|
Returns True if at least one subtitle stream has forced=1 disposition.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import json
|
|
||||||
|
|
||||||
# Method 1: Try JSON output (most reliable)
|
# Method 1: Try JSON output (most reliable)
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffprobe", "-v", "error",
|
"ffprobe", "-v", "error",
|
||||||
|
|||||||
1730
logs/conversion.log
1730
logs/conversion.log
File diff suppressed because it is too large
Load Diff
10
main.py
10
main.py
@ -106,12 +106,12 @@ Examples:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--encoder", dest="encoder", default="hevc",
|
"--encoder", dest="encoder", default="hevc",
|
||||||
choices=["hevc", "av1"],
|
choices=["hevc", "av1"],
|
||||||
help="Video encoder: 'hevc' for HEVC NVENC 10-bit (default), 'av1' for AV1 NVENC 8-bit"
|
help="Video encoder: 'hevc' for HEVC NVENC 10-bit (default), 'av1' for AV1 NVENC 8-bit. Auto-selected based on source bit depth if not specified"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--r", "--resolution", dest="resolution", default=None,
|
"--r", "--resolution", dest="resolution", default=None,
|
||||||
choices=["480", "720", "1080"],
|
choices=["480", "720", "1080"],
|
||||||
help="Force target resolution (if not specified: 4K->1080p, else preserve)"
|
help="Target resolution (acts as max, downscales if source is larger). If not specified: 4K→1080p, else preserve source"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--test", dest="test_mode", default=False, action="store_true",
|
"--test", dest="test_mode", default=False, action="store_true",
|
||||||
@ -127,11 +127,11 @@ Examples:
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--audio-select", dest="audio_select", default=None,
|
"--audio-select", dest="audio_select", default=None,
|
||||||
help="Pre-select audio streams to keep (comma-separated, e.g., 1,2). Skips interactive prompt. Requires --filter-audio"
|
help="Pre-select audio streams to keep (comma-separated, e.g., 1,2). Skips interactive prompt"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--keep-all-titles", dest="strip_all_titles", default=True, action="store_false",
|
"--keep-all-titles", dest="strip_all_titles", default=True, action="store_false",
|
||||||
help="Keep title metadata from all audio tracks (default: False, titles are stripped)"
|
help="Preserve title metadata on audio tracks (default: titles are stripped)"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--unforce-subs", dest="unforce_subs", default=False, action="store_true",
|
"--unforce-subs", dest="unforce_subs", default=False, action="store_true",
|
||||||
@ -151,7 +151,7 @@ Examples:
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--wait", "-w", dest="wait_seconds", type=int, nargs='?', const=-1, default=None,
|
"--wait", "-w", dest="wait_seconds", type=int, nargs='?', const=-1, default=None,
|
||||||
help="Wait after each successful file (default: 30s if --no-encode, 0s otherwise). Use --wait 0 to disable, --wait 60 for custom"
|
help="Wait after each file (default: 30s with --no-encode, 0s otherwise). Gives Plex time to detect changes"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--travel", dest="travel_mode", default=False, action="store_true",
|
"--travel", dest="travel_mode", default=False, action="store_true",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user