mirror of
https://github.com/vinefeeder/TwinVine.git
synced 2026-07-15 18:10:04 +02:00
1.4.8; search from menu
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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...")
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
__version__ = "1.4.7"
|
__version__ = "1.4.8"
|
||||||
|
|||||||
@@ -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
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user