mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-03-17 16:47:29 +00:00
Compare commits
4 Commits
1.3.0
...
1aaea23669
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aaea23669 | ||
|
|
e3571b9518 | ||
|
|
b478a00519 | ||
|
|
24fb8fb00c |
30
CHANGELOG.md
30
CHANGELOG.md
@@ -15,6 +15,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Enhanced PlayReady and Widevine DRM classes with mp4decrypt decryption support
|
- Enhanced PlayReady and Widevine DRM classes with mp4decrypt decryption support
|
||||||
- Service-specific decryption mapping allows choosing between `shaka` and `mp4decrypt` per service
|
- Service-specific decryption mapping allows choosing between `shaka` and `mp4decrypt` per service
|
||||||
- Improved error handling and progress reporting for mp4decrypt operations
|
- 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
|
### Changed
|
||||||
|
|
||||||
@@ -22,12 +40,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Updated `dl.py` to handle service-specific decryption method selection
|
- Updated `dl.py` to handle service-specific decryption method selection
|
||||||
- Refactored `Config` class to manage decryption method mapping per service
|
- Refactored `Config` class to manage decryption method mapping per service
|
||||||
- Enhanced DRM decrypt methods with `use_mp4decrypt` parameter for method selection
|
- 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
|
### Fixed
|
||||||
|
|
||||||
- **Service Track Filtering**: Cleaned up ATVP service to remove unnecessary track filtering
|
- **Proxy Configuration**: Fixed proxy server mapping in configuration
|
||||||
- Simplified track return logic to pass all tracks to dl.py for centralized filtering
|
- Renamed 'servers' to 'server_map' in proxy configuration to resolve Nord/Surfshark naming conflicts
|
||||||
- Removed unused codec and quality filter parameters from service initialization
|
- 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
|
## [1.2.0] - 2025-07-30
|
||||||
|
|
||||||
|
|||||||
@@ -765,8 +765,7 @@ class dl:
|
|||||||
DOWNLOAD_LICENCE_ONLY.set()
|
DOWNLOAD_LICENCE_ONLY.set()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use transient mode to prevent display remnants
|
with Live(Padding(download_table, (1, 5)), console=console, refresh_per_second=5):
|
||||||
with Live(Padding(download_table, (1, 5)), console=console, refresh_per_second=5, transient=True):
|
|
||||||
with ThreadPoolExecutor(downloads) as pool:
|
with ThreadPoolExecutor(downloads) as pool:
|
||||||
for download in futures.as_completed(
|
for download in futures.as_completed(
|
||||||
(
|
(
|
||||||
@@ -1035,7 +1034,7 @@ class dl:
|
|||||||
|
|
||||||
multiplex_tasks.append((task_id, task_tracks))
|
multiplex_tasks.append((task_id, task_tracks))
|
||||||
|
|
||||||
with Live(Padding(progress, (0, 5, 1, 5)), console=console, transient=True):
|
with Live(Padding(progress, (0, 5, 1, 5)), console=console):
|
||||||
for task_id, task_tracks in multiplex_tasks:
|
for task_id, task_tracks in multiplex_tasks:
|
||||||
progress.start_task(task_id) # TODO: Needed?
|
progress.start_task(task_id) # TODO: Needed?
|
||||||
muxed_path, return_code, errors = task_tracks.mux(
|
muxed_path, return_code, errors = task_tracks.mux(
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import atexit
|
|
||||||
import logging
|
import logging
|
||||||
import signal
|
|
||||||
import sys
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import IO, Callable, Iterable, List, Literal, Mapping, Optional, Union
|
from typing import IO, Callable, Iterable, List, Literal, Mapping, Optional, Union
|
||||||
@@ -170,8 +167,6 @@ class ComfyConsole(Console):
|
|||||||
time.monotonic.
|
time.monotonic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_cleanup_registered = False
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -238,9 +233,6 @@ class ComfyConsole(Console):
|
|||||||
if log_renderer:
|
if log_renderer:
|
||||||
self._log_render = log_renderer
|
self._log_render = log_renderer
|
||||||
|
|
||||||
# Register terminal cleanup handlers
|
|
||||||
self._register_cleanup()
|
|
||||||
|
|
||||||
def status(
|
def status(
|
||||||
self,
|
self,
|
||||||
status: RenderableType,
|
status: RenderableType,
|
||||||
@@ -291,38 +283,6 @@ class ComfyConsole(Console):
|
|||||||
|
|
||||||
return status_renderable
|
return status_renderable
|
||||||
|
|
||||||
def _register_cleanup(self):
|
|
||||||
"""Register terminal cleanup handlers."""
|
|
||||||
if not ComfyConsole._cleanup_registered:
|
|
||||||
ComfyConsole._cleanup_registered = True
|
|
||||||
|
|
||||||
# Register cleanup on normal exit
|
|
||||||
atexit.register(self._cleanup_terminal)
|
|
||||||
|
|
||||||
# Register cleanup on signals
|
|
||||||
signal.signal(signal.SIGINT, self._signal_handler)
|
|
||||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
||||||
|
|
||||||
def _cleanup_terminal(self):
|
|
||||||
"""Restore terminal to a clean state."""
|
|
||||||
try:
|
|
||||||
# Show cursor using ANSI escape codes
|
|
||||||
sys.stdout.write("\x1b[?25h") # Show cursor
|
|
||||||
sys.stdout.write("\x1b[0m") # Reset attributes
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
# Also use Rich's method
|
|
||||||
self.show_cursor(True)
|
|
||||||
except Exception:
|
|
||||||
# Silently fail if cleanup fails
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _signal_handler(self, signum, frame):
|
|
||||||
"""Handle signals with cleanup."""
|
|
||||||
self._cleanup_terminal()
|
|
||||||
# Exit after cleanup
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
catppuccin_mocha = {
|
catppuccin_mocha = {
|
||||||
# Colors based on "CatppuccinMocha" from Gogh themes
|
# Colors based on "CatppuccinMocha" from Gogh themes
|
||||||
|
|||||||
@@ -10,11 +10,22 @@ import requests
|
|||||||
|
|
||||||
|
|
||||||
class UpdateChecker:
|
class UpdateChecker:
|
||||||
"""Check for available updates from the GitHub repository."""
|
"""
|
||||||
|
Check for available updates from the GitHub repository.
|
||||||
|
|
||||||
|
This class provides functionality to check for newer versions of the application
|
||||||
|
by querying the GitHub releases API. It includes rate limiting, caching, and
|
||||||
|
both synchronous and asynchronous interfaces.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
REPO_URL: GitHub API URL for latest release
|
||||||
|
TIMEOUT: Request timeout in seconds
|
||||||
|
DEFAULT_CHECK_INTERVAL: Default time between checks in seconds (24 hours)
|
||||||
|
"""
|
||||||
|
|
||||||
REPO_URL = "https://api.github.com/repos/unshackle-dl/unshackle/releases/latest"
|
REPO_URL = "https://api.github.com/repos/unshackle-dl/unshackle/releases/latest"
|
||||||
TIMEOUT = 5
|
TIMEOUT = 5
|
||||||
DEFAULT_CHECK_INTERVAL = 24 * 60 * 60 # 24 hours in seconds
|
DEFAULT_CHECK_INTERVAL = 24 * 60 * 60
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_cache_file(cls) -> Path:
|
def _get_cache_file(cls) -> Path:
|
||||||
@@ -23,6 +34,86 @@ class UpdateChecker:
|
|||||||
|
|
||||||
return config.directories.cache / "update_check.json"
|
return config.directories.cache / "update_check.json"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _load_cache_data(cls) -> dict:
|
||||||
|
"""
|
||||||
|
Load cache data from file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cache data dictionary or empty dict if loading fails
|
||||||
|
"""
|
||||||
|
cache_file = cls._get_cache_file()
|
||||||
|
|
||||||
|
if not cache_file.exists():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(cache_file, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_version(version_string: str) -> str:
|
||||||
|
"""
|
||||||
|
Parse and normalize version string by removing 'v' prefix.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version_string: Raw version string from API
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cleaned version string
|
||||||
|
"""
|
||||||
|
return version_string.lstrip("v")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_valid_version(version: str) -> bool:
|
||||||
|
"""
|
||||||
|
Validate version string format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: Version string to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if version string is valid semantic version, False otherwise
|
||||||
|
"""
|
||||||
|
if not version or not isinstance(version, str):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
parts = version.split(".")
|
||||||
|
if len(parts) < 2:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
int(part)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _fetch_latest_version(cls) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Fetch the latest version from GitHub API.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Latest version string if successful, None otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.get(cls.REPO_URL, timeout=cls.TIMEOUT)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
latest_version = cls._parse_version(data.get("tag_name", ""))
|
||||||
|
|
||||||
|
return latest_version if cls._is_valid_version(latest_version) else None
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _should_check_for_updates(cls, check_interval: int = DEFAULT_CHECK_INTERVAL) -> bool:
|
def _should_check_for_updates(cls, check_interval: int = DEFAULT_CHECK_INTERVAL) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -34,45 +125,40 @@ class UpdateChecker:
|
|||||||
Returns:
|
Returns:
|
||||||
True if we should check for updates, False otherwise
|
True if we should check for updates, False otherwise
|
||||||
"""
|
"""
|
||||||
cache_file = cls._get_cache_file()
|
cache_data = cls._load_cache_data()
|
||||||
|
|
||||||
if not cache_file.exists():
|
if not cache_data:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
|
||||||
with open(cache_file, "r") as f:
|
|
||||||
cache_data = json.load(f)
|
|
||||||
|
|
||||||
last_check = cache_data.get("last_check", 0)
|
last_check = cache_data.get("last_check", 0)
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
return (current_time - last_check) >= check_interval
|
return (current_time - last_check) >= check_interval
|
||||||
|
|
||||||
except (json.JSONDecodeError, KeyError, OSError):
|
|
||||||
# If cache is corrupted or unreadable, allow check
|
|
||||||
return True
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _update_cache(cls, latest_version: Optional[str] = None) -> None:
|
def _update_cache(cls, latest_version: Optional[str] = None, current_version: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Update the cache file with the current timestamp and latest version.
|
Update the cache file with the current timestamp and version info.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
latest_version: The latest version found, if any
|
latest_version: The latest version found, if any
|
||||||
|
current_version: The current version being used
|
||||||
"""
|
"""
|
||||||
cache_file = cls._get_cache_file()
|
cache_file = cls._get_cache_file()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Ensure cache directory exists
|
|
||||||
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
cache_data = {"last_check": time.time(), "latest_version": latest_version}
|
cache_data = {
|
||||||
|
"last_check": time.time(),
|
||||||
|
"latest_version": latest_version,
|
||||||
|
"current_version": current_version,
|
||||||
|
}
|
||||||
|
|
||||||
with open(cache_file, "w") as f:
|
with open(cache_file, "w") as f:
|
||||||
json.dump(cache_data, f)
|
json.dump(cache_data, f, indent=2)
|
||||||
|
|
||||||
except (OSError, json.JSONEncodeError):
|
except (OSError, json.JSONEncodeError):
|
||||||
# Silently fail if we can't write cache
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -87,6 +173,9 @@ class UpdateChecker:
|
|||||||
Returns:
|
Returns:
|
||||||
True if latest > current, False otherwise
|
True if latest > current, False otherwise
|
||||||
"""
|
"""
|
||||||
|
if not UpdateChecker._is_valid_version(current) or not UpdateChecker._is_valid_version(latest):
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_parts = [int(x) for x in current.split(".")]
|
current_parts = [int(x) for x in current.split(".")]
|
||||||
latest_parts = [int(x) for x in latest.split(".")]
|
latest_parts = [int(x) for x in latest.split(".")]
|
||||||
@@ -116,20 +205,14 @@ class UpdateChecker:
|
|||||||
Returns:
|
Returns:
|
||||||
The latest version string if an update is available, None otherwise
|
The latest version string if an update is available, None otherwise
|
||||||
"""
|
"""
|
||||||
|
if not cls._is_valid_version(current_version):
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
response = await loop.run_in_executor(None, lambda: requests.get(cls.REPO_URL, timeout=cls.TIMEOUT))
|
latest_version = await loop.run_in_executor(None, cls._fetch_latest_version)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if latest_version and cls._compare_versions(current_version, latest_version):
|
||||||
return None
|
|
||||||
|
|
||||||
data = response.json()
|
|
||||||
latest_version = data.get("tag_name", "").lstrip("v")
|
|
||||||
|
|
||||||
if not latest_version:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if cls._compare_versions(current_version, latest_version):
|
|
||||||
return latest_version
|
return latest_version
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -137,6 +220,31 @@ class UpdateChecker:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_cached_update_info(cls, current_version: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Check if there's a cached update available for the current version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_version: The current version string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The latest version string if an update is available from cache, None otherwise
|
||||||
|
"""
|
||||||
|
cache_data = cls._load_cache_data()
|
||||||
|
|
||||||
|
if not cache_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cached_current = cache_data.get("current_version")
|
||||||
|
cached_latest = cache_data.get("latest_version")
|
||||||
|
|
||||||
|
if cached_current == current_version and cached_latest:
|
||||||
|
if cls._compare_versions(current_version, cached_latest):
|
||||||
|
return cached_latest
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_for_updates_sync(cls, current_version: str, check_interval: Optional[int] = None) -> Optional[str]:
|
def check_for_updates_sync(cls, current_version: str, check_interval: Optional[int] = None) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
@@ -149,40 +257,20 @@ class UpdateChecker:
|
|||||||
Returns:
|
Returns:
|
||||||
The latest version string if an update is available, None otherwise
|
The latest version string if an update is available, None otherwise
|
||||||
"""
|
"""
|
||||||
# Use config value if not specified
|
if not cls._is_valid_version(current_version):
|
||||||
|
return None
|
||||||
|
|
||||||
if check_interval is None:
|
if check_interval is None:
|
||||||
from unshackle.core.config import config
|
from unshackle.core.config import config
|
||||||
|
|
||||||
check_interval = config.update_check_interval * 60 * 60 # Convert hours to seconds
|
check_interval = config.update_check_interval * 60 * 60
|
||||||
|
|
||||||
# Check if we should skip this check due to rate limiting
|
|
||||||
if not cls._should_check_for_updates(check_interval):
|
if not cls._should_check_for_updates(check_interval):
|
||||||
return None
|
return cls._get_cached_update_info(current_version)
|
||||||
|
|
||||||
try:
|
latest_version = cls._fetch_latest_version()
|
||||||
response = requests.get(cls.REPO_URL, timeout=cls.TIMEOUT)
|
cls._update_cache(latest_version, current_version)
|
||||||
|
if latest_version and cls._compare_versions(current_version, latest_version):
|
||||||
if response.status_code != 200:
|
|
||||||
# Update cache even on failure to prevent rapid retries
|
|
||||||
cls._update_cache()
|
|
||||||
return None
|
|
||||||
|
|
||||||
data = response.json()
|
|
||||||
latest_version = data.get("tag_name", "").lstrip("v")
|
|
||||||
|
|
||||||
if not latest_version:
|
|
||||||
cls._update_cache()
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Update cache with the latest version info
|
|
||||||
cls._update_cache(latest_version)
|
|
||||||
|
|
||||||
if cls._compare_versions(current_version, latest_version):
|
|
||||||
return latest_version
|
return latest_version
|
||||||
|
|
||||||
except Exception:
|
|
||||||
# Update cache even on exception to prevent rapid retries
|
|
||||||
cls._update_cache()
|
|
||||||
pass
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user