mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2025-10-23 15:11:08 +00:00
Merge branch 'feature/pysubs2-subtitle conversion' into main
This commit is contained in:
@@ -58,6 +58,7 @@ dependencies = [
|
|||||||
"httpx>=0.28.1,<0.29",
|
"httpx>=0.28.1,<0.29",
|
||||||
"cryptography>=45.0.0",
|
"cryptography>=45.0.0",
|
||||||
"subby",
|
"subby",
|
||||||
|
"pysubs2>=1.7.0,<2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
11
uv.lock
generated
11
uv.lock
generated
@@ -1180,6 +1180,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
|
{ url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pysubs2"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/31/4a/becf78d9d3df56e6c4a9c50b83794e5436b6c5ab6dd8a3f934e94c89338c/pysubs2-1.8.0.tar.gz", hash = "sha256:3397bb58a4a15b1325ba2ae3fd4d7c214e2c0ddb9f33190d6280d783bb433b20", size = 1130048, upload-time = "2024-12-24T12:39:47.769Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/09/0fc0719162e5ad723f71d41cf336f18b6b5054d70dc0fe42ace6b4d2bdc9/pysubs2-1.8.0-py3-none-any.whl", hash = "sha256:05716f5039a9ebe32cd4d7673f923cf36204f3a3e99987f823ab83610b7035a0", size = 43516, upload-time = "2024-12-24T12:39:44.469Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pywidevine"
|
name = "pywidevine"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
@@ -1529,6 +1538,7 @@ dependencies = [
|
|||||||
{ name = "pymp4" },
|
{ name = "pymp4" },
|
||||||
{ name = "pymysql" },
|
{ name = "pymysql" },
|
||||||
{ name = "pyplayready" },
|
{ name = "pyplayready" },
|
||||||
|
{ name = "pysubs2" },
|
||||||
{ name = "pywidevine", extra = ["serve"] },
|
{ name = "pywidevine", extra = ["serve"] },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "requests", extra = ["socks"] },
|
{ name = "requests", extra = ["socks"] },
|
||||||
@@ -1578,6 +1588,7 @@ requires-dist = [
|
|||||||
{ name = "pymp4", specifier = ">=1.4.0,<2" },
|
{ name = "pymp4", specifier = ">=1.4.0,<2" },
|
||||||
{ name = "pymysql", specifier = ">=1.1.0,<2" },
|
{ name = "pymysql", specifier = ">=1.1.0,<2" },
|
||||||
{ name = "pyplayready", specifier = ">=0.6.3,<0.7" },
|
{ name = "pyplayready", specifier = ">=0.6.3,<0.7" },
|
||||||
|
{ name = "pysubs2", specifier = ">=1.7.0,<2" },
|
||||||
{ name = "pywidevine", extras = ["serve"], specifier = ">=1.8.0,<2" },
|
{ name = "pywidevine", extras = ["serve"], specifier = ">=1.8.0,<2" },
|
||||||
{ name = "pyyaml", specifier = ">=6.0.1,<7" },
|
{ name = "pyyaml", specifier = ">=6.0.1,<7" },
|
||||||
{ name = "requests", extras = ["socks"], specifier = ">=2.31.0,<3" },
|
{ name = "requests", extras = ["socks"], specifier = ">=2.31.0,<3" },
|
||||||
|
|||||||
Reference in New Issue
Block a user