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
+894
-392
No files matched your search
@@ -7,7 +7,7 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from serverlibrary import get_audio_tracks
|
||||
|
||||
def check_command(command):
|
||||
if shutil.which(command) is None:
|
||||
@@ -60,52 +60,6 @@ def scan_dvd(dvd_name: Path):
|
||||
|
||||
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):
|
||||
"""
|
||||
Select German first, English second.
|
||||
|
||||
+688
-292
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"
|
||||
|
||||
[project]
|
||||
name = "serverlibary"
|
||||
name = "serverlibrary"
|
||||
version = "0.1.0"
|
||||
description = "Serverhost Python library"
|
||||
authors = [{ name = "Lucas" }]
|
||||
Reference in new issue
Block a user