Merge branch 'main' into feature/add-rest-api

This commit is contained in:
Andy
2025-10-01 04:53:44 +00:00
9 changed files with 91 additions and 15 deletions

View File

@@ -659,11 +659,12 @@ Control subtitle conversion and SDH (hearing-impaired) stripping behavior.
- `subby`: Always use subby with CommonIssuesFixer. - `subby`: Always use subby with CommonIssuesFixer.
- `subtitleedit`: Prefer SubtitleEdit when available; otherwise fallback to standard conversion. - `subtitleedit`: Prefer SubtitleEdit when available; otherwise fallback to standard conversion.
- `pycaption`: Use only the pycaption library (no SubtitleEdit, no subby). - `pycaption`: Use only the pycaption library (no SubtitleEdit, no subby).
- `pysubs2`: Use pysubs2 library (supports SRT, SSA, ASS, WebVTT, TTML, SAMI, MicroDVD, MPL2, TMP formats).
- `sdh_method`: How to strip SDH cues. Default: `auto`. - `sdh_method`: How to strip SDH cues. Default: `auto`.
- `auto`: Try subby for SRT first, then SubtitleEdit, then filter-subs. - `auto`: Try subby for SRT first, then SubtitleEdit, then filter-subs.
- `subby`: Use subbys SDHStripper (SRT only). - `subby`: Use subby's SDHStripper (SRT only).
- `subtitleedit`: Use SubtitleEdits RemoveTextForHI when available. - `subtitleedit`: Use SubtitleEdit's RemoveTextForHI when available.
- `filter-subs`: Use the subtitle-filter library. - `filter-subs`: Use the subtitle-filter library.
Example: Example:

View File

@@ -54,11 +54,12 @@ dependencies = [
"urllib3>=2.2.1,<3", "urllib3>=2.2.1,<3",
"chardet>=5.2.0,<6", "chardet>=5.2.0,<6",
"curl-cffi>=0.7.0b4,<0.8", "curl-cffi>=0.7.0b4,<0.8",
"pyplayready>=0.6.0,<0.7", "pyplayready>=0.6.3,<0.7",
"httpx>=0.28.1,<0.29", "httpx>=0.28.1,<0.29",
"cryptography>=45.0.0", "cryptography>=45.0.0",
"subby", "subby",
"aiohttp-swagger3>=0.9.0,<1", "aiohttp-swagger3>=0.9.0,<1",
"pysubs2>=1.7.0,<2",
] ]
[project.urls] [project.urls]
@@ -114,4 +115,4 @@ no_implicit_optional = true
[tool.uv.sources] [tool.uv.sources]
unshackle = { workspace = true } unshackle = { workspace = true }
subby = { git = "https://github.com/vevv/subby.git" } subby = { git = "https://github.com/vevv/subby.git", rev = "5a925c367ffb3f5e53fd114ae222d3be1fdff35d" }

View File

@@ -213,7 +213,7 @@ class dl:
@click.option( @click.option(
"--sub-format", "--sub-format",
type=SubtitleCodecChoice(Subtitle.Codec), type=SubtitleCodecChoice(Subtitle.Codec),
default="srt", default=None,
help="Set Output Subtitle Format, only converting if necessary.", help="Set Output Subtitle Format, only converting if necessary.",
) )
@click.option("-V", "--video-only", is_flag=True, default=False, help="Only download video tracks.") @click.option("-V", "--video-only", is_flag=True, default=False, help="Only download video tracks.")
@@ -1701,10 +1701,14 @@ class dl:
# All DecryptLabs CDMs use DecryptLabsRemoteCDM # All DecryptLabs CDMs use DecryptLabsRemoteCDM
return DecryptLabsRemoteCDM(service_name=service, vaults=self.vaults, **cdm_api) return DecryptLabsRemoteCDM(service_name=service, vaults=self.vaults, **cdm_api)
else: else:
del cdm_api["name"] return RemoteCdm(
if "type" in cdm_api: device_type=cdm_api['Device Type'],
del cdm_api["type"] system_id=cdm_api['System ID'],
return RemoteCdm(**cdm_api) security_level=cdm_api['Security Level'],
host=cdm_api['Host'],
secret=cdm_api['Secret'],
device_name=cdm_api['Device Name'],
)
prd_path = config.directories.prds / f"{cdm_name}.prd" prd_path = config.directories.prds / f"{cdm_name}.prd"
if not prd_path.is_file(): if not prd_path.is_file():

View File

@@ -5,10 +5,10 @@ from typing import Optional
import click import click
import requests import requests
from Crypto.Random import get_random_bytes from Crypto.Random import get_random_bytes
from pyplayready import InvalidCertificateChain, OutdatedDevice
from pyplayready.cdm import Cdm from pyplayready.cdm import Cdm
from pyplayready.crypto.ecc_key import ECCKey from pyplayready.crypto.ecc_key import ECCKey
from pyplayready.device import Device from pyplayready.device import Device
from pyplayready.misc.exceptions import InvalidCertificateChain, OutdatedDevice
from pyplayready.system.bcert import Certificate, CertificateChain from pyplayready.system.bcert import Certificate, CertificateChain
from pyplayready.system.pssh import PSSH from pyplayready.system.pssh import PSSH

View File

@@ -1,3 +1,9 @@
import warnings
# Suppress SyntaxWarning from unmaintained tinycss package (dependency of subby)
# Must be set before any imports that might trigger tinycss loading
warnings.filterwarnings("ignore", category=SyntaxWarning, module="tinycss")
import atexit import atexit
import logging import logging
from pathlib import Path from pathlib import Path

View File

@@ -253,8 +253,8 @@ class DASH:
): ):
if not session: if not session:
session = Session() session = Session()
elif not isinstance(session, Session): elif not isinstance(session, (Session, CurlSession)):
raise TypeError(f"Expected session to be a {Session}, not {session!r}") raise TypeError(f"Expected session to be a {Session} or {CurlSession}, not {session!r}")
if proxy: if proxy:
session.proxies.update({"all": proxy}) session.proxies.update({"all": proxy})

View File

@@ -10,6 +10,7 @@ from pathlib import Path
from typing import Any, Callable, Iterable, Optional, Union from typing import Any, Callable, Iterable, Optional, Union
import pycaption import pycaption
import pysubs2
import requests import requests
from construct import Container from construct import Container
from pycaption import Caption, CaptionList, CaptionNode, WebVTTReader from pycaption import Caption, CaptionList, CaptionNode, WebVTTReader
@@ -33,6 +34,9 @@ class Subtitle(Track):
TimedTextMarkupLang = "TTML" # https://wikipedia.org/wiki/Timed_Text_Markup_Language TimedTextMarkupLang = "TTML" # https://wikipedia.org/wiki/Timed_Text_Markup_Language
WebVTT = "VTT" # https://wikipedia.org/wiki/WebVTT WebVTT = "VTT" # https://wikipedia.org/wiki/WebVTT
SAMI = "SMI" # https://wikipedia.org/wiki/SAMI SAMI = "SMI" # https://wikipedia.org/wiki/SAMI
MicroDVD = "SUB" # https://wikipedia.org/wiki/MicroDVD
MPL2 = "MPL2" # MPL2 subtitle format
TMP = "TMP" # TMP subtitle format
# MPEG-DASH box-encapsulated subtitle formats # MPEG-DASH box-encapsulated subtitle formats
fTTML = "STPP" # https://www.w3.org/TR/2018/REC-ttml-imsc1.0.1-20180424 fTTML = "STPP" # https://www.w3.org/TR/2018/REC-ttml-imsc1.0.1-20180424
fVTT = "WVTT" # https://www.w3.org/TR/webvtt1 fVTT = "WVTT" # https://www.w3.org/TR/webvtt1
@@ -56,6 +60,12 @@ class Subtitle(Track):
return Subtitle.Codec.WebVTT return Subtitle.Codec.WebVTT
elif mime in ("smi", "sami"): elif mime in ("smi", "sami"):
return Subtitle.Codec.SAMI return Subtitle.Codec.SAMI
elif mime in ("sub", "microdvd"):
return Subtitle.Codec.MicroDVD
elif mime == "mpl2":
return Subtitle.Codec.MPL2
elif mime == "tmp":
return Subtitle.Codec.TMP
elif mime == "stpp": elif mime == "stpp":
return Subtitle.Codec.fTTML return Subtitle.Codec.fTTML
elif mime == "wvtt": elif mime == "wvtt":
@@ -391,6 +401,57 @@ class Subtitle(Track):
# Fall back to existing conversion method on any error # Fall back to existing conversion method on any error
return self._convert_standard(codec) return self._convert_standard(codec)
def convert_with_pysubs2(self, codec: Subtitle.Codec) -> Path:
"""
Convert subtitle using pysubs2 library for broad format support.
pysubs2 is a pure-Python library supporting SubRip (SRT), SubStation Alpha
(SSA/ASS), WebVTT, TTML, SAMI, MicroDVD, MPL2, and TMP formats.
"""
if not self.path or not self.path.exists():
raise ValueError("You must download the subtitle track first.")
if self.codec == codec:
return self.path
output_path = self.path.with_suffix(f".{codec.value.lower()}")
original_path = self.path
codec_to_pysubs2_format = {
Subtitle.Codec.SubRip: "srt",
Subtitle.Codec.SubStationAlpha: "ssa",
Subtitle.Codec.SubStationAlphav4: "ass",
Subtitle.Codec.WebVTT: "vtt",
Subtitle.Codec.TimedTextMarkupLang: "ttml",
Subtitle.Codec.SAMI: "sami",
Subtitle.Codec.MicroDVD: "microdvd",
Subtitle.Codec.MPL2: "mpl2",
Subtitle.Codec.TMP: "tmp",
}
pysubs2_output_format = codec_to_pysubs2_format.get(codec)
if pysubs2_output_format is None:
return self._convert_standard(codec)
try:
subs = pysubs2.load(str(self.path), encoding="utf-8")
subs.save(str(output_path), format_=pysubs2_output_format, encoding="utf-8")
if original_path.exists() and original_path != output_path:
original_path.unlink()
self.path = output_path
self.codec = codec
if callable(self.OnConverted):
self.OnConverted(codec)
return output_path
except Exception:
return self._convert_standard(codec)
def convert(self, codec: Subtitle.Codec) -> Path: def convert(self, codec: Subtitle.Codec) -> Path:
""" """
Convert this Subtitle to another Format. Convert this Subtitle to another Format.
@@ -400,6 +461,7 @@ class Subtitle(Track):
- 'subby': Always uses subby with CommonIssuesFixer - 'subby': Always uses subby with CommonIssuesFixer
- 'subtitleedit': Uses SubtitleEdit when available, falls back to pycaption - 'subtitleedit': Uses SubtitleEdit when available, falls back to pycaption
- 'pycaption': Uses only pycaption library - 'pycaption': Uses only pycaption library
- 'pysubs2': Uses pysubs2 library
""" """
# Check configuration for conversion method # Check configuration for conversion method
conversion_method = config.subtitle.get("conversion_method", "auto") conversion_method = config.subtitle.get("conversion_method", "auto")
@@ -407,11 +469,12 @@ class Subtitle(Track):
if conversion_method == "subby": if conversion_method == "subby":
return self.convert_with_subby(codec) return self.convert_with_subby(codec)
elif conversion_method == "subtitleedit": elif conversion_method == "subtitleedit":
return self._convert_standard(codec) # SubtitleEdit is used in standard conversion return self._convert_standard(codec)
elif conversion_method == "pycaption": elif conversion_method == "pycaption":
return self._convert_pycaption_only(codec) return self._convert_pycaption_only(codec)
elif conversion_method == "pysubs2":
return self.convert_with_pysubs2(codec)
elif conversion_method == "auto": elif conversion_method == "auto":
# Use subby for formats it handles better
if self.codec in (Subtitle.Codec.WebVTT, Subtitle.Codec.SAMI): if self.codec in (Subtitle.Codec.WebVTT, Subtitle.Codec.SAMI):
return self.convert_with_subby(codec) return self.convert_with_subby(codec)
else: else:

View File

@@ -253,6 +253,7 @@ tmdb_api_key: ""
# - subby: Always use subby with advanced processing # - subby: Always use subby with advanced processing
# - pycaption: Use only pycaption library (no SubtitleEdit, no subby) # - pycaption: Use only pycaption library (no SubtitleEdit, no subby)
# - subtitleedit: Prefer SubtitleEdit when available, fall back to pycaption # - subtitleedit: Prefer SubtitleEdit when available, fall back to pycaption
# - pysubs2: Use pysubs2 library (supports SRT/SSA/ASS/WebVTT/TTML/SAMI/MicroDVD/MPL2/TMP)
subtitle: subtitle:
conversion_method: auto conversion_method: auto
sdh_method: auto sdh_method: auto

2
uv.lock generated
View File

@@ -1610,7 +1610,7 @@ requires-dist = [
{ name = "httpx", specifier = ">=0.28.1,<0.29" }, { name = "httpx", specifier = ">=0.28.1,<0.29" },
{ name = "jsonpickle", specifier = ">=3.0.4,<4" }, { name = "jsonpickle", specifier = ">=3.0.4,<4" },
{ name = "langcodes", specifier = ">=3.4.0,<4" }, { name = "langcodes", specifier = ">=3.4.0,<4" },
{ name = "lxml", specifier = ">=5.2.1,<6" }, { name = "lxml", specifier = ">=5.2.1,<7" },
{ name = "pproxy", specifier = ">=2.7.9,<3" }, { name = "pproxy", specifier = ">=2.7.9,<3" },
{ name = "protobuf", specifier = ">=4.25.3,<5" }, { name = "protobuf", specifier = ">=4.25.3,<5" },
{ name = "pycaption", specifier = ">=2.2.6,<3" }, { name = "pycaption", specifier = ">=2.2.6,<3" },