forked from kenzuya/unshackle
feat(fonts): add Linux font support for ASS/SSA subtitles
Implements cross-platform font discovery and intelligent fallback system for ASS/SSA subtitle rendering on Linux/macOS systems. Windows support has not been tested
This commit is contained in:
@@ -31,6 +31,7 @@ dependencies = [
|
||||
"click>=8.1.8,<9",
|
||||
"construct>=2.8.8,<3",
|
||||
"crccheck>=1.3.0,<2",
|
||||
"fonttools>=4.0.0,<5",
|
||||
"jsonpickle>=3.0.4,<4",
|
||||
"langcodes>=3.4.0,<4",
|
||||
"lxml>=5.2.1,<7",
|
||||
@@ -60,6 +61,7 @@ dependencies = [
|
||||
"subby",
|
||||
"aiohttp-swagger3>=0.9.0,<1",
|
||||
"pysubs2>=1.7.0,<2",
|
||||
"PyExecJS>=1.5.1,<2",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -57,8 +57,8 @@ from unshackle.core.titles.episode import Episode
|
||||
from unshackle.core.tracks import Audio, Subtitle, Tracks, Video
|
||||
from unshackle.core.tracks.attachment import Attachment
|
||||
from unshackle.core.tracks.hybrid import Hybrid
|
||||
from unshackle.core.utilities import (get_debug_logger, get_system_fonts, init_debug_logger, is_close_match,
|
||||
time_elapsed_since)
|
||||
from unshackle.core.utilities import (find_font_with_fallbacks, get_debug_logger, get_system_fonts, init_debug_logger,
|
||||
is_close_match, suggest_font_packages, time_elapsed_since)
|
||||
from unshackle.core.utils import tags
|
||||
from unshackle.core.utils.click_types import (LANGUAGE_RANGE, QUALITY_LIST, SEASON_RANGE, ContextData, MultipleChoice,
|
||||
SubtitleCodecChoice, VideoCodecChoice)
|
||||
@@ -69,7 +69,7 @@ from unshackle.core.vaults import Vaults
|
||||
|
||||
class dl:
|
||||
@staticmethod
|
||||
def _truncate_pssh_for_display(pssh_string: str, drm_type: str) -> str:
|
||||
def truncate_pssh_for_display(pssh_string: str, drm_type: str) -> str:
|
||||
"""Truncate PSSH string for display when not in debug mode."""
|
||||
if logging.root.level == logging.DEBUG or not pssh_string:
|
||||
return pssh_string
|
||||
@@ -80,6 +80,115 @@ class dl:
|
||||
|
||||
return pssh_string[: max_width - 3] + "..."
|
||||
|
||||
def find_custom_font(self, font_name: str) -> Optional[Path]:
|
||||
"""
|
||||
Find font in custom fonts directory.
|
||||
|
||||
Args:
|
||||
font_name: Font family name to find
|
||||
|
||||
Returns:
|
||||
Path to font file, or None if not found
|
||||
"""
|
||||
family_dir = Path(config.directories.fonts, font_name)
|
||||
if family_dir.exists():
|
||||
fonts = list(family_dir.glob("*.*tf"))
|
||||
return fonts[0] if fonts else None
|
||||
return None
|
||||
|
||||
def prepare_temp_font(
|
||||
self,
|
||||
font_name: str,
|
||||
matched_font: Path,
|
||||
system_fonts: dict[str, Path],
|
||||
temp_font_files: list[Path]
|
||||
) -> Path:
|
||||
"""
|
||||
Copy system font to temp and log if using fallback.
|
||||
|
||||
Args:
|
||||
font_name: Requested font name
|
||||
matched_font: Path to matched system font
|
||||
system_fonts: Dictionary of available system fonts
|
||||
temp_font_files: List to track temp files for cleanup
|
||||
|
||||
Returns:
|
||||
Path to temp font file
|
||||
"""
|
||||
# Find the matched name for logging
|
||||
matched_name = next(
|
||||
(name for name, path in system_fonts.items() if path == matched_font),
|
||||
None
|
||||
)
|
||||
|
||||
if matched_name and matched_name.lower() != font_name.lower():
|
||||
self.log.info(f"Using '{matched_name}' as fallback for '{font_name}'")
|
||||
|
||||
# Create unique temp file path
|
||||
safe_name = font_name.replace(" ", "_").replace("/", "_")
|
||||
temp_path = config.directories.temp / f"font_{safe_name}{matched_font.suffix}"
|
||||
|
||||
# Copy if not already exists
|
||||
if not temp_path.exists():
|
||||
shutil.copy2(matched_font, temp_path)
|
||||
temp_font_files.append(temp_path)
|
||||
|
||||
return temp_path
|
||||
|
||||
def _attach_subtitle_fonts(
|
||||
self,
|
||||
font_names: list[str],
|
||||
title: Title_T,
|
||||
temp_font_files: list[Path]
|
||||
) -> tuple[int, list[str]]:
|
||||
"""
|
||||
Attach fonts for subtitle rendering.
|
||||
|
||||
Args:
|
||||
font_names: List of font names requested by subtitles
|
||||
title: Title object to attach fonts to
|
||||
temp_font_files: List to track temp files for cleanup
|
||||
|
||||
Returns:
|
||||
Tuple of (fonts_attached_count, missing_fonts_list)
|
||||
"""
|
||||
system_fonts = get_system_fonts()
|
||||
self.log.info(f"Discovered {len(system_fonts)} system font families")
|
||||
|
||||
font_count = 0
|
||||
missing_fonts = []
|
||||
|
||||
for font_name in set(font_names):
|
||||
# Try custom fonts first
|
||||
if custom_font := self.find_custom_font(font_name):
|
||||
title.tracks.add(Attachment(path=custom_font, name=f"{font_name} ({custom_font.stem})"))
|
||||
font_count += 1
|
||||
continue
|
||||
|
||||
# Try system fonts with fallback
|
||||
if system_font := find_font_with_fallbacks(font_name, system_fonts):
|
||||
temp_path = self.prepare_temp_font(font_name, system_font, system_fonts, temp_font_files)
|
||||
title.tracks.add(Attachment(path=temp_path, name=f"{font_name} ({system_font.stem})"))
|
||||
font_count += 1
|
||||
else:
|
||||
self.log.warning(f"Subtitle uses font '{font_name}' but it could not be found")
|
||||
missing_fonts.append(font_name)
|
||||
|
||||
return font_count, missing_fonts
|
||||
|
||||
def _suggest_missing_fonts(self, missing_fonts: list[str]) -> None:
|
||||
"""
|
||||
Show package installation suggestions for missing fonts.
|
||||
|
||||
Args:
|
||||
missing_fonts: List of font names that couldn't be found
|
||||
"""
|
||||
if suggestions := suggest_font_packages(missing_fonts):
|
||||
self.log.info("Install font packages to improve subtitle rendering:")
|
||||
for package_cmd, fonts in suggestions.items():
|
||||
self.log.info(f" $ sudo apt install {package_cmd}")
|
||||
self.log.info(f" → Provides: {', '.join(fonts)}")
|
||||
|
||||
@click.command(
|
||||
short_help="Download, Decrypt, and Mux tracks for titles from a Service.",
|
||||
cls=Services,
|
||||
@@ -794,6 +903,7 @@ class dl:
|
||||
continue
|
||||
|
||||
console.print(Padding(Rule(f"[rule.text]{title}"), (1, 2)))
|
||||
temp_font_files = []
|
||||
|
||||
if isinstance(title, Episode) and not self.tmdb_searched:
|
||||
kind = "tv"
|
||||
@@ -1435,26 +1545,16 @@ class dl:
|
||||
if line.startswith("Style: "):
|
||||
font_names.append(line.removesuffix("Style: ").split(",")[1])
|
||||
|
||||
font_count = 0
|
||||
system_fonts = get_system_fonts()
|
||||
for font_name in set(font_names):
|
||||
family_dir = Path(config.directories.fonts, font_name)
|
||||
fonts_from_system = [file for name, file in system_fonts.items() if name.startswith(font_name)]
|
||||
if family_dir.exists():
|
||||
fonts = family_dir.glob("*.*tf")
|
||||
for font in fonts:
|
||||
title.tracks.add(Attachment(path=font, name=f"{font_name} ({font.stem})"))
|
||||
font_count += 1
|
||||
elif fonts_from_system:
|
||||
for font in fonts_from_system:
|
||||
title.tracks.add(Attachment(path=font, name=f"{font_name} ({font.stem})"))
|
||||
font_count += 1
|
||||
else:
|
||||
self.log.warning(f"Subtitle uses font [text2]{font_name}[/] but it could not be found...")
|
||||
font_count, missing_fonts = self._attach_subtitle_fonts(
|
||||
font_names, title, temp_font_files
|
||||
)
|
||||
|
||||
if font_count:
|
||||
self.log.info(f"Attached {font_count} fonts for the Subtitles")
|
||||
|
||||
if missing_fonts and sys.platform != "win32":
|
||||
self._suggest_missing_fonts(missing_fonts)
|
||||
|
||||
# Handle DRM decryption BEFORE repacking (must decrypt first!)
|
||||
service_name = service.__class__.__name__.upper()
|
||||
decryption_method = config.decryption_map.get(service_name, config.decryption)
|
||||
@@ -1608,8 +1708,17 @@ class dl:
|
||||
video_track.delete()
|
||||
for track in title.tracks:
|
||||
track.delete()
|
||||
|
||||
# Clear temp font attachment paths and delete other attachments
|
||||
for attachment in title.tracks.attachments:
|
||||
attachment.delete()
|
||||
if attachment.path and attachment.path in temp_font_files:
|
||||
attachment.path = None
|
||||
else:
|
||||
attachment.delete()
|
||||
|
||||
# Clean up temp fonts
|
||||
for temp_path in temp_font_files:
|
||||
temp_path.unlink(missing_ok=True)
|
||||
|
||||
else:
|
||||
# dont mux
|
||||
@@ -1752,7 +1861,7 @@ class dl:
|
||||
)
|
||||
|
||||
with self.DRM_TABLE_LOCK:
|
||||
pssh_display = self._truncate_pssh_for_display(drm.pssh.dumps(), "Widevine")
|
||||
pssh_display = self.truncate_pssh_for_display(drm.pssh.dumps(), "Widevine")
|
||||
cek_tree = Tree(Text.assemble(("Widevine", "cyan"), (f"({pssh_display})", "text"), overflow="fold"))
|
||||
pre_existing_tree = next(
|
||||
(x for x in table.columns[0].cells if isinstance(x, Tree) and x.label == cek_tree.label), None
|
||||
@@ -1921,7 +2030,7 @@ class dl:
|
||||
)
|
||||
|
||||
with self.DRM_TABLE_LOCK:
|
||||
pssh_display = self._truncate_pssh_for_display(drm.pssh_b64 or "", "PlayReady")
|
||||
pssh_display = self.truncate_pssh_for_display(drm.pssh_b64 or "", "PlayReady")
|
||||
cek_tree = Tree(
|
||||
Text.assemble(
|
||||
("PlayReady", "cyan"),
|
||||
|
||||
@@ -21,6 +21,7 @@ from uuid import uuid4
|
||||
import chardet
|
||||
import requests
|
||||
from construct import ValidationError
|
||||
from fontTools import ttLib
|
||||
from langcodes import Language, closest_match
|
||||
from pymp4.parser import Box
|
||||
from unidecode import unidecode
|
||||
@@ -29,6 +30,30 @@ from unshackle.core.cacher import Cacher
|
||||
from unshackle.core.config import config
|
||||
from unshackle.core.constants import LANGUAGE_EXACT_DISTANCE, LANGUAGE_MAX_DISTANCE
|
||||
|
||||
"""
|
||||
Utility functions for the unshackle media archival tool.
|
||||
|
||||
This module provides various utility functions including:
|
||||
- Font discovery and fallback system for subtitle rendering
|
||||
- Cross-platform system font scanning with Windows → Linux font family mapping
|
||||
- Log file management and rotation
|
||||
- IP geolocation with caching and provider rotation
|
||||
- Language matching utilities
|
||||
- MP4/ISOBMFF box parsing
|
||||
- File sanitization and path handling
|
||||
- Structured JSON debug logging
|
||||
|
||||
Font System:
|
||||
The font subsystem enables cross-platform font discovery for ASS/SSA subtitles.
|
||||
On Linux, it scans standard font directories and maps Windows font names (Arial,
|
||||
Times New Roman) to their Linux equivalents (Liberation Sans, Liberation Serif).
|
||||
|
||||
Main Font Functions:
|
||||
- get_system_fonts(): Discover installed fonts across platforms
|
||||
- find_font_with_fallbacks(): Match fonts with intelligent fallback strategies
|
||||
- suggest_font_packages(): Recommend packages to install for missing fonts
|
||||
"""
|
||||
|
||||
|
||||
def rotate_log_file(log_path: Path, keep: int = 20) -> Path:
|
||||
"""
|
||||
@@ -428,21 +453,254 @@ def get_extension(value: Union[str, Path, ParseResult]) -> Optional[str]:
|
||||
return ext
|
||||
|
||||
|
||||
def get_system_fonts() -> dict[str, Path]:
|
||||
if sys.platform == "win32":
|
||||
import winreg
|
||||
def extract_font_family(font_path: Path) -> Optional[str]:
|
||||
"""
|
||||
Extract font family name from TTF/OTF file using fontTools.
|
||||
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as reg:
|
||||
key = winreg.OpenKey(reg, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", 0, winreg.KEY_READ)
|
||||
total_fonts = winreg.QueryInfoKey(key)[1]
|
||||
return {
|
||||
name.replace(" (TrueType)", ""): Path(r"C:\Windows\Fonts", filename)
|
||||
for n in range(0, total_fonts)
|
||||
for name, filename, _ in [winreg.EnumValue(key, n)]
|
||||
}
|
||||
else:
|
||||
# TODO: Get System Fonts for Linux and mac OS
|
||||
return {}
|
||||
Args:
|
||||
font_path: Path to the font file
|
||||
|
||||
Returns:
|
||||
Font family name if successfully extracted, None otherwise
|
||||
"""
|
||||
try:
|
||||
font = ttLib.TTFont(font_path)
|
||||
name_table = font["name"]
|
||||
|
||||
# Try to get family name (nameID 1) for Windows platform (platformID 3)
|
||||
# This matches the naming convention used in Windows registry
|
||||
for record in name_table.names:
|
||||
if record.nameID == 1 and record.platformID == 3:
|
||||
return record.toUnicode()
|
||||
|
||||
# Fallback to other platforms if Windows name not found
|
||||
for record in name_table.names:
|
||||
if record.nameID == 1:
|
||||
return record.toUnicode()
|
||||
|
||||
except Exception:
|
||||
# Silently ignore font parsing errors (corrupted fonts, etc.)
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_windows_fonts() -> dict[str, Path]:
|
||||
"""
|
||||
Get fonts from Windows registry.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping font family names to their file paths
|
||||
"""
|
||||
import winreg
|
||||
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as reg:
|
||||
key = winreg.OpenKey(reg, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", 0, winreg.KEY_READ)
|
||||
total_fonts = winreg.QueryInfoKey(key)[1]
|
||||
return {
|
||||
name.replace(" (TrueType)", ""): Path(r"C:\Windows\Fonts", filename)
|
||||
for n in range(0, total_fonts)
|
||||
for name, filename, _ in [winreg.EnumValue(key, n)]
|
||||
}
|
||||
|
||||
|
||||
def _scan_font_directory(font_dir: Path, fonts: dict[str, Path], log: logging.Logger) -> None:
|
||||
"""
|
||||
Scan a single directory for fonts.
|
||||
|
||||
Args:
|
||||
font_dir: Directory to scan
|
||||
fonts: Dictionary to populate with found fonts
|
||||
log: Logger instance for error reporting
|
||||
"""
|
||||
font_files = list(font_dir.rglob("*.ttf")) + list(font_dir.rglob("*.otf"))
|
||||
|
||||
for font_file in font_files:
|
||||
try:
|
||||
if family_name := extract_font_family(font_file):
|
||||
if family_name not in fonts:
|
||||
fonts[family_name] = font_file
|
||||
except Exception as e:
|
||||
log.debug(f"Failed to process {font_file}: {e}")
|
||||
|
||||
|
||||
def _get_unix_fonts() -> dict[str, Path]:
|
||||
"""
|
||||
Get fonts from Linux/macOS standard directories.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping font family names to their file paths
|
||||
"""
|
||||
log = logging.getLogger("get_system_fonts")
|
||||
fonts = {}
|
||||
|
||||
font_dirs = [
|
||||
Path("/usr/share/fonts"),
|
||||
Path("/usr/local/share/fonts"),
|
||||
Path.home() / ".fonts",
|
||||
Path.home() / ".local/share/fonts",
|
||||
]
|
||||
|
||||
for font_dir in font_dirs:
|
||||
if not font_dir.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
_scan_font_directory(font_dir, fonts, log)
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to scan {font_dir}: {e}")
|
||||
|
||||
log.debug(f"Discovered {len(fonts)} system font families")
|
||||
return fonts
|
||||
|
||||
|
||||
def get_system_fonts() -> dict[str, Path]:
|
||||
"""
|
||||
Get system fonts as a mapping of font family names to font file paths.
|
||||
|
||||
On Windows: Uses registry to get font display names
|
||||
On Linux/macOS: Scans standard font directories and extracts family names using fontTools
|
||||
|
||||
Returns:
|
||||
Dictionary mapping font family names to their file paths
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
return _get_windows_fonts()
|
||||
return _get_unix_fonts()
|
||||
|
||||
|
||||
# Common Windows font names mapped to their Linux equivalents
|
||||
# Ordered by preference (first match is used)
|
||||
FONT_ALIASES = {
|
||||
"Arial": ["Liberation Sans", "DejaVu Sans", "Nimbus Sans", "FreeSans"],
|
||||
"Arial Black": ["Liberation Sans", "DejaVu Sans", "Nimbus Sans"],
|
||||
"Arial Bold": ["Liberation Sans", "DejaVu Sans"],
|
||||
"Arial Unicode MS": ["DejaVu Sans", "Noto Sans", "FreeSans"],
|
||||
"Times New Roman": ["Liberation Serif", "DejaVu Serif", "Nimbus Roman", "FreeSerif"],
|
||||
"Courier New": ["Liberation Mono", "DejaVu Sans Mono", "Nimbus Mono PS", "FreeMono"],
|
||||
"Comic Sans MS": ["Comic Neue", "Comic Relief", "DejaVu Sans"],
|
||||
"Georgia": ["Gelasio", "DejaVu Serif", "Liberation Serif"],
|
||||
"Impact": ["Impact", "Anton", "Liberation Sans"],
|
||||
"Trebuchet MS": ["Ubuntu", "DejaVu Sans", "Liberation Sans"],
|
||||
"Verdana": ["DejaVu Sans", "Bitstream Vera Sans", "Liberation Sans"],
|
||||
"Tahoma": ["DejaVu Sans", "Liberation Sans"],
|
||||
"Adobe Arabic": ["Noto Sans Arabic", "DejaVu Sans"],
|
||||
"Noto Sans Thai": ["Noto Sans Thai", "Noto Sans"],
|
||||
}
|
||||
|
||||
|
||||
def find_case_insensitive(font_name: str, fonts: dict[str, Path]) -> Optional[Path]:
|
||||
"""
|
||||
Find font by case-insensitive name match.
|
||||
|
||||
Args:
|
||||
font_name: Font family name to find
|
||||
fonts: Dictionary of available fonts
|
||||
|
||||
Returns:
|
||||
Path to matched font, or None if not found
|
||||
"""
|
||||
font_lower = font_name.lower()
|
||||
for name, path in fonts.items():
|
||||
if name.lower() == font_lower:
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def find_font_with_fallbacks(font_name: str, system_fonts: dict[str, Path]) -> Optional[Path]:
|
||||
"""
|
||||
Find a font by name with intelligent fallback matching.
|
||||
|
||||
Tries multiple strategies in order:
|
||||
1. Exact match (case-sensitive)
|
||||
2. Case-insensitive match
|
||||
3. Alias lookup (Windows → Linux font equivalents)
|
||||
4. Partial/prefix match
|
||||
|
||||
Args:
|
||||
font_name: The requested font family name (e.g., "Arial", "Times New Roman")
|
||||
system_fonts: Dictionary of available fonts (family name → path)
|
||||
|
||||
Returns:
|
||||
Path to the matched font file, or None if no match found
|
||||
"""
|
||||
if not system_fonts:
|
||||
return None
|
||||
|
||||
# Strategy 1: Exact match (case-sensitive)
|
||||
if font_name in system_fonts:
|
||||
return system_fonts[font_name]
|
||||
|
||||
# Strategy 2: Case-insensitive match
|
||||
if result := find_case_insensitive(font_name, system_fonts):
|
||||
return result
|
||||
|
||||
# Strategy 3: Alias lookup
|
||||
if font_name in FONT_ALIASES:
|
||||
for alias in FONT_ALIASES[font_name]:
|
||||
# Try exact match for alias
|
||||
if alias in system_fonts:
|
||||
return system_fonts[alias]
|
||||
# Try case-insensitive match for alias
|
||||
if result := find_case_insensitive(alias, system_fonts):
|
||||
return result
|
||||
|
||||
# Strategy 4: Partial/prefix match as last resort
|
||||
font_name_lower = font_name.lower()
|
||||
for name, path in system_fonts.items():
|
||||
if name.lower().startswith(font_name_lower):
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Mapping of font families to system packages that provide them
|
||||
FONT_PACKAGES = {
|
||||
"liberation": {
|
||||
"debian": "fonts-liberation fonts-liberation2",
|
||||
"fonts": ["Liberation Sans", "Liberation Serif", "Liberation Mono"],
|
||||
},
|
||||
"dejavu": {
|
||||
"debian": "fonts-dejavu fonts-dejavu-core fonts-dejavu-extra",
|
||||
"fonts": ["DejaVu Sans", "DejaVu Serif", "DejaVu Sans Mono"],
|
||||
},
|
||||
"noto": {
|
||||
"debian": "fonts-noto fonts-noto-core",
|
||||
"fonts": ["Noto Sans", "Noto Serif", "Noto Sans Mono", "Noto Sans Arabic", "Noto Sans Thai"],
|
||||
},
|
||||
"ubuntu": {
|
||||
"debian": "fonts-ubuntu",
|
||||
"fonts": ["Ubuntu", "Ubuntu Mono"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def suggest_font_packages(missing_fonts: list[str]) -> dict[str, list[str]]:
|
||||
"""
|
||||
Suggest system packages to install for missing fonts.
|
||||
|
||||
Args:
|
||||
missing_fonts: List of font family names that couldn't be found
|
||||
|
||||
Returns:
|
||||
Dictionary mapping package names to lists of fonts they would provide
|
||||
"""
|
||||
suggestions = {}
|
||||
|
||||
# Check which fonts from aliases would help
|
||||
needed_aliases = set()
|
||||
for font in missing_fonts:
|
||||
if font in FONT_ALIASES:
|
||||
needed_aliases.update(FONT_ALIASES[font])
|
||||
|
||||
# Map needed aliases to packages
|
||||
for package_name, package_info in FONT_PACKAGES.items():
|
||||
provided_fonts = package_info["fonts"]
|
||||
matching_fonts = [f for f in provided_fonts if f in needed_aliases]
|
||||
if matching_fonts:
|
||||
suggestions[package_info["debian"]] = matching_fonts
|
||||
|
||||
return suggestions
|
||||
|
||||
|
||||
class FPS(ast.NodeVisitor):
|
||||
|
||||
37
uv.lock
generated
37
uv.lock
generated
@@ -502,6 +502,39 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.60.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/70/03e9d89a053caff6ae46053890eba8e4a5665a7c5638279ed4492e6d4b8b/fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28", size = 2810747, upload-time = "2025-09-29T21:10:59.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/41/449ad5aff9670ab0df0f61ee593906b67a36d7e0b4d0cd7fa41ac0325bf5/fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15", size = 2346909, upload-time = "2025-09-29T21:11:02.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/18/e5970aa96c8fad1cb19a9479cc3b7602c0c98d250fcdc06a5da994309c50/fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c", size = 4864572, upload-time = "2025-09-29T21:11:05.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/20/9b2b4051b6ec6689480787d506b5003f72648f50972a92d04527a456192c/fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea", size = 4794635, upload-time = "2025-09-29T21:11:08.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/52/c791f57347c1be98f8345e3dca4ac483eb97666dd7c47f3059aeffab8b59/fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652", size = 4843878, upload-time = "2025-09-29T21:11:10.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/e9/35c24a8d01644cee8c090a22fad34d5b61d1e0a8ecbc9945ad785ebf2e9e/fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a", size = 4954555, upload-time = "2025-09-29T21:11:13.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/86/fb1e994971be4bdfe3a307de6373ef69a9df83fb66e3faa9c8114893d4cc/fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce", size = 2232019, upload-time = "2025-09-29T21:11:15.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/84/62a19e2bd56f0e9fb347486a5b26376bade4bf6bbba64dda2c103bd08c94/fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038", size = 2276803, upload-time = "2025-09-29T21:11:18.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.7.0"
|
||||
@@ -1581,6 +1614,7 @@ dependencies = [
|
||||
{ name = "crccheck" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "curl-cffi" },
|
||||
{ name = "fonttools" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jsonpickle" },
|
||||
{ name = "langcodes" },
|
||||
@@ -1633,6 +1667,7 @@ requires-dist = [
|
||||
{ name = "crccheck", specifier = ">=1.3.0,<2" },
|
||||
{ name = "cryptography", specifier = ">=45.0.0" },
|
||||
{ name = "curl-cffi", specifier = ">=0.7.0b4,<0.8" },
|
||||
{ name = "fonttools", specifier = ">=4.0.0,<5" },
|
||||
{ name = "httpx", specifier = ">=0.28.1,<0.29" },
|
||||
{ name = "jsonpickle", specifier = ">=3.0.4,<4" },
|
||||
{ name = "langcodes", specifier = ">=3.4.0,<4" },
|
||||
@@ -1641,7 +1676,7 @@ requires-dist = [
|
||||
{ name = "protobuf", specifier = ">=4.25.3,<5" },
|
||||
{ name = "pycaption", specifier = ">=2.2.6,<3" },
|
||||
{ name = "pycryptodomex", specifier = ">=3.20.0,<4" },
|
||||
{ name = "pyexecjs", specifier = ">=1.5.1" },
|
||||
{ name = "pyexecjs", specifier = ">=1.5.1,<2" },
|
||||
{ name = "pyjwt", specifier = ">=2.8.0,<3" },
|
||||
{ name = "pymediainfo", specifier = ">=6.1.0,<7" },
|
||||
{ name = "pymp4", specifier = ">=1.4.0,<2" },
|
||||
|
||||
Reference in New Issue
Block a user