improvements
This commit is contained in:
parent
e8e8a032b1
commit
6b278ebc7d
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
processing/*
|
||||||
|
__pycache__
|
||||||
85
config.xml
Normal file
85
config.xml
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<config>
|
||||||
|
|
||||||
|
<!-- =============================
|
||||||
|
GENERAL SETTINGS
|
||||||
|
============================= -->
|
||||||
|
<general>
|
||||||
|
<!-- Default temporary working folder (relative to script) -->
|
||||||
|
<processing_folder>processing</processing_folder>
|
||||||
|
|
||||||
|
<!-- File suffix added to encoded outputs -->
|
||||||
|
<suffix> -EHX</suffix>
|
||||||
|
|
||||||
|
<!-- Allowed input extensions -->
|
||||||
|
<extensions>.mkv,.mp4</extensions>
|
||||||
|
</general>
|
||||||
|
|
||||||
|
<!-- =============================
|
||||||
|
PATH MAPPINGS (Windows → Linux)
|
||||||
|
============================= -->
|
||||||
|
<path_mappings>
|
||||||
|
<map from="P:\tv" to="/mnt/plex/tv" />
|
||||||
|
<map from="P:\anime" to="/mnt/plex/anime" />
|
||||||
|
</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>
|
||||||
|
<movie_1080>32</movie_1080>
|
||||||
|
<movie_720>34</movie_720>
|
||||||
|
</cq>
|
||||||
|
|
||||||
|
<!-- Fallback bitrate-based mode -->
|
||||||
|
<fallback>
|
||||||
|
<bitrate_1080>1500k</bitrate_1080>
|
||||||
|
<maxrate_1080>1750k</maxrate_1080>
|
||||||
|
<bufsize_1080>2250k</bufsize_1080>
|
||||||
|
|
||||||
|
<bitrate_720>900k</bitrate_720>
|
||||||
|
<maxrate_720>1250k</maxrate_720>
|
||||||
|
<bufsize_720>1600k</bufsize_720>
|
||||||
|
</fallback>
|
||||||
|
|
||||||
|
<!-- Scale filter defaults -->
|
||||||
|
<filters>
|
||||||
|
<default>lanczos</default>
|
||||||
|
<tv>bicubic</tv>
|
||||||
|
</filters>
|
||||||
|
</encode>
|
||||||
|
|
||||||
|
<!-- =============================
|
||||||
|
AUDIO BUCKETS
|
||||||
|
============================= -->
|
||||||
|
<audio>
|
||||||
|
<stereo>
|
||||||
|
<low>64000</low>
|
||||||
|
<medium>96000</medium>
|
||||||
|
<high>128000</high>
|
||||||
|
</stereo>
|
||||||
|
<multi_channel>
|
||||||
|
<low>160000</low>
|
||||||
|
<high>192000</high>
|
||||||
|
</multi_channel>
|
||||||
|
</audio>
|
||||||
|
|
||||||
|
</config>
|
||||||
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
122
core/config_helper.py
Normal file
122
core/config_helper.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Default XML content to write if missing
|
||||||
|
DEFAULT_XML = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<config>
|
||||||
|
<general>
|
||||||
|
<processing_folder>processing</processing_folder>
|
||||||
|
<suffix> -EHX</suffix>
|
||||||
|
<extensions>.mkv,.mp4</extensions>
|
||||||
|
</general>
|
||||||
|
<path_mappings>
|
||||||
|
<map from="P:\\tv" to="/mnt/plex/tv" />
|
||||||
|
<map from="P:\\anime" to="/mnt/plex/anime" />
|
||||||
|
</path_mappings>
|
||||||
|
<encode>
|
||||||
|
<cq>
|
||||||
|
<tv_1080>28</tv_1080>
|
||||||
|
<tv_720>32</tv_720>
|
||||||
|
<movie_1080>32</movie_1080>
|
||||||
|
<movie_720>34</movie_720>
|
||||||
|
</cq>
|
||||||
|
<fallback>
|
||||||
|
<bitrate_1080>1500k</bitrate_1080>
|
||||||
|
<maxrate_1080>1750k</maxrate_1080>
|
||||||
|
<bufsize_1080>2250k</bufsize_1080>
|
||||||
|
<bitrate_720>900k</bitrate_720>
|
||||||
|
<maxrate_720>1250k</maxrate_720>
|
||||||
|
<bufsize_720>1600k</bufsize_720>
|
||||||
|
</fallback>
|
||||||
|
<filters>
|
||||||
|
<default>lanczos</default>
|
||||||
|
<tv>bicubic</tv>
|
||||||
|
</filters>
|
||||||
|
</encode>
|
||||||
|
<audio>
|
||||||
|
<stereo>
|
||||||
|
<low>64000</low>
|
||||||
|
<medium>96000</medium>
|
||||||
|
<high>128000</high>
|
||||||
|
</stereo>
|
||||||
|
<multi_channel>
|
||||||
|
<low>160000</low>
|
||||||
|
<high>192000</high>
|
||||||
|
</multi_channel>
|
||||||
|
</audio>
|
||||||
|
</config>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def load_config_xml(path: Path) -> dict:
|
||||||
|
if not path.exists():
|
||||||
|
path.write_text(DEFAULT_XML, encoding="utf-8")
|
||||||
|
print(f"ℹ️ Created default config.xml at {path}")
|
||||||
|
|
||||||
|
tree = ET.parse(path)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# --- General ---
|
||||||
|
general = root.find("general")
|
||||||
|
processing_folder_elem = general.find("processing_folder") if general is not None else None
|
||||||
|
processing_folder = processing_folder_elem.text if processing_folder_elem is not None else "processing"
|
||||||
|
|
||||||
|
suffix_elem = general.find("suffix") if general is not None else None
|
||||||
|
suffix = suffix_elem.text if suffix_elem is not None else " -EHX"
|
||||||
|
|
||||||
|
extensions_elem = general.find("extensions") if general is not None else None
|
||||||
|
extensions = extensions_elem.text.split(",") if extensions_elem is not None else [".mkv", ".mp4"]
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
|
||||||
|
# --- Encode ---
|
||||||
|
encode_elem = root.find("encode")
|
||||||
|
cq = {}
|
||||||
|
fallback = {}
|
||||||
|
filters = {}
|
||||||
|
if encode_elem is not None:
|
||||||
|
cq_elem = encode_elem.find("cq")
|
||||||
|
if cq_elem is not None:
|
||||||
|
for child in cq_elem:
|
||||||
|
if child.text:
|
||||||
|
cq[child.tag] = int(child.text)
|
||||||
|
|
||||||
|
fallback_elem = encode_elem.find("fallback")
|
||||||
|
if fallback_elem is not None:
|
||||||
|
for child in fallback_elem:
|
||||||
|
if child.text:
|
||||||
|
fallback[child.tag] = child.text
|
||||||
|
|
||||||
|
filters_elem = encode_elem.find("filters")
|
||||||
|
if filters_elem is not None:
|
||||||
|
for child in filters_elem:
|
||||||
|
if child.text:
|
||||||
|
filters[child.tag] = child.text
|
||||||
|
|
||||||
|
# --- Audio ---
|
||||||
|
audio = {"stereo": {}, "multi_channel": {}}
|
||||||
|
stereo_elem = root.find("audio/stereo")
|
||||||
|
if stereo_elem is not None:
|
||||||
|
for child in stereo_elem:
|
||||||
|
if child.text:
|
||||||
|
audio["stereo"][child.tag] = int(child.text)
|
||||||
|
|
||||||
|
multi_elem = root.find("audio/multi_channel")
|
||||||
|
if multi_elem is not None:
|
||||||
|
for child in multi_elem:
|
||||||
|
if child.text:
|
||||||
|
audio["multi_channel"][child.tag] = int(child.text)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"processing_folder": processing_folder,
|
||||||
|
"suffix": suffix,
|
||||||
|
"extensions": [ext.lower() for ext in extensions],
|
||||||
|
"path_mappings": path_mappings,
|
||||||
|
"encode": {"cq": cq, "fallback": fallback, "filters": filters},
|
||||||
|
"audio": audio
|
||||||
|
}
|
||||||
190
main.py
190
main.py
@ -1,101 +1,31 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import requests
|
|
||||||
|
|
||||||
# =============================
|
from core.config_helper import load_config_xml
|
||||||
# CONFIGURATION
|
|
||||||
# =============================
|
|
||||||
SONARR_URL = "http://10.0.0.10:8989/api/v3"
|
|
||||||
RADARR_URL = "http://10.0.0.10:7878/api/v3"
|
|
||||||
SONARR_API_KEY = os.getenv("SONARR_API_KEY", "")
|
|
||||||
RADARR_API_KEY = os.getenv("RADARR_API_KEY", "")
|
|
||||||
|
|
||||||
PATH_MAPPINGS = {
|
|
||||||
"P:\\tv": "/mnt/plex/tv",
|
|
||||||
"P:\\anime": "/mnt/plex/anime",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Relative processing folder next to the script
|
|
||||||
DEFAULT_PROCESSING_FOLDER = Path(__file__).parent / "processing"
|
|
||||||
|
|
||||||
SUFFIX = " -EHX"
|
|
||||||
|
|
||||||
# =============================
|
# =============================
|
||||||
# AUDIO BUCKET LOGIC
|
# AUDIO BUCKET LOGIC
|
||||||
# =============================
|
# =============================
|
||||||
def choose_audio_bitrate(channels: int, bitrate_kbps: int) -> int:
|
def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict) -> int:
|
||||||
if channels == 2:
|
if channels == 2:
|
||||||
if bitrate_kbps < 80:
|
if bitrate_kbps < 80:
|
||||||
return 64000
|
return audio_config["stereo"]["low"]
|
||||||
elif bitrate_kbps < 112:
|
elif bitrate_kbps < 112:
|
||||||
return 96000
|
return audio_config["stereo"]["medium"]
|
||||||
else:
|
else:
|
||||||
return 128000
|
return audio_config["stereo"]["high"]
|
||||||
else:
|
else:
|
||||||
if bitrate_kbps < 176:
|
if bitrate_kbps < 176:
|
||||||
return 160000
|
return audio_config["multi_channel"]["low"]
|
||||||
else:
|
else:
|
||||||
return 192000
|
return audio_config["multi_channel"]["high"]
|
||||||
|
|
||||||
# =============================
|
# =============================
|
||||||
# PATH NORMALIZATION FOR SONARR/RADARR
|
# AUDIO STREAM DETECTION
|
||||||
# =============================
|
|
||||||
def normalize_path_for_service(local_path: str) -> str:
|
|
||||||
for win_path, linux_path in PATH_MAPPINGS.items():
|
|
||||||
if local_path.lower().startswith(win_path.lower()):
|
|
||||||
return local_path.replace(win_path, linux_path).replace("\\", "/")
|
|
||||||
return local_path.replace("\\", "/")
|
|
||||||
|
|
||||||
# =============================
|
|
||||||
# SONARR / RADARR RENAME
|
|
||||||
# =============================
|
|
||||||
def get_service_preferred_name(input_file: Path, service="sonarr") -> str | None:
|
|
||||||
api_key = SONARR_API_KEY if service == "sonarr" else RADARR_API_KEY
|
|
||||||
url_base = SONARR_URL if service == "sonarr" else RADARR_URL
|
|
||||||
|
|
||||||
if not api_key:
|
|
||||||
print(f"⚠️ No {service.upper()} API key; skipping rename lookup.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
norm_path = normalize_path_for_service(str(input_file))
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = requests.get(f"{url_base}/episodefile" if service=="sonarr" else f"{url_base}/moviefile",
|
|
||||||
headers={"X-Api-Key": api_key}, timeout=10)
|
|
||||||
r.raise_for_status()
|
|
||||||
all_files = r.json()
|
|
||||||
|
|
||||||
for f in all_files:
|
|
||||||
if f.get("path", "").lower() == norm_path.lower():
|
|
||||||
id_field = "id"
|
|
||||||
series_id_field = "seriesId" if service=="sonarr" else "movieId"
|
|
||||||
preview_endpoint = "rename/preview"
|
|
||||||
|
|
||||||
series_id = f[series_id_field]
|
|
||||||
file_id = f[id_field]
|
|
||||||
|
|
||||||
preview = requests.post(f"{url_base}/{preview_endpoint}",
|
|
||||||
headers={"X-Api-Key": api_key, "Content-Type": "application/json"},
|
|
||||||
json=[{series_id_field: series_id, "episodeFileId" if service=="sonarr" else "movieFileId": file_id}],
|
|
||||||
timeout=10)
|
|
||||||
preview.raise_for_status()
|
|
||||||
data = preview.json()
|
|
||||||
if data and "newName" in data[0]:
|
|
||||||
new_name = data[0]["newName"]
|
|
||||||
print(f"✅ {service.capitalize()} rename: {input_file.name} → {new_name}")
|
|
||||||
return new_name
|
|
||||||
print(f"ℹ️ No {service.capitalize()} match found for {input_file.name}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ {service.capitalize()} rename lookup failed: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# =============================
|
|
||||||
# AUDIO STREAMS DETECTION
|
|
||||||
# =============================
|
# =============================
|
||||||
def get_audio_streams(input_file: Path):
|
def get_audio_streams(input_file: Path):
|
||||||
cmd = [
|
cmd = [
|
||||||
@ -118,7 +48,8 @@ def get_audio_streams(input_file: Path):
|
|||||||
# =============================
|
# =============================
|
||||||
# FFmpeg ENCODE
|
# FFmpeg ENCODE
|
||||||
# =============================
|
# =============================
|
||||||
def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, scale_height: int, filter_flags: str):
|
def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, scale_height: int,
|
||||||
|
filter_flags: str, audio_config: dict, encode_config: dict):
|
||||||
streams = get_audio_streams(input_file)
|
streams = get_audio_streams(input_file)
|
||||||
|
|
||||||
print("\n🧩 ENCODE SETTINGS")
|
print("\n🧩 ENCODE SETTINGS")
|
||||||
@ -129,54 +60,39 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
|
|||||||
|
|
||||||
print(" • Audio Streams:")
|
print(" • Audio Streams:")
|
||||||
for (index, channels, bitrate) in streams:
|
for (index, channels, bitrate) in streams:
|
||||||
br = choose_audio_bitrate(channels, bitrate)
|
br = choose_audio_bitrate(channels, bitrate, audio_config)
|
||||||
print(f" - Stream #{index}: {channels}ch, orig≈{bitrate}kbps → target {br/1000:.1f}kbps")
|
print(f" - Stream #{index}: {channels}ch, orig≈{bitrate}kbps → target {br/1000:.1f}kbps")
|
||||||
|
|
||||||
# --- Build CQ encode command ---
|
# Build CQ encode command
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg", "-y", "-i", str(input_file),
|
"ffmpeg", "-y", "-i", str(input_file),
|
||||||
"-vf", f"scale={scale_width}:{scale_height}:flags={filter_flags}:force_original_aspect_ratio=decrease",
|
"-vf", f"scale={scale_width}:{scale_height}:flags={filter_flags}:force_original_aspect_ratio=decrease",
|
||||||
"-map", "0:v", "-map", "0:a", "-map", "0:s?",
|
"-map", "0:v", "-map", "0:a", "-map", "0:s?",
|
||||||
"-c:v", "av1_nvenc", "-preset", "p1", f"-cq", str(cq), "-pix_fmt", "p010le"
|
"-c:v", "av1_nvenc", "-preset", "p1", "-cq", str(cq), "-pix_fmt", "p010le"
|
||||||
]
|
]
|
||||||
|
|
||||||
for i, (index, channels, bitrate) in enumerate(streams):
|
for i, (index, channels, bitrate) in enumerate(streams):
|
||||||
br = choose_audio_bitrate(channels, bitrate)
|
br = choose_audio_bitrate(channels, bitrate, audio_config)
|
||||||
cmd += [f"-c:a:{i}", "aac", f"-b:a:{i}", str(br), f"-ac:{i}", str(channels)]
|
cmd += [f"-c:a:{i}", "aac", f"-b:a:{i}", str(br), f"-ac:{i}", str(channels)]
|
||||||
|
|
||||||
cmd += ["-c:s", "copy", str(output_file)]
|
cmd += ["-c:s", "copy", str(output_file)]
|
||||||
|
|
||||||
print(f"\n🎬 Running CQ encode: {output_file.name}")
|
|
||||||
subprocess.run(cmd, check=True)
|
subprocess.run(cmd, check=True)
|
||||||
|
|
||||||
# --- Check size reduction ---
|
# Check size reduction
|
||||||
orig_size = input_file.stat().st_size
|
orig_size = input_file.stat().st_size
|
||||||
out_size = output_file.stat().st_size
|
out_size = output_file.stat().st_size
|
||||||
reduction_ratio = out_size / orig_size
|
reduction_ratio = out_size / orig_size
|
||||||
print(f"📦 Original: {orig_size/1e6:.2f} MB → Encoded: {out_size/1e6:.2f} MB ({reduction_ratio:.1%} of original)")
|
print(f"📦 Original: {orig_size/1e6:.2f} MB → Encoded: {out_size/1e6:.2f} MB ({reduction_ratio:.1%} of original)")
|
||||||
|
|
||||||
# --- Fallback if too large ---
|
# Fallback if too large
|
||||||
if reduction_ratio >= 0.5:
|
if reduction_ratio >= 0.5:
|
||||||
print(f"⚠️ Size reduction insufficient ({reduction_ratio:.0%}). Retrying with bitrate-based encode...")
|
print(f"⚠️ Size reduction insufficient ({reduction_ratio:.0%}). Retrying with bitrate-based encode...")
|
||||||
|
|
||||||
output_file.unlink(missing_ok=True)
|
output_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
# Pick bitrate settings based on resolution
|
|
||||||
if scale_height >= 1080:
|
if scale_height >= 1080:
|
||||||
vb, maxrate, bufsize = "1500k", "1750k", "2250k"
|
vb, maxrate, bufsize = encode_config["fallback"]["bitrate_1080"], encode_config["fallback"]["maxrate_1080"], encode_config["fallback"]["bufsize_1080"]
|
||||||
else:
|
else:
|
||||||
vb, maxrate, bufsize = "900k", "1250k", "1600k"
|
vb, maxrate, bufsize = encode_config["fallback"]["bitrate_720"], encode_config["fallback"]["maxrate_720"], encode_config["fallback"]["bufsize_720"]
|
||||||
|
|
||||||
print("\n🧩 FALLBACK SETTINGS")
|
|
||||||
print(f" • Bitrate Mode: Target {vb}, Maxrate {maxrate}, Bufsize {bufsize}")
|
|
||||||
print(f" • Resolution: {scale_width}x{scale_height}")
|
|
||||||
print(f" • Filter: {filter_flags}")
|
|
||||||
print(" • Audio Streams:")
|
|
||||||
for (index, channels, bitrate) in streams:
|
|
||||||
br = choose_audio_bitrate(channels, bitrate)
|
|
||||||
print(f" - Stream #{index}: {channels}ch → {br/1000:.1f}kbps AAC")
|
|
||||||
|
|
||||||
# --- Build fallback command ---
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg", "-y", "-i", str(input_file),
|
"ffmpeg", "-y", "-i", str(input_file),
|
||||||
"-vf", f"scale={scale_width}:{scale_height}:flags={filter_flags}:force_original_aspect_ratio=decrease",
|
"-vf", f"scale={scale_width}:{scale_height}:flags={filter_flags}:force_original_aspect_ratio=decrease",
|
||||||
@ -185,45 +101,41 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
|
|||||||
"-b:v", vb, "-maxrate", maxrate, "-bufsize", bufsize,
|
"-b:v", vb, "-maxrate", maxrate, "-bufsize", bufsize,
|
||||||
"-pix_fmt", "p010le"
|
"-pix_fmt", "p010le"
|
||||||
]
|
]
|
||||||
|
|
||||||
for i, (index, channels, bitrate) in enumerate(streams):
|
for i, (index, channels, bitrate) in enumerate(streams):
|
||||||
br = choose_audio_bitrate(channels, bitrate)
|
br = choose_audio_bitrate(channels, bitrate, audio_config)
|
||||||
cmd += [f"-c:a:{i}", "aac", f"-b:a:{i}", str(br), f"-ac:{i}", str(channels)]
|
cmd += [f"-c:a:{i}", "aac", f"-b:a:{i}", str(br), f"-ac:{i}", str(channels)]
|
||||||
|
|
||||||
cmd += ["-c:s", "copy", str(output_file)]
|
cmd += ["-c:s", "copy", str(output_file)]
|
||||||
|
|
||||||
print(f"\n🎬 Running fallback bitrate encode: {output_file.name}")
|
|
||||||
subprocess.run(cmd, check=True)
|
subprocess.run(cmd, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# =============================
|
# =============================
|
||||||
# PROCESS FOLDER
|
# PROCESS FOLDER
|
||||||
# =============================
|
# =============================
|
||||||
def process_folder(folder: Path, cq: int, resolution: str, rename: bool, processing_folder: Path):
|
def process_folder(folder: Path, cq: int, resolution: str, processing_folder: Path, config: dict):
|
||||||
if not folder.exists():
|
if not folder.exists():
|
||||||
print(f"❌ Folder not found: {folder}")
|
print(f"❌ Folder not found: {folder}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Determine defaults based on folder type and resolution
|
filter_flags_default = config["encode"]["filters"]["default"]
|
||||||
filter_flags = "lanczos"
|
filter_flags_tv = config["encode"]["filters"]["tv"]
|
||||||
res_height = 1080 if resolution == "1080" else 720
|
|
||||||
res_width = 1920 if resolution == "1080" else 1280
|
|
||||||
|
|
||||||
folder_lower = str(folder).lower()
|
folder_lower = str(folder).lower()
|
||||||
if "\\tv\\" in folder_lower or "/tv/" in folder_lower:
|
if "\\tv\\" in folder_lower or "/tv/" in folder_lower:
|
||||||
filter_flags = "bicubic"
|
filter_flags = filter_flags_tv
|
||||||
cq_default = 28 if resolution=="1080" else 32
|
cq_default_key = f"tv_{resolution}"
|
||||||
else:
|
else:
|
||||||
cq_default = 32 if resolution=="1080" else 34
|
filter_flags = filter_flags_default
|
||||||
|
cq_default_key = f"movie_{resolution}"
|
||||||
|
|
||||||
if cq is None:
|
if cq is None:
|
||||||
cq = cq_default
|
cq = config["encode"]["cq"][cq_default_key]
|
||||||
|
|
||||||
|
res_height = 1080 if resolution == "1080" else 720
|
||||||
|
res_width = 1920 if resolution == "1080" else 1280
|
||||||
|
|
||||||
processing_folder.mkdir(parents=True, exist_ok=True)
|
processing_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
for file in folder.rglob("*"):
|
for file in folder.rglob("*"):
|
||||||
if file.suffix.lower() not in [".mkv", ".mp4"]:
|
if file.suffix.lower() not in config["extensions"]:
|
||||||
continue
|
continue
|
||||||
if any(tag in file.name.lower() for tag in ["ehx", "megusta"]):
|
if any(tag in file.name.lower() for tag in ["ehx", "megusta"]):
|
||||||
print(f"⏭️ Skipping: {file.name}")
|
print(f"⏭️ Skipping: {file.name}")
|
||||||
@ -232,33 +144,22 @@ def process_folder(folder: Path, cq: int, resolution: str, rename: bool, process
|
|||||||
print("="*60)
|
print("="*60)
|
||||||
print(f"📁 Processing: {file.name}")
|
print(f"📁 Processing: {file.name}")
|
||||||
|
|
||||||
# --- Copy to processing folder first ---
|
|
||||||
temp_input = processing_folder / file.name
|
temp_input = processing_folder / file.name
|
||||||
shutil.copy2(file, temp_input)
|
shutil.copy2(file, temp_input)
|
||||||
|
temp_output = processing_folder / f"{file.stem}{config['suffix']}{file.suffix}"
|
||||||
|
|
||||||
# --- Run FFmpeg ---
|
run_ffmpeg(temp_input, temp_output, cq, res_width, res_height, filter_flags,
|
||||||
temp_output = processing_folder / f"{file.stem}{SUFFIX}{file.suffix}"
|
config["audio"], config["encode"])
|
||||||
run_ffmpeg(temp_input, temp_output, cq, res_width, res_height, filter_flags)
|
|
||||||
|
|
||||||
# --- Optional rename via Sonarr/Radarr ---
|
dest_file = file.parent / temp_output.name
|
||||||
final_name = temp_output.name
|
|
||||||
if rename:
|
|
||||||
rename_file = get_service_preferred_name(temp_input, "sonarr")
|
|
||||||
if not rename_file:
|
|
||||||
rename_file = get_service_preferred_name(temp_input, "radarr")
|
|
||||||
if rename_file:
|
|
||||||
final_name = rename_file + temp_output.suffix
|
|
||||||
|
|
||||||
# --- Move to completed folder or back to original ---
|
|
||||||
dest_file = file.parent / final_name
|
|
||||||
shutil.move(temp_output, dest_file)
|
shutil.move(temp_output, dest_file)
|
||||||
print(f"🚚 Moved {temp_output.name} → {dest_file.name}")
|
print(f"🚚 Moved {temp_output.name} → {dest_file.name}")
|
||||||
|
|
||||||
# --- Cleanup ---
|
# Cleanup
|
||||||
if dest_file.exists():
|
if dest_file.exists():
|
||||||
try:
|
try:
|
||||||
temp_input.unlink() # remove processing copy
|
temp_input.unlink()
|
||||||
file.unlink() # remove original
|
file.unlink()
|
||||||
print(f"🧹 Deleted original and processing copy")
|
print(f"🧹 Deleted original and processing copy")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Could not delete files: {e}")
|
print(f"⚠️ Could not delete files: {e}")
|
||||||
@ -267,16 +168,21 @@ def process_folder(folder: Path, cq: int, resolution: str, rename: bool, process
|
|||||||
# MAIN
|
# MAIN
|
||||||
# =============================
|
# =============================
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Batch encode videos with optional Sonarr/Radarr rename")
|
parser = argparse.ArgumentParser(description="Batch encode videos")
|
||||||
parser.add_argument("folder", help="Path to folder containing videos")
|
parser.add_argument("folder", help="Path to folder containing videos")
|
||||||
parser.add_argument("--cq", type=int, help="Override default CQ")
|
parser.add_argument("--cq", type=int, help="Override default CQ")
|
||||||
parser.add_argument("--r", "--resolution", dest="resolution", default="1080", choices=["720","1080"], help="Target resolution (720 or 1080)")
|
parser.add_argument("--r", "--resolution", dest="resolution", choices=["720","1080"], help="Target resolution (720 or 1080)")
|
||||||
parser.add_argument("--rename", action="store_true", help="Attempt Sonarr/Radarr rename")
|
parser.add_argument("--processing", type=str, help="Processing folder")
|
||||||
parser.add_argument("--processing", type=str, default=str(DEFAULT_PROCESSING_FOLDER), help="Processing folder")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
process_folder(Path(args.folder), args.cq, args.resolution, args.rename, Path(args.processing))
|
config_path = Path(__file__).parent / "config.xml"
|
||||||
|
config = load_config_xml(config_path)
|
||||||
|
|
||||||
|
cq = args.cq if args.cq is not None else None
|
||||||
|
resolution = args.resolution if args.resolution else "1080"
|
||||||
|
processing_folder = Path(args.processing) if args.processing else Path(config["processing_folder"])
|
||||||
|
|
||||||
|
process_folder(Path(args.folder), cq, resolution, processing_folder, config)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user