ffmpeg handling py
proper testing for chapters and metadata remove all russian finction
This commit is contained in:
1 parent
99573fb241
commit
c5cf55fe21
7 files changed
+854
-352
No files matched your search
@@ -7,7 +7,7 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from serverlibrary import get_audio_tracks
|
||||||
|
|
||||||
def check_command(command):
|
def check_command(command):
|
||||||
if shutil.which(command) is None:
|
if shutil.which(command) is None:
|
||||||
@@ -60,52 +60,6 @@ def scan_dvd(dvd_name: Path):
|
|||||||
|
|
||||||
return titles
|
return titles
|
||||||
|
|
||||||
def get_audio_tracks(dvd, title):
|
|
||||||
"""Return HandBrake audio track information for a DVD title."""
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
[
|
|
||||||
"HandBrakeCLI",
|
|
||||||
"-i", dvd,
|
|
||||||
"-t", str(title),
|
|
||||||
"--scan",
|
|
||||||
],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
output = result.stdout
|
|
||||||
|
|
||||||
# Examples HandBrake may produce:
|
|
||||||
#
|
|
||||||
# + 1, Deutsch (AC3, 2.0 ch, 192 kbps)
|
|
||||||
# + 2, English (AC3, 2.0 ch, 224 kbps)
|
|
||||||
#
|
|
||||||
# Also handles German/Englisch/deu/eng where present.
|
|
||||||
|
|
||||||
tracks = []
|
|
||||||
|
|
||||||
pattern = re.compile(
|
|
||||||
r"\+\s+(\d+),\s+(.+?)\s+\((?:AC3|AAC|MP3|E-AC-3|DTS)"
|
|
||||||
)
|
|
||||||
|
|
||||||
for match in pattern.finditer(output):
|
|
||||||
track_number = int(match.group(1))
|
|
||||||
description = match.group(2).strip()
|
|
||||||
|
|
||||||
tracks.append({
|
|
||||||
"number": track_number,
|
|
||||||
"description": description,
|
|
||||||
})
|
|
||||||
if not tracks:
|
|
||||||
tracks.append({
|
|
||||||
"number": 0,
|
|
||||||
"description": "",
|
|
||||||
})
|
|
||||||
return tracks
|
|
||||||
|
|
||||||
def choose_audio_tracks(tracks):
|
def choose_audio_tracks(tracks):
|
||||||
"""
|
"""
|
||||||
Select German first, English second.
|
Select German first, English second.
|
||||||
|
|||||||
+648
-252
File diff suppressed because it is too large.
Load diff
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
from .core import safe_rename
|
|
||||||
from .core import safe_delete
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Helpers
|
|
||||||
# -----------------------------
|
|
||||||
def safe_delete(path: Path):
|
|
||||||
if path.is_dir():
|
|
||||||
shutil.rmtree(path)
|
|
||||||
else:
|
|
||||||
path.unlink()
|
|
||||||
|
|
||||||
def safe_rename(src: Path, dst: Path, dry: bool=False, confirm: bool=False, silent=False):
|
|
||||||
"""
|
|
||||||
Safely rename a file or directory without overwriting existing files.
|
|
||||||
Automatically appends _1, _2, ... if needed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
src = Path(src)
|
|
||||||
dst = Path(dst)
|
|
||||||
|
|
||||||
# nothing to do
|
|
||||||
if src.resolve() == dst.resolve():
|
|
||||||
return
|
|
||||||
|
|
||||||
directory = dst.parent
|
|
||||||
base = dst.stem
|
|
||||||
ext = dst.suffix
|
|
||||||
|
|
||||||
final_dst = dst
|
|
||||||
counter = 1
|
|
||||||
|
|
||||||
# find free name
|
|
||||||
while final_dst.exists():
|
|
||||||
final_dst = directory / f"{base}_{counter}{ext}"
|
|
||||||
counter += 1
|
|
||||||
if not silent:
|
|
||||||
print(f"{src} → {dst}")
|
|
||||||
|
|
||||||
if dry:
|
|
||||||
return
|
|
||||||
|
|
||||||
if confirm:
|
|
||||||
input("Press Enter to confirm...")
|
|
||||||
|
|
||||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
src.rename(dst)
|
|
||||||
|
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
from .core import safe_rename
|
||||||
|
from .core import safe_delete
|
||||||
|
from .core import get_audio_tracks
|
||||||
|
from .core import get_subtitle_tracks
|
||||||
Executable
+200
@@ -0,0 +1,200 @@
|
|||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Helpers
|
||||||
|
# -----------------------------
|
||||||
|
def safe_delete(path: Path):
|
||||||
|
if path.is_dir():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
else:
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
def safe_rename(src: Path, dst: Path, dry: bool=False, confirm: bool=False, silent=False):
|
||||||
|
"""
|
||||||
|
Safely rename a file or directory without overwriting existing files.
|
||||||
|
Automatically appends _1, _2, ... if needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
src = Path(src)
|
||||||
|
dst = Path(dst)
|
||||||
|
|
||||||
|
# nothing to do
|
||||||
|
if src.resolve() == dst.resolve():
|
||||||
|
return
|
||||||
|
|
||||||
|
directory = dst.parent
|
||||||
|
base = dst.stem
|
||||||
|
ext = dst.suffix
|
||||||
|
|
||||||
|
final_dst = dst
|
||||||
|
counter = 1
|
||||||
|
|
||||||
|
# find free name
|
||||||
|
while final_dst.exists():
|
||||||
|
final_dst = directory / f"{base}_{counter}{ext}"
|
||||||
|
counter += 1
|
||||||
|
ret=f"{src} → {dst}"
|
||||||
|
if not silent:
|
||||||
|
print(ret)
|
||||||
|
|
||||||
|
if dry:
|
||||||
|
return
|
||||||
|
|
||||||
|
if confirm:
|
||||||
|
input("Press Enter to confirm...")
|
||||||
|
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
src.rename(dst)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def get_audio_tracks( source: str, title: int | None = None, handbrake: bool = False) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Return audio track information from a media file or DVD title.
|
||||||
|
|
||||||
|
When ``handbrake`` is True, audio tracks are detected using
|
||||||
|
HandBrakeCLI's scan output. This is useful for DVDs and other
|
||||||
|
optical media sources where ffprobe may provide incomplete track
|
||||||
|
information.
|
||||||
|
|
||||||
|
When ``handbrake`` is False, ffprobe is used to inspect the media
|
||||||
|
file and return all audio streams.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source:
|
||||||
|
Path to the media file or DVD device/directory.
|
||||||
|
|
||||||
|
title:
|
||||||
|
DVD title number to scan when using HandBrakeCLI.
|
||||||
|
Ignored when using ffprobe.
|
||||||
|
|
||||||
|
handbrake:
|
||||||
|
If True, use HandBrakeCLI for track detection.
|
||||||
|
If False, use ffprobe.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dictionaries containing:
|
||||||
|
|
||||||
|
- number: audio track number/index
|
||||||
|
- description: language, title, or track description
|
||||||
|
|
||||||
|
Example:
|
||||||
|
get_audio_tracks("movie.mkv")
|
||||||
|
[{'number': 1, 'description': 'eng'}]
|
||||||
|
|
||||||
|
get_audio_tracks("/dev/dvd", title=1, handbrake=True)
|
||||||
|
[{'number': 1, 'description': 'Deutsch'}]
|
||||||
|
"""
|
||||||
|
tracks: list[dict] = []
|
||||||
|
|
||||||
|
if handbrake:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"HandBrakeCLI",
|
||||||
|
"-i", source,
|
||||||
|
"-t", str(title),
|
||||||
|
"--scan",
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
pattern = re.compile(
|
||||||
|
r"\+\s+(\d+),\s+(.+?)\s+\((?:AC3|AAC|MP3|E-AC-3|DTS)"
|
||||||
|
)
|
||||||
|
|
||||||
|
for match in pattern.finditer(result.stdout):
|
||||||
|
tracks.append({
|
||||||
|
"number": int(match.group(1)),
|
||||||
|
"description": match.group(2).strip(),
|
||||||
|
})
|
||||||
|
|
||||||
|
else:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"ffprobe",
|
||||||
|
"-v", "quiet",
|
||||||
|
"-print_format", "json",
|
||||||
|
"-show_streams",
|
||||||
|
source,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
info = json.loads(result.stdout)
|
||||||
|
|
||||||
|
for stream in info["streams"]:
|
||||||
|
if stream.get("codec_type") != "audio":
|
||||||
|
continue
|
||||||
|
|
||||||
|
tracks.append({
|
||||||
|
"number": stream["index"],
|
||||||
|
"description": (
|
||||||
|
stream.get("tags", {}).get("title")
|
||||||
|
or stream.get("tags", {}).get("language")
|
||||||
|
or f"Track {stream['index']}"
|
||||||
|
),
|
||||||
|
"primary": stream.get("codec_type") == "audio" and stream.get("disposition", {}).get("default") == 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if not tracks:
|
||||||
|
tracks.append({
|
||||||
|
"number": 0,
|
||||||
|
"description": "",
|
||||||
|
"primary": False
|
||||||
|
})
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
|
||||||
|
def get_subtitle_tracks(source):
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"ffprobe",
|
||||||
|
"-v", "quiet",
|
||||||
|
"-print_format", "json",
|
||||||
|
"-show_streams",
|
||||||
|
source,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
info = json.loads(result.stdout)
|
||||||
|
|
||||||
|
tracks = []
|
||||||
|
|
||||||
|
for stream in info["streams"]:
|
||||||
|
if stream.get("codec_type") != "subtitle":
|
||||||
|
continue
|
||||||
|
|
||||||
|
tracks.append({
|
||||||
|
"number": stream["index"],
|
||||||
|
"description": (
|
||||||
|
stream.get("tags", {}).get("title")
|
||||||
|
or stream.get("tags", {}).get("language")
|
||||||
|
or f"Subtitle {stream['index']}"
|
||||||
|
),
|
||||||
|
"primary": (
|
||||||
|
stream.get("disposition", {}).get("default") == 1
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
if not tracks:
|
||||||
|
tracks.append({
|
||||||
|
"number": 0,
|
||||||
|
"description": "",
|
||||||
|
"primary": False,
|
||||||
|
})
|
||||||
|
|
||||||
|
return tracks
|
||||||
@@ -3,7 +3,7 @@ requires = ["setuptools", "wheel"]
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "serverlibary"
|
name = "serverlibrary"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Serverhost Python library"
|
description = "Serverhost Python library"
|
||||||
authors = [{ name = "Lucas" }]
|
authors = [{ name = "Lucas" }]
|
||||||
Reference in new issue
Block a user