mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-05-17 14:29:27 +00:00
Compare commits
2 Commits
8e598f7d6a
...
1.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e10c760821 | ||
|
|
990084ab1f |
32
CHANGELOG.md
32
CHANGELOG.md
@@ -5,6 +5,38 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [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
|
## [1.4.1] - 2025-08-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "unshackle"
|
name = "unshackle"
|
||||||
version = "1.4.1"
|
version = "1.4.2"
|
||||||
description = "Modular Movie, TV, and Music Archival Software."
|
description = "Modular Movie, TV, and Music Archival Software."
|
||||||
authors = [{ name = "unshackle team" }]
|
authors = [{ name = "unshackle team" }]
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "1.4.1"
|
__version__ = "1.4.2"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from pathlib import Path
|
|||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from requests.adapters import HTTPAdapter, Retry
|
||||||
|
|
||||||
from unshackle.core import binaries
|
from unshackle.core import binaries
|
||||||
from unshackle.core.config import config
|
from unshackle.core.config import config
|
||||||
@@ -25,6 +26,22 @@ HEADERS = {"User-Agent": "unshackle-tags/1.0"}
|
|||||||
log = logging.getLogger("TAGS")
|
log = logging.getLogger("TAGS")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_session() -> requests.Session:
|
||||||
|
"""Create a requests session with retry logic for network failures."""
|
||||||
|
session = requests.Session()
|
||||||
|
session.headers.update(HEADERS)
|
||||||
|
|
||||||
|
retry = Retry(
|
||||||
|
total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET", "POST"]
|
||||||
|
)
|
||||||
|
|
||||||
|
adapter = HTTPAdapter(max_retries=retry)
|
||||||
|
session.mount("https://", adapter)
|
||||||
|
session.mount("http://", adapter)
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
def _api_key() -> Optional[str]:
|
def _api_key() -> Optional[str]:
|
||||||
return config.tmdb_api_key or os.getenv("TMDB_API_KEY")
|
return config.tmdb_api_key or os.getenv("TMDB_API_KEY")
|
||||||
|
|
||||||
@@ -59,7 +76,8 @@ def search_simkl(title: str, year: Optional[int], kind: str) -> Tuple[Optional[d
|
|||||||
filename += " 2160p.mkv"
|
filename += " 2160p.mkv"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.post("https://api.simkl.com/search/file", json={"file": filename}, headers=HEADERS, timeout=30)
|
session = _get_session()
|
||||||
|
resp = session.post("https://api.simkl.com/search/file", json={"file": filename}, timeout=30)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
log.debug("Simkl API response received")
|
log.debug("Simkl API response received")
|
||||||
@@ -139,17 +157,21 @@ def search_tmdb(title: str, year: Optional[int], kind: str) -> Tuple[Optional[in
|
|||||||
if year is not None:
|
if year is not None:
|
||||||
params["year" if kind == "movie" else "first_air_date_year"] = year
|
params["year" if kind == "movie" else "first_air_date_year"] = year
|
||||||
|
|
||||||
r = requests.get(
|
try:
|
||||||
f"https://api.themoviedb.org/3/search/{kind}",
|
session = _get_session()
|
||||||
params=params,
|
r = session.get(
|
||||||
headers=HEADERS,
|
f"https://api.themoviedb.org/3/search/{kind}",
|
||||||
timeout=30,
|
params=params,
|
||||||
)
|
timeout=30,
|
||||||
r.raise_for_status()
|
)
|
||||||
js = r.json()
|
r.raise_for_status()
|
||||||
results = js.get("results") or []
|
js = r.json()
|
||||||
log.debug("TMDB returned %d results", len(results))
|
results = js.get("results") or []
|
||||||
if not results:
|
log.debug("TMDB returned %d results", len(results))
|
||||||
|
if not results:
|
||||||
|
return None, None
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
log.warning("Failed to search TMDB for %s: %s", title, exc)
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
best_ratio = 0.0
|
best_ratio = 0.0
|
||||||
@@ -196,10 +218,10 @@ def get_title(tmdb_id: int, kind: str) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(
|
session = _get_session()
|
||||||
|
r = session.get(
|
||||||
f"https://api.themoviedb.org/3/{kind}/{tmdb_id}",
|
f"https://api.themoviedb.org/3/{kind}/{tmdb_id}",
|
||||||
params={"api_key": api_key},
|
params={"api_key": api_key},
|
||||||
headers=HEADERS,
|
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -219,10 +241,10 @@ def get_year(tmdb_id: int, kind: str) -> Optional[int]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(
|
session = _get_session()
|
||||||
|
r = session.get(
|
||||||
f"https://api.themoviedb.org/3/{kind}/{tmdb_id}",
|
f"https://api.themoviedb.org/3/{kind}/{tmdb_id}",
|
||||||
params={"api_key": api_key},
|
params={"api_key": api_key},
|
||||||
headers=HEADERS,
|
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -243,16 +265,21 @@ def external_ids(tmdb_id: int, kind: str) -> dict:
|
|||||||
return {}
|
return {}
|
||||||
url = f"https://api.themoviedb.org/3/{kind}/{tmdb_id}/external_ids"
|
url = f"https://api.themoviedb.org/3/{kind}/{tmdb_id}/external_ids"
|
||||||
log.debug("Fetching external IDs for %s %s", kind, tmdb_id)
|
log.debug("Fetching external IDs for %s %s", kind, tmdb_id)
|
||||||
r = requests.get(
|
|
||||||
url,
|
try:
|
||||||
params={"api_key": api_key},
|
session = _get_session()
|
||||||
headers=HEADERS,
|
r = session.get(
|
||||||
timeout=30,
|
url,
|
||||||
)
|
params={"api_key": api_key},
|
||||||
r.raise_for_status()
|
timeout=30,
|
||||||
js = r.json()
|
)
|
||||||
log.debug("External IDs response: %s", js)
|
r.raise_for_status()
|
||||||
return js
|
js = r.json()
|
||||||
|
log.debug("External IDs response: %s", js)
|
||||||
|
return js
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
log.warning("Failed to fetch external IDs for %s %s: %s", kind, tmdb_id, exc)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _apply_tags(path: Path, tags: dict[str, str]) -> None:
|
def _apply_tags(path: Path, tags: dict[str, str]) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user