8 Commits

Author SHA1 Message Date
Andy
dc9823cd28 chore(release): bump version to 2.1.0 2025-11-27 23:35:56 +00:00
Andy
6fa3554b70 fix(dl): preserve attachments when rebuilding track list
Attachments (screenshots, fonts) were being dropped when title.tracks was rebuilt from kept_tracks, causing image files to remain in temp directory after muxing. The cleanup code iterated over an empty attachments list since they were orphaned during track filtering.
2025-11-27 23:03:53 +00:00
Andy
2d4bf140fa fix(dash): add AdaptationSet-level BaseURL resolution
Add support for BaseURL elements at the AdaptationSet level per DASH spec. The URL resolution chain now properly follows: MPD → Period → AdaptationSet → Representation.
2025-11-25 16:09:28 +00:00
Andy
d0816787ce fix: restrict WindscribeVPN to supported regions 2025-11-24 18:17:00 +00:00
Andy
3d384b8e3e fix(windscribevpn): add error handling for unsupported regions in get_proxy method 2025-11-23 08:06:43 +00:00
Andy
e0a666ada6 fix(utilities): make space-hyphen-space handling conditional on scene_naming 2025-11-21 19:22:20 +00:00
Andy
26c81779fa fix(utilities): handle space-hyphen-space separators in sanitize_filename
Pre-process space-hyphen-space patterns (e.g., "Title - Episode") before other character replacements to prevent creating problematic dot-hyphen-dot (.-.) patterns in filenames.

This addresses PR #44 by fixing the root cause rather than post-processing the problematic pattern. The fix ensures that titles like "Show - S01E01" become "Show.S01E01"
2025-11-21 19:14:54 +00:00
Andy
3b32462251 feat(cdm): add per-track quality-based CDM selection during runtime DRM switching
Enable quality-based CDM selection during runtime DRM switching by passing track quality to get_cdm() calls. This allows different CDMs to be used for different video quality levels within the same download session.

Example configuration:
  cdm:
    SERVICE:
      "<=1080": wv_l3_local     # Widevine L3 for SD/HD
      ">1080": pr_sl3_remote    # PlayReady SL3 for 4K
2025-11-16 21:59:10 +00:00
8 changed files with 72 additions and 10 deletions

View File

@@ -5,6 +5,47 @@ 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).
## [2.1.0] - 2025-11-27
### Added
- **Per-Track Quality-Based CDM Selection**: Dynamic CDM switching during runtime DRM operations
- Enables quality-based CDM selection during runtime DRM switching
- Different CDMs can be used for different video quality levels within the same download session
- Example: Use Widevine L3 for SD/HD and PlayReady SL3 for 4K content
- **Enhanced Track Export**: Improved export functionality with additional metadata
- Added URL field to track export for easier identification
- Added descriptor information in export output
- Keys now exported in hex-formatted strings
### Changed
- **Dependencies**: Upgraded to latest compatible versions
- Updated various dependencies to their latest versions
### Fixed
- **Attachment Preservation**: Fixed attachments being dropped during track filtering
- Attachments (screenshots, fonts) were being lost when track list was rebuilt
- Fixes image files remaining in temp directory after muxing
- **DASH BaseURL Resolution**: Added AdaptationSet-level BaseURL support per DASH spec
- URL resolution chain now properly follows: MPD → Period → AdaptationSet → Representation
- **WindscribeVPN Region Support**: Restricted to supported regions with proper error handling
- Added error handling for unsupported regions in get_proxy method
- Prevents cryptic errors when using unsupported region codes
- **Filename Sanitization**: Fixed space-hyphen-space handling in filenames
- Pre-process space-hyphen-space patterns (e.g., "Title - Episode") before other replacements
- Made space-hyphen-space handling conditional on scene_naming setting
- Addresses PR #44 by fixing the root cause
- **CICP Enum Values**: Corrected values to match ITU-T H.273 specification
- Added Primaries.Unspecified (value 2) per H.273 spec
- Renamed Primaries/Transfer value 0 from Unspecified to Reserved for spec accuracy
- Simplified Transfer value 2 from Unspecified_Image to Unspecified
- Verified against ITU-T H.273, ISO/IEC 23091-2, H.264/H.265 specs, and FFmpeg enums
- **HLS Byte Range Parsing**: Fixed TypeError in range_offset conversion
- Converted range_offset to int to prevent TypeError in calculate_byte_range
- **pyplayready Compatibility**: Pinned to <0.7 to avoid KID extraction bug
## [2.0.0] - 2025-11-10
### Breaking Changes

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "unshackle"
version = "2.0.0"
version = "2.1.0"
description = "Modular Movie, TV, and Music Archival Software."
authors = [{ name = "unshackle team" }]
requires-python = ">=3.10,<3.13"

View File

@@ -1368,6 +1368,7 @@ class dl:
kept_tracks.extend(title.tracks.subtitles)
if keep_chapters:
kept_tracks.extend(title.tracks.chapters)
kept_tracks.extend(title.tracks.attachments)
title.tracks = Tracks(kept_tracks)
@@ -1847,25 +1848,32 @@ class dl:
if not drm:
return
track_quality = None
if isinstance(track, Video) and track.height:
pass
track_quality = track.height
if isinstance(drm, Widevine):
if not isinstance(self.cdm, (WidevineCdm, DecryptLabsRemoteCDM)) or (
isinstance(self.cdm, DecryptLabsRemoteCDM) and self.cdm.is_playready
):
widevine_cdm = self.get_cdm(self.service, self.profile, drm="widevine")
widevine_cdm = self.get_cdm(self.service, self.profile, drm="widevine", quality=track_quality)
if widevine_cdm:
self.log.info("Switching to Widevine CDM for Widevine content")
if track_quality:
self.log.info(f"Switching to Widevine CDM for Widevine {track_quality}p content")
else:
self.log.info("Switching to Widevine CDM for Widevine content")
self.cdm = widevine_cdm
elif isinstance(drm, PlayReady):
if not isinstance(self.cdm, (PlayReadyCdm, DecryptLabsRemoteCDM)) or (
isinstance(self.cdm, DecryptLabsRemoteCDM) and not self.cdm.is_playready
):
playready_cdm = self.get_cdm(self.service, self.profile, drm="playready")
playready_cdm = self.get_cdm(self.service, self.profile, drm="playready", quality=track_quality)
if playready_cdm:
self.log.info("Switching to PlayReady CDM for PlayReady content")
if track_quality:
self.log.info(f"Switching to PlayReady CDM for PlayReady {track_quality}p content")
else:
self.log.info("Switching to PlayReady CDM for PlayReady content")
self.cdm = playready_cdm
if isinstance(drm, Widevine):

View File

@@ -1 +1 @@
__version__ = "2.0.0"
__version__ = "2.1.0"

View File

@@ -297,8 +297,9 @@ class DASH:
manifest_base_url = track.url
elif not re.match("^https?://", manifest_base_url, re.IGNORECASE):
manifest_base_url = urljoin(track.url, f"./{manifest_base_url}")
period_base_url = urljoin(manifest_base_url, period.findtext("BaseURL"))
rep_base_url = urljoin(period_base_url, representation.findtext("BaseURL"))
period_base_url = urljoin(manifest_base_url, period.findtext("BaseURL") or "")
adaptation_set_base_url = urljoin(period_base_url, adaptation_set.findtext("BaseURL") or "")
rep_base_url = urljoin(adaptation_set_base_url, representation.findtext("BaseURL") or "")
period_duration = period.get("duration") or manifest.get("mediaPresentationDuration")
init_data: Optional[bytes] = None

View File

@@ -44,8 +44,17 @@ class WindscribeVPN(Proxy):
def get_proxy(self, query: str) -> Optional[str]:
"""
Get an HTTPS proxy URI for a WindscribeVPN server.
Note: Windscribe's static OpenVPN credentials work reliably on US, AU, and NZ servers.
"""
query = query.lower()
supported_regions = {"us", "au", "nz"}
if query not in supported_regions and query not in self.server_map:
raise ValueError(
f"Windscribe proxy does not currently support the '{query.upper()}' region. "
f"Supported regions with reliable credentials: {', '.join(sorted(supported_regions))}. "
)
if query in self.server_map:
hostname = self.server_map[query]
@@ -58,6 +67,7 @@ class WindscribeVPN(Proxy):
if not hostname:
return None
hostname = hostname.split(':')[0]
return f"https://{self.username}:{self.password}@{hostname}:443"
def get_random_server(self, country_code: str) -> Optional[str]:

View File

@@ -127,6 +127,8 @@ def sanitize_filename(filename: str, spacer: str = ".") -> str:
# remove or replace further characters as needed
filename = "".join(c for c in filename if unicodedata.category(c) != "Mn") # hidden characters
filename = filename.replace("/", " & ").replace(";", " & ") # e.g. multi-episode filenames
if spacer == ".":
filename = re.sub(r" - ", spacer, filename) # title separators to spacer (avoids .-. pattern)
filename = re.sub(r"[:; ]", spacer, filename) # structural chars to (spacer)
filename = re.sub(r"[\\*!?¿,'\"" "()<>|$#~]", "", filename) # not filename safe chars
filename = re.sub(rf"[{spacer}]{{2,}}", spacer, filename) # remove extra neighbouring (spacer)s

2
uv.lock generated
View File

@@ -1565,7 +1565,7 @@ wheels = [
[[package]]
name = "unshackle"
version = "2.0.0"
version = "2.1.0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp-swagger3" },