mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2025-10-23 15:11:08 +00:00
Merge branch 'main' into feature/add-rest-api
This commit is contained in:
@@ -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 subby’s SDHStripper (SRT only).
|
- `subby`: Use subby's SDHStripper (SRT only).
|
||||||
- `subtitleedit`: Use SubtitleEdit’s 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:
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
2
uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user