1.4.8; search from menu

This commit is contained in:
VineFeeder
2025-10-15 11:35:58 +01:00
parent b05047259f
commit fa8f7a2a57
23 changed files with 669 additions and 78 deletions
+4
View File
@@ -13,4 +13,8 @@
**28 Sept 2025** **28 Sept 2025**
- added HellYes GUI - added HellYes GUI
- added images to reflect GUI change - added images to reflect GUI change
**15 Oct 2025**
- corrected keyword search when using menu in terminal window. Previously that mode of search only took a url striing; now it takes keywords.
- code additions to reflect upstream envied changes.
+3 -2
View File
@@ -4,7 +4,7 @@ build-backend = "uv_build"
[project] [project]
name = "envied" name = "envied"
version = "1.4.7" version = "1.4.8"
description = "Modular Movie, TV, and Music Archival Software." description = "Modular Movie, TV, and Music Archival Software."
authors = [{ name = "rlaphoenix and others" }] authors = [{ name = "rlaphoenix and others" }]
requires-python = ">=3.11,<3.15" requires-python = ">=3.11,<3.15"
@@ -60,9 +60,10 @@ dependencies = [
"subby", "subby",
"webvtt-py>=0.5.1", "webvtt-py>=0.5.1",
"isodate>=0.7.2", "isodate>=0.7.2",
"beaupy>=3.10.1", "beaupy>=3.10.2",
"scrapy>=2.13.3", "scrapy>=2.13.3",
"mypy>=1.18.2,<2", "mypy>=1.18.2,<2",
"pysubs2>=1.7.0,<2",
] ]
[project.urls] [project.urls]
+459
View File
@@ -0,0 +1,459 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4.8] - 2025-10-08
### Added
- **Exact Language Matching**: New `--exact-lang` flag for precise language matching
- Enables strict language code matching without fallbacks
- **No-Mux Flag**: New `--no-mux` flag to skip muxing tracks into container files
- Useful for keeping individual track files separate
- **AC4 Audio Codec Support**: Enhanced audio format handling
- Added AC4 codec support in Audio class with updated mime/profile handling
- **pysubs2 Subtitle Conversion**: Extended subtitle format support
- Added pysubs2 subtitle conversion with extended format support
- Configurable conversion method in configuration
### Changed
- **Audio Track Sorting**: Optimized audio track selection logic
- Improved audio track sorting by grouping descriptive tracks and sorting by bitrate
- Better identification of ATMOS and DD+ as highest quality for filenaming
- **pyplayready Update**: Upgraded to version 0.6.3
- Updated import paths to resolve compatibility issues
- Fixed lxml constraints for better dependency management
- **pysubs2 Conversion Method**: Moved from auto to manual configuration
- pysubs2 no longer auto-selected during testing phase
### Fixed
- **Remote CDM**: Fixed curl_cffi compatibility
- Added curl_cffi to instance checks in RemoteCDM
- **Temporary File Handling**: Improved encoding handling
- Specified UTF-8 encoding when opening temporary files
### Reverted
- **tinycss SyntaxWarning Suppression**: Removed ineffective warning filter
- Reverted warnings filter that didn't work as expected for suppressing tinycss warnings
## [1.4.7] - 2025-09-25
### Added
- **curl_cffi Session Support**: Enhanced anti-bot protection with browser impersonation
- Added new session utility with curl_cffi support for bypassing anti-bot measures
- Browser impersonation support for Chrome, Firefox, and Safari user agents
- Full backward compatibility with requests.Session maintained
- Suppressed HTTPS proxy warnings for improved user experience
- **Download Retry Functionality**: Configurable retry mechanism for failed downloads
- Added retry count option to download function for improved reliability
- **Subtitle Requirements Options**: Enhanced subtitle download control
- Added options for required subtitles in download command
- Better control over subtitle track selection and requirements
- **Quality Selection Enhancement**: Improved quality selection options
- Added best available quality option in download command for optimal track selection
- **DecryptLabs API Integration**: Enhanced remote CDM configuration
- Added decrypt_labs_api_key to Config initialization for better API integration
### Changed
- **Manifest Parser Updates**: Enhanced compatibility across all parsers
- Updated DASH, HLS, ISM, and M3U8 parsers to accept curl_cffi sessions
- Improved cookie handling compatibility between requests and curl_cffi
- **Logging Improvements**: Reduced log verbosity for better user experience
- Changed duplicate track log level to debug to reduce console noise
- Dynamic CDM selection messages moved to debug-only output
### Fixed
- **Remote CDM Reuse**: Fixed KeyError in dynamic CDM selection
- Prevents KeyError when reusing remote CDMs in dynamic selection process
- Creates copy of CDM dictionary before modification to prevent configuration mutation
- Allows same CDM to be selected multiple times within session without errors
## [1.4.6] - 2025-09-13
### Added
- **Quality-Based CDM Selection**: Dynamic CDM selection based on video resolution
- Automatically selects appropriate CDM (L3/L1) based on video track quality
- Supports quality thresholds in configuration (>=, >, <=, <, exact match)
- Pre-selects optimal CDM based on highest quality across all video tracks
- Maintains backward compatibility with existing CDM configurations
- **Automatic Audio Language Metadata**: Intelligent embedded audio language detection
- Automatically sets audio language metadata when no separate audio tracks exist
- Smart video track selection based on title language with fallbacks
- Enhanced FFmpeg repackaging with audio stream metadata injection
- **Lazy DRM Loading**: Deferred DRM loading for multi-track key retrieval optimization
- Add deferred DRM loading to M3U8 parser to mark tracks for later processing
- Just-in-time DRM loading during download process for better performance
### Changed
- **Enhanced CDM Management**: Improved CDM switching logic for multi-quality downloads
- CDM selection now based on highest quality track to avoid inefficient switching
- Quality-based selection only within same DRM type (Widevine-to-Widevine, PlayReady-to-PlayReady)
- Single CDM used per session for better performance and reliability
### Fixed
- **Vault Caching Issues**: Fixed vault count display and NoneType iteration errors
- Fix 'NoneType' object is not iterable error in DecryptLabsRemoteCDM
- Fix vault count display showing 0/3 instead of actual successful vault count
- **Service Name Transmission**: Resolved DecryptLabsRemoteCDM service name issues
- Fixed DecryptLabsRemoteCDM sending 'generic' instead of proper service names
- Added case-insensitive vault lookups for SQLite/MySQL vaults
- Added local vault integration to DecryptLabsRemoteCDM
- **Import Organization**: Improved import ordering and code formatting
- Reorder imports in decrypt_labs_remote_cdm.py for better organization
- Clean up trailing whitespace in vault files
### Configuration
- **New CDM Configuration Format**: Extended `cdm:` section supports quality-based selection
```yaml
cdm:
SERVICE_NAME:
"<=1080": l3_cdm_name
">1080": l1_cdm_name
default: l3_cdm_name
```
## [1.4.5] - 2025-09-09
### Added
- **Enhanced CDM Key Caching**: Improved key caching and session management for L1/L2 devices
- Optimized `get_cached_keys_if_exists` functionality for better performance with L1/L2 devices
- Enhanced cached key retrieval logic with improved session handling
- **Widevine Common Certificate Fallback**: Added fallback to Widevine common certificate for L1 devices
- Improved compatibility for L1 devices when service certificates are unavailable
- **Enhanced Vault Loading**: Improved vault loading and key copying logic
- Better error handling and key management in vault operations
- **PSSH Display Optimization**: Truncated PSSH string display in non-debug mode for cleaner output
- **CDM Error Messaging**: Added error messages for missing service certificates in CDM sessions
### Changed
- **Dynamic Version Headers**: Updated User-Agent headers to use dynamic version strings
- DecryptLabsRemoteCDM now uses dynamic version import instead of hardcoded version
- **Intelligent CDM Caching**: Implemented intelligent caching system for CDM license requests
- Enhanced caching logic reduces redundant license requests and improves performance
- **Enhanced Tag Handling**: Improved tag handling for TV shows and movies from Simkl data
- Better metadata processing and formatting for improved media tagging
### Fixed
- **CDM Session Management**: Clean up session data when retrieving cached keys
- Remove decrypt_labs_session_id and challenge from session when cached keys exist but there are missing kids
- Ensures clean state for subsequent requests and prevents session conflicts
- **Tag Formatting**: Fixed formatting issues in tag processing
- **Import Order**: Fixed import order issues in tags module
## [1.4.4] - 2025-09-02
### Added
- **Enhanced DecryptLabs CDM Support**: Comprehensive remote CDM functionality
- Full support for Widevine, PlayReady, and ChromeCDM through DecryptLabsRemoteCDM
- Enhanced session management and caching support for remote WV/PR operations
- Support for cached keys and improved license handling
- New CDM configurations for Chrome and PlayReady devices with updated User-Agent and service certificate
- **Advanced Configuration Options**: New device and language preferences
- Added configuration options for device certificate status list
- Enhanced language preference settings
### Changed
- **DRM Decryption Enhancements**: Streamlined decryption process
- Simplified decrypt method by removing unused parameter and streamlined logic
- Improved DecryptLabs CDM configurations with better device support
### Fixed
- **Matroska Tag Compliance**: Enhanced media container compatibility
- Fixed Matroska tag compliance with official specification
- **Application Branding**: Cleaned up version display
- Removed old devine version reference from banner to avoid developer confusion
- Updated branding while maintaining original GNU license compliance
- **IP Information Handling**: Improved geolocation services
- Enhanced get_ip_info functionality with better failover handling
- Added support for 429 error handling and multiple API provider fallback
- Implemented cached IP info retrieval with fallback tester to avoid rate limiting
- **Dependencies**: Streamlined package requirements
- Removed unnecessary data extra requirement from langcodes
### Removed
- Deprecated version references in application banner for clarity
## [1.4.3] - 2025-08-20
### Added
- Cached IP info helper for region detection
- New `get_cached_ip_info()` with 24h cache and provider rotation (ipinfo/ipapi) with 429 handling.
- Reduces external calls and stabilizes non-proxy region lookups for caching/logging.
### Changed
- DRM decryption selection is fully configuration-driven
- Widevine and PlayReady now select the decrypter based solely on `decryption` in YAML (including per-service mapping).
- Shaka Packager remains the default decrypter when not specified.
- `dl.py` logs the chosen tool based on the resolved configuration.
- Geofencing and proxy verification improvements
- Safer geofence checks with error handling and clearer logs.
- Always verify proxy exit region via live IP lookup; fallback to proxy parsing on failure.
- Example config updated to default to Shaka
- `unshackle.yaml`/example now sets `decryption.default: shaka` (service overrides still supported).
### Removed
- Deprecated parameter `use_mp4decrypt`
- Removed from `Widevine.decrypt()` and `PlayReady.decrypt()` and all callsites.
- Internal naming switched from mp4decrypt-specific flags to generic `decrypter` selection.
## [1.4.2] - 2025-08-14
### Added
- **Session Management for API Requests**: Enhanced API reliability with retry logic
- Implemented session management for tags functionality with automatic retry mechanisms
- Improved API request stability and error handling
- **Series Year Configuration**: New `series_year` option for title naming control
- Added configurable `series_year` option to control year inclusion in series titles
- Enhanced YAML configuration with series year handling options
- **Audio Language Override**: New audio language selection option
- Added `audio_language` option to override default language selection for audio tracks
- Provides more granular control over audio track selection
- **Vault Key Reception Control**: Enhanced vault security options
- Added `no_push` option to Vault and its subclasses to control key reception
- Improved key management security and flexibility
### Changed
- **HLS Segment Processing**: Enhanced segment retrieval and merging capabilities
- Enhanced segment retrieval to allow all file types for better compatibility
- Improved segment merging with recursive file search and fallback to binary concatenation
- Fixed issues with VTT files from HLS not being found correctly due to format changes
- Added cleanup of empty segment directories after processing
- **Documentation**: Updated README.md with latest information
### Fixed
- **Audio Track Selection**: Improved per-language logic for audio tracks
- Adjusted `per_language` logic to ensure correct audio track selection
- Fixed issue where all tracks for selected language were being downloaded instead of just the intended ones
## [1.4.1] - 2025-08-08
### Added
- **Title Caching System**: Intelligent title caching to reduce redundant API calls
- Configurable title caching with 30-minute default cache duration
- 24-hour fallback cache on API failures for improved reliability
- Region-aware caching to handle geo-restricted content properly
- SHA256 hashing for cache keys to handle complex title IDs
- Added `--no-cache` CLI flag to bypass caching when needed
- Added `--reset-cache` CLI flag to clear existing cache data
- New cache configuration variables in config system
- Documented caching options in example configuration file
- Significantly improves performance when debugging or modifying CLI parameters
- **Enhanced Tagging Configuration**: New options for customizing tag behavior
- Added `tag_group_name` config option to control group name inclusion in tags
- Added `tag_imdb_tmdb` config option to control IMDB/TMDB details in tags
- Added Simkl API endpoint support as fallback when no TMDB API key is provided
- Enhanced tag_file function to prioritize provided TMDB ID when `--tmdb` flag is used
- Improved TMDB ID handling with better prioritization logic
### Changed
- **Language Selection Enhancement**: Improved default language handling
- Updated language option default to 'orig' when no `-l` flag is set
- Avoids hardcoded 'en' default and respects original content language
- **Tagging Logic Improvements**: Simplified and enhanced tagging functionality
- Simplified Simkl search logic with soft-fail when no results found
- Enhanced tag_file function with better TMDB ID prioritization
- Improved error handling in tagging operations
### Fixed
- **Subtitle Processing**: Enhanced subtitle filtering for edge cases
- Fixed ValueError in subtitle filtering for multiple colons in time references
- Improved handling of subtitles containing complex time formatting
- Better error handling for malformed subtitle timestamps
### Removed
- **Docker Support**: Removed Docker configuration from repository
- Removed Dockerfile and .dockerignore files
- Cleaned up README.md Docker-related documentation
- Focuses on direct installation methods
## [1.4.0] - 2025-08-05
### Added
- **HLG Transfer Characteristics Preservation**: Enhanced video muxing to preserve HLG color metadata
- Added automatic detection of HLG video tracks during muxing process
- Implemented `--color-transfer-characteristics 0:18` argument for mkvmerge when processing HLG content
- Prevents incorrect conversion from HLG (18) to BT.2020 (14) transfer characteristics
- Ensures proper HLG playback support on compatible hardware without manual editing
- **Original Language Support**: Enhanced language selection with 'orig' keyword support
- Added support for 'orig' language selector for both video and audio tracks
- Automatically detects and uses the title's original language when 'orig' is specified
- Improved language processing logic with better duplicate handling
- Enhanced help text to document original language selection usage
- **Forced Subtitle Support**: Added option to include forced subtitle tracks
- New functionality to download and include forced subtitle tracks alongside regular subtitles
- **WebVTT Subtitle Filtering**: Enhanced subtitle processing capabilities
- Added filtering for unwanted cues in WebVTT subtitles
- Improved subtitle quality by removing unnecessary metadata
### Changed
- **DRM Track Decryption**: Improved DRM decryption track selection logic
- Enhanced `get_drm_for_cdm()` method usage for better DRM-CDM matching
- Added warning messages when no matching DRM is found for tracks
- Improved error handling and logging for DRM decryption failures
- **Series Tree Representation**: Enhanced episode tree display formatting
- Updated series tree to show season breakdown with episode counts
- Improved visual representation with "S{season}({count})" format
- Better organization of series information in console output
- **Hybrid Processing UI**: Enhanced extraction and conversion processes
- Added dynamic spinning bars to follow the rest of the codebase design
- Improved visual feedback during hybrid HDR processing operations
- **Track Selection Logic**: Enhanced multi-track selection capabilities
- Fixed track selection to support combining -V, -A, -S flags properly
- Improved flexibility in selecting multiple track types simultaneously
- **Service Subtitle Support**: Added configuration for services without subtitle support
- Services can now indicate if they don't support subtitle downloads
- Prevents unnecessary subtitle download attempts for unsupported services
- **Update Checker**: Enhanced update checking logic and cache handling
- Improved rate limiting and caching mechanisms for update checks
- Better performance and reduced API calls to GitHub
### Fixed
- **PlayReady KID Extraction**: Enhanced KID extraction from PSSH data
- Added base64 support and XML parsing for better KID detection
- Fixed issue where only one KID was being extracted for certain services
- Improved multi-KID support for PlayReady protected content
- **Dolby Vision Detection**: Improved DV codec detection across all formats
- Fixed detection of dvhe.05.06 codec which was not being recognized correctly
- Enhanced detection logic in Episode and Movie title classes
- Better support for various Dolby Vision codec variants
## [1.3.0] - 2025-08-03
### Added
- **mp4decrypt Support**: Alternative DRM decryption method using mp4decrypt from Bento4
- Added `mp4decrypt` binary detection and support in binaries module
- New `decryption` configuration option in unshackle.yaml for service-specific decryption methods
- Enhanced PlayReady and Widevine DRM classes with mp4decrypt decryption support
- Service-specific decryption mapping allows choosing between `shaka` and `mp4decrypt` per service
- Improved error handling and progress reporting for mp4decrypt operations
- **Scene Naming Configuration**: New `scene_naming` option for controlling file naming conventions
- Added scene naming logic to movie, episode, and song title classes
- Configurable through unshackle.yaml to enable/disable scene naming standards
- **Terminal Cleanup and Signal Handling**: Enhanced console management
- Implemented proper terminal cleanup on application exit
- Added signal handling for graceful shutdown in ComfyConsole
- **Configuration Template**: New `unshackle-example.yaml` template file
- Replaced main `unshackle.yaml` with example template to prevent git conflicts
- Users can now modify their local config without affecting repository updates
- **Enhanced Credential Management**: Improved CDM and vault configuration
- Expanded credential management documentation in configuration
- Enhanced CDM configuration examples and guidelines
- **Video Transfer Standards**: Added `Unspecified_Image` option to Transfer enum
- Implements ITU-T H.Sup19 standard value 2 for image characteristics
- Supports still image coding systems and unknown transfer characteristics
- **Update Check Rate Limiting**: Enhanced update checking system
- Added configurable update check intervals to prevent excessive API calls
- Improved rate limiting for GitHub API requests
### Changed
- **DRM Decryption Architecture**: Enhanced decryption system with dual method support
- Updated `dl.py` to handle service-specific decryption method selection
- Refactored `Config` class to manage decryption method mapping per service
- Enhanced DRM decrypt methods with `use_mp4decrypt` parameter for method selection
- **Error Handling**: Improved exception handling in Hybrid class
- Replaced log.exit calls with ValueError exceptions for better error propagation
- Enhanced error handling consistency across hybrid processing
### Fixed
- **Proxy Configuration**: Fixed proxy server mapping in configuration
- Renamed 'servers' to 'server_map' in proxy configuration to resolve Nord/Surfshark naming conflicts
- Updated configuration structure for better compatibility with proxy providers
- **HTTP Vault**: Improved URL handling and key retrieval logic
- Fixed URL processing issues in HTTP-based key vaults
- Enhanced key retrieval reliability and error handling
## [1.2.0] - 2025-07-30
### Added
- **Update Checker**: Automatic GitHub release version checking on startup
- Configurable update notifications via `update_checks` setting in unshackle.yaml
- Non-blocking HTTP requests with 5-second timeout for performance
- Smart semantic version comparison supporting all version formats (x.y.z, x.y, x)
- Graceful error handling for network issues and API failures
- User-friendly update notifications with current → latest version display
- Direct links to GitHub releases page for easy updates
- **HDR10+ Support**: Enhanced HDR10+ metadata processing for hybrid tracks
- HDR10+ tool binary support (`hdr10plus_tool`) added to binaries module
- HDR10+ to Dolby Vision conversion capabilities in hybrid processing
- Enhanced metadata extraction for HDR10+ content
- **Duration Fix Handling**: Added duration correction for video and hybrid tracks
- **Temporary Directory Management**: Automatic creation of temp directories for attachment downloads
### Changed
- Enhanced configuration system with new `update_checks` boolean option (defaults to true)
- Updated sample unshackle.yaml with update checker configuration documentation
- Improved console styling consistency using `bright_black` for dimmed text
- **Environment Dependency Check**: Complete overhaul with detailed categorization and status summary
- Organized dependencies by category (Core, HDR, Download, Subtitle, Player, Network)
- Enhanced status reporting with compact summary display
- Improved tool requirement tracking and missing dependency alerts
- **Hybrid Track Processing**: Significant improvements to HDR10+ and Dolby Vision handling
- Enhanced metadata extraction and processing workflows
- Better integration with HDR processing tools
### Removed
- **Docker Workflow**: Removed Docker build and publish GitHub Actions workflow for manual builds
## [1.1.0] - 2025-07-29
### Added
- **HDR10+DV Hybrid Processing**: New `-r HYBRID` command for processing HDR10 and Dolby Vision tracks
- Support for hybrid HDR processing and injection using dovi_tool
- New hybrid track processing module for seamless HDR10/DV conversion
- Automatic detection and handling of HDR10 and DV metadata
- Support for HDR10 and DV tracks in hybrid mode for EXAMPLE service
- Binary availability check for dovi_tool in hybrid mode operations
- Enhanced track processing capabilities for HDR content
### Fixed
- Import order issues and missing json import in hybrid processing
- UV installation process and error handling improvements
- Binary search functionality updated to use `binaries.find`
### Changed
- Updated package version from 1.0.2 to 1.1.0
- Enhanced dl.py command processing for hybrid mode support
- Improved core titles (episode/movie) processing for HDR content
- Extended tracks module with hybrid processing capabilities
+16 -6
View File
@@ -162,6 +162,12 @@ class dl:
) )
@click.option("-sl", "--s-lang", type=LANGUAGE_RANGE, default=["all"], help="Language wanted for Subtitles.") @click.option("-sl", "--s-lang", type=LANGUAGE_RANGE, default=["all"], help="Language wanted for Subtitles.")
@click.option("-fs", "--forced-subs", is_flag=True, default=False, help="Include forced subtitle tracks.") @click.option("-fs", "--forced-subs", is_flag=True, default=False, help="Include forced subtitle tracks.")
@click.option(
"--exact-lang",
is_flag=True,
default=False,
help="Use exact language matching (no variants). With this flag, -l es-419 matches ONLY es-419, not es-ES or other variants.",
)
@click.option( @click.option(
"--proxy", "--proxy",
type=str, type=str,
@@ -438,6 +444,7 @@ class dl:
a_lang: list[str], a_lang: list[str],
s_lang: list[str], s_lang: list[str],
forced_subs: bool, forced_subs: bool,
exact_lang: bool,
sub_format: Optional[Subtitle.Codec], sub_format: Optional[Subtitle.Codec],
video_only: bool, video_only: bool,
audio_only: bool, audio_only: bool,
@@ -709,7 +716,8 @@ class dl:
else: else:
if language not in processed_video_lang: if language not in processed_video_lang:
processed_video_lang.append(language) processed_video_lang.append(language)
title.tracks.videos = title.tracks.by_language(title.tracks.videos, processed_video_lang) title.tracks.videos = title.tracks.by_language(
title.tracks.videos, processed_video_lang, exact_match=exact_lang)
if not title.tracks.videos: if not title.tracks.videos:
self.log.error(f"There's no {processed_video_lang} Video Track...") self.log.error(f"There's no {processed_video_lang} Video Track...")
sys.exit(1) sys.exit(1)
@@ -772,16 +780,18 @@ class dl:
# filter subtitle tracks # filter subtitle tracks
if s_lang and "all" not in s_lang: if s_lang and "all" not in s_lang:
from envied.core.utilities import is_exact_match
match_func = is_exact_match if exact_lang else is_close_match
missing_langs = [ missing_langs = [
lang_ lang_
for lang_ in s_lang for lang_ in s_lang
if not any(is_close_match(lang_, [sub.language]) for sub in title.tracks.subtitles) if not any(match_func(lang_, [sub.language]) for sub in title.tracks.subtitles)
] ]
if missing_langs: if missing_langs:
self.log.error(", ".join(missing_langs) + " not found in tracks") self.log.error(", ".join(missing_langs) + " not found in tracks")
sys.exit(1) sys.exit(1)
title.tracks.select_subtitles(lambda x: is_close_match(x.language, s_lang)) title.tracks.select_subtitles(lambda x: match_func(x.language, s_lang))
if not title.tracks.subtitles: if not title.tracks.subtitles:
self.log.error(f"There's no {s_lang} Subtitle Track...") self.log.error(f"There's no {s_lang} Subtitle Track...")
sys.exit(1) sys.exit(1)
@@ -845,7 +855,7 @@ class dl:
elif "all" not in processed_lang: elif "all" not in processed_lang:
per_language = 1 per_language = 1
title.tracks.audio = title.tracks.by_language( title.tracks.audio = title.tracks.by_language(
title.tracks.audio, processed_lang, per_language=per_language title.tracks.audio, processed_lang, per_language=per_language, exact_match=exact_lang
) )
if not title.tracks.audio: if not title.tracks.audio:
self.log.error(f"There's no {processed_lang} Audio Track, cannot continue...") self.log.error(f"There's no {processed_lang} Audio Track, cannot continue...")
@@ -1040,11 +1050,11 @@ class dl:
if family_dir.exists(): if family_dir.exists():
fonts = family_dir.glob("*.*tf") fonts = family_dir.glob("*.*tf")
for font in fonts: for font in fonts:
title.tracks.add(Attachment(font, f"{font_name} ({font.stem})")) title.tracks.add(Attachment(path=font, name=f"{font_name} ({font.stem})"))
font_count += 1 font_count += 1
elif fonts_from_system: elif fonts_from_system:
for font in fonts_from_system: for font in fonts_from_system:
title.tracks.add(Attachment(font, f"{font_name} ({font.stem})")) title.tracks.add(Attachment(path=font, name=f"{font_name} ({font.stem})"))
font_count += 1 font_count += 1
else: else:
self.log.warning(f"Subtitle uses font [text2]{font_name}[/] but it could not be found...") self.log.warning(f"Subtitle uses font [text2]{font_name}[/] but it could not be found...")
+1 -1
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 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 -1
View File
@@ -1 +1 @@
__version__ = "1.4.7" __version__ = "1.4.8"
+2 -2
View File
@@ -1,6 +1,6 @@
import atexit import atexit
import logging import logging
import warnings #import warnings
from pathlib import Path from pathlib import Path
import click import click
@@ -8,7 +8,7 @@ import urllib3
from rich import traceback from rich import traceback
# Suppress SyntaxWarning from unmaintained tinycss package (dependency of subby) # Suppress SyntaxWarning from unmaintained tinycss package (dependency of subby)
warnings.filterwarnings("ignore", category=SyntaxWarning, module="tinycss") #warnings.filterwarnings("ignore", category=SyntaxWarning, module="tinycss")
from rich.console import Group from rich.console import Group
from rich.padding import Padding from rich.padding import Padding
@@ -6,6 +6,7 @@ DOWNLOAD_LICENCE_ONLY = Event()
DRM_SORT_MAP = ["ClearKey", "Widevine"] DRM_SORT_MAP = ["ClearKey", "Widevine"]
LANGUAGE_MAX_DISTANCE = 5 # this is max to be considered "same", e.g., en, en-US, en-AU LANGUAGE_MAX_DISTANCE = 5 # this is max to be considered "same", e.g., en, en-US, en-AU
LANGUAGE_EXACT_DISTANCE = 0 # exact match only, no variants
VIDEO_CODEC_MAP = {"AVC": "H.264", "HEVC": "H.265"} VIDEO_CODEC_MAP = {"AVC": "H.264", "HEVC": "H.265"}
DYNAMIC_RANGE_MAP = {"HDR10": "HDR", "HDR10+": "HDR10P", "Dolby Vision": "DV", "HDR10 / HDR10+": "HDR10P", "HDR10 / HDR10": "HDR"} DYNAMIC_RANGE_MAP = {"HDR10": "HDR", "HDR10+": "HDR10P", "Dolby Vision": "DV", "HDR10 / HDR10+": "HDR10P", "HDR10 / HDR10": "HDR"}
AUDIO_CODEC_MAP = {"E-AC-3": "DDP", "AC-3": "DD"} AUDIO_CODEC_MAP = {"E-AC-3": "DDP", "AC-3": "DD"}
@@ -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:
@@ -964,4 +1027,4 @@ class Subtitle(Track):
) )
__all__ = ("Subtitle",) __all__ = ("Subtitle",)
@@ -14,7 +14,7 @@ from rich.tree import Tree
from envied.core import binaries from envied.core import binaries
from envied.core.config import config from envied.core.config import config
from envied.core.console import console from envied.core.console import console
from envied.core.constants import LANGUAGE_MAX_DISTANCE, AnyTrack, TrackT from envied.core.constants import LANGUAGE_EXACT_DISTANCE, LANGUAGE_MAX_DISTANCE, AnyTrack, TrackT
from envied.core.events import events from envied.core.events import events
from envied.core.tracks.attachment import Attachment from envied.core.tracks.attachment import Attachment
from envied.core.tracks.audio import Audio from envied.core.tracks.audio import Audio
@@ -295,11 +295,14 @@ class Tracks:
self.videos = selected self.videos = selected
@staticmethod @staticmethod
def by_language(tracks: list[TrackT], languages: list[str], per_language: int = 0) -> list[TrackT]: def by_language(
tracks: list[TrackT], languages: list[str], per_language: int = 0, exact_match: bool = False
) -> list[TrackT]:
distance = LANGUAGE_EXACT_DISTANCE if exact_match else LANGUAGE_MAX_DISTANCE
selected = [] selected = []
for language in languages: for language in languages:
selected.extend( selected.extend(
[x for x in tracks if closest_supported_match(x.language, [language], LANGUAGE_MAX_DISTANCE)][ [x for x in tracks if closest_supported_match(str(x.language), [language], distance)][
: per_language or None : per_language or None
] ]
) )
+8 -1
View File
@@ -24,7 +24,7 @@ from unidecode import unidecode
from envied.core.cacher import Cacher from envied.core.cacher import Cacher
from envied.core.config import config from envied.core.config import config
from envied.core.constants import LANGUAGE_MAX_DISTANCE from envied.core.constants import LANGUAGE_EXACT_DISTANCE, LANGUAGE_MAX_DISTANCE
def rotate_log_file(log_path: Path, keep: int = 20) -> Path: def rotate_log_file(log_path: Path, keep: int = 20) -> Path:
@@ -113,6 +113,13 @@ def is_close_match(language: Union[str, Language], languages: Sequence[Union[str
return False return False
return closest_match(language, list(map(str, languages)))[1] <= LANGUAGE_MAX_DISTANCE return closest_match(language, list(map(str, languages)))[1] <= LANGUAGE_MAX_DISTANCE
def is_exact_match(language: Union[str, Language], languages: Sequence[Union[str, Language, None]]) -> bool:
"""Check if a language is an exact match to any of the provided languages."""
languages = [x for x in languages if x]
if not languages:
return False
return closest_match(language, list(map(str, languages)))[1] <= LANGUAGE_EXACT_DISTANCE
def get_boxes(data: bytes, box_type: bytes, as_bytes: bool = False) -> Box: def get_boxes(data: bytes, box_type: bytes, as_bytes: bool = False) -> Box:
""" """
@@ -1,2 +1,2 @@
BATCH_DOWNLOAD: true BATCH_DOWNLOAD: false
TERMINAL_RESET: true TERMINAL_RESET: true
@@ -90,15 +90,17 @@ class All4Loader(BaseLoader):
elif inx == 0: elif inx == 0:
# from greedy-search OR selecting Browse-category # from greedy-search OR selecting Browse-category
# example: https://www.channel4.com/programmes/the-great-british-bake-off/on-demand/75228-001 # example: https://www.channel4.com/programmes/the-great-british-bake-off/on-demand/75228-001
if not 'https' in search_term:
# need a search keyword(s) from url return self.fetch_videos(search_term)
# need a search keyword(s) from category url
# split and select series name # split and select series name
search_term = search_term.split("/")[4] else:
# fetch_videos_by_category search_term may have other params to remove search_term = search_term.split("/")[4]
if "?" in search_term: # fetch_videos_by_category search_term may have other params to remove
search_term = search_term.split("?")[0].replace("-", " ") if "?" in search_term:
search_term = search_term.split("?")[0].replace("-", " ")
return self.fetch_videos(search_term) return self.fetch_videos(search_term)
elif "http" in search_term and inx == 2: elif "http" in search_term and inx == 2:
self.category = category self.category = category
@@ -185,12 +185,8 @@ class BbcLoader(BaseLoader):
search_term.split("series")[0].replace("-", " ").strip() search_term.split("series")[0].replace("-", " ").strip()
) )
return self.fetch_videos(search_term) return self.fetch_videos(search_term)
else:
else: return self.fetch_videos(search_term)
print(
f"No search term found in {search_term}\nTry again with a series name in the url."
)
return
elif "http" in search_term and inx == 2: elif "http" in search_term and inx == 2:
self.category = category self.category = category
@@ -62,7 +62,6 @@ class ItvxLoader(BaseLoader):
e, e,
"Is the package for vinefeeder/envied installed correctly ?", "Is the package for vinefeeder/envied installed correctly ?",
) )
return return
# keyword search # keyword search
@@ -73,14 +72,16 @@ class ItvxLoader(BaseLoader):
# ALTERNATIVES BELOW FROM POP-UP MENU # ALTERNATIVES BELOW FROM POP-UP MENU
elif inx == 0: elif inx == 0:
# from greedy-search OR selecting Browse-category # from greedy-search OR selecting Browse-category
if not 'https' in search_term:
# need a search keyword(s) from url return self.fetch_videos(search_term)
# need a search keyword(s) from category url
# split and select series name # split and select series name
search_term = search_term.split("/")[4].replace("-", " ") else:
# fetch_videos_by_category search_term may have other params to remove search_term = search_term.split("/")[4].replace("-", " ")
if "?" in search_term: # fetch_videos_by_category search_term may have other params to remove
search_term = search_term.split("?")[0].replace("-", " ") if "?" in search_term:
return self.fetch_videos(search_term) search_term = search_term.split("?")[0].replace("-", " ")
return self.fetch_videos(search_term)
elif "http" in search_term and inx == 2: elif "http" in search_term and inx == 2:
self.category = category self.category = category
@@ -66,9 +66,12 @@ class My5Loader(BaseLoader):
# ALTERNATIVES BELOW FROM POP-UP MENU # ALTERNATIVES BELOW FROM POP-UP MENU
elif inx == 0: elif inx == 0:
# from greedy-search OR selecting Browse-category # from greedy-search OR selecting Browse-category
if not 'https' in search_term:
return self.fetch_videos(search_term)
# example: https://www.channel5.com/the-teacher/season-2/episode-2 # example: https://www.channel5.com/the-teacher/season-2/episode-2
# need a search keyword(s) from url # need a search keyword(s) from url
# split and select series name # split and select series name
if "show" in search_term: if "show" in search_term:
match = re.search(r"\/show\/([a-z-]+)", search_term) match = re.search(r"\/show\/([a-z-]+)", search_term)
@@ -73,14 +73,18 @@ class PlexLoader(BaseLoader):
elif inx == 0: elif inx == 0:
# from greedy-search OR selecting Browse-category # from greedy-search OR selecting Browse-category
# example: https://www.channel4.com/programmes/the-great-british-bake-off/on-demand/75228-001 # example: https://www.channel4.com/programmes/the-great-british-bake-off/on-demand/75228-001
if not 'https' in search_term:
# need a search keyword(s) from url return self.fetch_videos(search_term)
# need a search keyword(s) from category url
# split and select series name # split and select series name
search_term = search_term.split("/")[4].replace("-", " ") else:
# fetch_videos_by_category search_term may have other params to remove # need a search keyword(s) from url
if "?" in search_term: # split and select series name
search_term = search_term.split("?")[0].replace("-", " ") search_term = search_term.split("/")[4].replace("-", " ")
return self.fetch_videos(search_term) # fetch_videos_by_category search_term may have other params to remove
if "?" in search_term:
search_term = search_term.split("?")[0].replace("-", " ")
return self.fetch_videos(search_term)
elif "http" in search_term and inx == 2: elif "http" in search_term and inx == 2:
self.category = category self.category = category
@@ -82,14 +82,18 @@ class RteLoader(BaseLoader):
elif inx == 0: elif inx == 0:
# from greedy-search OR selecting Browse-category # from greedy-search OR selecting Browse-category
# example: https://www.channel4.com/programmes/the-great-british-bake-off/on-demand/75228-001 # example: https://www.channel4.com/programmes/the-great-british-bake-off/on-demand/75228-001
if not 'https' in search_term:
# need a search keyword(s) from url return self.fetch_videos(search_term)
# need a search keyword(s) from category url
# split and select series name # split and select series name
search_term = search_term.split("/")[4].replace("-", " ") else:
# fetch_videos_by_category search_term may have other params to remove # need a search keyword(s) from url
if "?" in search_term: # split and select series name
search_term = search_term.split("?")[0].replace("-", " ") search_term = search_term.split("/")[4].replace("-", " ")
return self.fetch_videos(search_term) # fetch_videos_by_category search_term may have other params to remove
if "?" in search_term:
search_term = search_term.split("?")[0].replace("-", " ")
return self.fetch_videos(search_term)
elif "http" in search_term and inx == 2: elif "http" in search_term and inx == 2:
self.category = category self.category = category
@@ -78,12 +78,16 @@ class StvLoader(BaseLoader):
# https://player.stv.tv/summary/joan # https://player.stv.tv/summary/joan
# need a search keyword(s) from url # need a search keyword(s) from url
# split and select series name # split and select series name
if not 'https' in search_term:
search_term = search_term.split("/")[-1] return self.fetch_videos(search_term)
# fetch_videos_by_category search_term may have other params to remove # need a search keyword(s) from category url
if "?" in search_term: # split and select series name
search_term = search_term.split("?")[0].replace("-", " ") else:
return self.fetch_videos(search_term) search_term = search_term.split("/")[-1]
# fetch_videos_by_category search_term may have other params to remove
if "?" in search_term:
search_term = search_term.split("?")[0].replace("-", " ")
return self.fetch_videos(search_term)
elif "http" in search_term and inx == 2: elif "http" in search_term and inx == 2:
self.category = category self.category = category
@@ -110,12 +110,16 @@ class TptvLoader(BaseLoader):
# ALTERNATIVES BELOW FROM POP-UP MENU # ALTERNATIVES BELOW FROM POP-UP MENU
elif inx == 0: elif inx == 0:
# from greedy-search OR selecting Browse-category # from greedy-search OR selecting Browse-category
if not 'https' in search_term:
search_term = search_term.split("/")[-1] return self.fetch_videos(search_term)
# fetch_videos_by_category search_term may have other params to remove # need a search keyword(s) from category url
if "?" in search_term: # split and select series name
search_term = search_term.split("?")[0].replace("-", " ") else:
return self.fetch_videos(search_term) search_term = search_term.split("/")[-1]
# fetch_videos_by_category search_term may have other params to remove
if "?" in search_term:
search_term = search_term.split("?")[0].replace("-", " ")
return self.fetch_videos(search_term)
elif "http" in search_term and inx == 2: elif "http" in search_term and inx == 2:
self.category = category self.category = category
@@ -94,9 +94,14 @@ class TvnzLoader(BaseLoader):
# from greedy-search OR selecting Browse-category # from greedy-search OR selecting Browse-category
# need a search keyword(s) from url # need a search keyword(s) from url
# split and select series name # split and select series name
search_term = search_term.split("/")[4] if not 'https' in search_term:
return self.fetch_videos(search_term)
# need a search keyword(s) from category url
# split and select series name
else:
search_term = search_term.split("/")[4]
return self.fetch_videos(search_term) return self.fetch_videos(search_term)
elif "http" in search_term and inx == 2: elif "http" in search_term and inx == 2:
self.category = category self.category = category
@@ -72,11 +72,15 @@ class ULoader(BaseLoader):
elif inx == 0: elif inx == 0:
# from greedy-search OR selecting Browse-category # from greedy-search OR selecting Browse-category
# example https://u.co.uk/shows/worlds-most-dangerous-roads/series-6/episode-1/6363704256112 # example https://u.co.uk/shows/worlds-most-dangerous-roads/series-6/episode-1/6363704256112
if not 'https' in search_term:
# need a search keyword(s) from url return self.fetch_videos(search_term)
# need a search keyword(s) from category url
# split and select series name # split and select series name
search_term = search_term.split("/")[4].replace("-", " ") else:
return self.fetch_videos(search_term) # need a search keyword(s) from url
# split and select series name
search_term = search_term.split("/")[4].replace("-", " ")
return self.fetch_videos(search_term)
elif "http" in search_term and inx == 2: elif "http" in search_term and inx == 2:
self.category = category self.category = category
Generated
+21 -5
View File
@@ -150,7 +150,7 @@ wheels = [
[[package]] [[package]]
name = "beaupy" name = "beaupy"
version = "3.10.1" version = "3.10.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "emoji" }, { name = "emoji" },
@@ -158,9 +158,9 @@ dependencies = [
{ name = "questo" }, { name = "questo" },
{ name = "rich" }, { name = "rich" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/24/60/3b3b4d23e135c6828792f84d46be86c126fe72771121723c5775c43a6812/beaupy-3.10.1.tar.gz", hash = "sha256:8d8a1cde212264a0402a672eed322488fb212f2e11c80c08ed011a70b489ef12", size = 16417, upload-time = "2025-01-14T18:19:27.888Z" } sdist = { url = "https://files.pythonhosted.org/packages/d0/ce/81f6aa40d2c779a3f0a0ac11d130a9c14c6b4f53700cd279980f3b9ecdf0/beaupy-3.10.2.tar.gz", hash = "sha256:da1a644f71e60fddc599084a5ef8bd41118b9c779527ce501289729e60e7db90", size = 16535, upload-time = "2025-10-06T18:47:50.063Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/a8/0787af1185f990023d69240a61c46b853bad35d040206b9704179bac8dac/beaupy-3.10.1-py3-none-any.whl", hash = "sha256:b31337581fc445fc81b07701d97de6409d6b13a4d0b620ca461475c2756c45ca", size = 14675, upload-time = "2025-01-14T18:19:25.533Z" }, { url = "https://files.pythonhosted.org/packages/d6/bf/42536b458a7108a3b880bbfc99783e73b1c3b4c24d4ede3057bb22c44e07/beaupy-3.10.2-py3-none-any.whl", hash = "sha256:c5398416b6a9dfed25d56be4caf4b995c41c9d18aa132203ef69fa3d8953bcc5", size = 14842, upload-time = "2025-10-06T18:47:48.8Z" },
] ]
[[package]] [[package]]
@@ -547,7 +547,7 @@ wheels = [
[[package]] [[package]]
name = "envied" name = "envied"
version = "1.4.7" version = "1.4.8"
source = { editable = "packages/envied" } source = { editable = "packages/envied" }
dependencies = [ dependencies = [
{ name = "appdirs" }, { name = "appdirs" },
@@ -574,6 +574,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"] },
@@ -604,7 +605,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "appdirs", specifier = ">=1.4.4,<2" }, { name = "appdirs", specifier = ">=1.4.4,<2" },
{ name = "beaupy", specifier = ">=3.10.1" }, { name = "beaupy", specifier = ">=3.10.2" },
{ name = "brotli", specifier = ">=1.1.0,<2" }, { name = "brotli", specifier = ">=1.1.0,<2" },
{ name = "chardet", specifier = ">=5.2.0,<6" }, { name = "chardet", specifier = ">=5.2.0,<6" },
{ name = "click", specifier = ">=8.1.8,<9" }, { name = "click", specifier = ">=8.1.8,<9" },
@@ -627,6 +628,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" },
@@ -1683,6 +1685,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/1e/c6a28a142f14e735088534cc92951c3f48cccd77cdd4f3b10d7996be420f/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b", size = 303833, upload-time = "2025-05-23T12:26:41.075Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/c6a28a142f14e735088534cc92951c3f48cccd77cdd4f3b10d7996be420f/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b", size = 303833, upload-time = "2025-05-23T12:26:41.075Z" },
{ url = "https://files.pythonhosted.org/packages/89/63/e5adf350c1c3123d4865c013f164c5265512fa79f09ad464fb2fdf9f9e61/pyqt6_sip-13.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277", size = 53527, upload-time = "2025-05-23T12:26:42.625Z" }, { url = "https://files.pythonhosted.org/packages/89/63/e5adf350c1c3123d4865c013f164c5265512fa79f09ad464fb2fdf9f9e61/pyqt6_sip-13.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277", size = 53527, upload-time = "2025-05-23T12:26:42.625Z" },
{ url = "https://files.pythonhosted.org/packages/58/74/2df4195306d050fbf4963fb5636108a66e5afa6dc05fd9e81e51ec96c384/pyqt6_sip-13.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244", size = 45373, upload-time = "2025-05-23T12:26:43.536Z" }, { url = "https://files.pythonhosted.org/packages/58/74/2df4195306d050fbf4963fb5636108a66e5afa6dc05fd9e81e51ec96c384/pyqt6_sip-13.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244", size = 45373, upload-time = "2025-05-23T12:26:43.536Z" },
{ url = "https://files.pythonhosted.org/packages/23/57/74b4eb7a51b9133958daa8409b55de95e44feb694d4e2e3eba81a070ca20/pyqt6_sip-13.10.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8a76a06a8e5c5b1f17a3f6f3c834ca324877e07b960b18b8b9bbfd9c536ec658", size = 112354, upload-time = "2025-10-08T08:44:00.22Z" },
{ url = "https://files.pythonhosted.org/packages/f2/cb/fdef02e0d6ee8443a9683a43650d61c6474b634b6ae6e1c6f097da6310bf/pyqt6_sip-13.10.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9128d770a611200529468397d710bc972f1dcfe12bfcbb09a3ccddcd4d54fa5b", size = 323488, upload-time = "2025-10-08T08:44:01.965Z" },
{ url = "https://files.pythonhosted.org/packages/8c/5b/8ede8d6234c3ea884cbd097d7d47ff9910fb114efe041af62b4453acd23b/pyqt6_sip-13.10.2-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d820a0fae7315932c08f27dc0a7e33e0f50fe351001601a8eb9cf6f22b04562e", size = 303881, upload-time = "2025-10-08T08:44:04.086Z" },
{ url = "https://files.pythonhosted.org/packages/be/44/b5e78b072d1594643b0f1ff348f2bf54d4adb5a3f9b9f0989c54e33238d6/pyqt6_sip-13.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:3213bb6e102d3842a3bb7e59d5f6e55f176c80880ff0b39d0dac0cfe58313fb3", size = 55098, upload-time = "2025-10-08T08:44:08.943Z" },
{ url = "https://files.pythonhosted.org/packages/e2/91/357e9fcef5d830c3d50503d35e0357818aca3540f78748cc214dfa015d00/pyqt6_sip-13.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:ce33ff1f94960ad4b08035e39fa0c3c9a67070bec39ffe3e435c792721504726", size = 46088, upload-time = "2025-10-08T08:44:10.014Z" },
] ]
[[package]] [[package]]
@@ -1694,6 +1701,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 = "pytest" name = "pytest"
version = "8.4.2" version = "8.4.2"