From 5a12cb33e260f85bbef773a8cf6e31f1e9637d86 Mon Sep 17 00:00:00 2001 From: rlaphoenix Date: Tue, 2 Apr 2024 13:03:53 +0100 Subject: [PATCH] refactor(Track): Move from OnXyz callables to Event observer Fixes #85 --- devine/commands/dl.py | 19 ++++++++++--- devine/core/manifests/dash.py | 16 ++++++----- devine/core/manifests/hls.py | 16 ++++++----- devine/core/service.py | 51 +++++++++++++++++++++++++++++++++++ devine/core/tracks/track.py | 27 +++++++------------ devine/core/tracks/tracks.py | 10 +++---- 6 files changed, 100 insertions(+), 39 deletions(-) diff --git a/devine/commands/dl.py b/devine/commands/dl.py index 86d2fe4..9b1dc69 100644 --- a/devine/commands/dl.py +++ b/devine/commands/dl.py @@ -43,6 +43,7 @@ from devine.core.console import console from devine.core.constants import DOWNLOAD_LICENCE_ONLY, AnyTrack, context_settings from devine.core.credential import Credential from devine.core.drm import DRM_T, Widevine +from devine.core.events import events from devine.core.proxies import Basic, Hola, NordVPN from devine.core.service import Service from devine.core.services import Services @@ -324,6 +325,14 @@ class dl: with console.status(f"Delaying by {delay} seconds..."): time.sleep(delay) + with console.status("Subscribing to events...", spinner="dots"): + events.reset() + events.subscribe(events.Types.SEGMENT_DOWNLOADED, service.on_segment_downloaded) + events.subscribe(events.Types.TRACK_DOWNLOADED, service.on_track_downloaded) + events.subscribe(events.Types.TRACK_DECRYPTED, service.on_track_decrypted) + events.subscribe(events.Types.TRACK_REPACKED, service.on_track_repacked) + events.subscribe(events.Types.TRACK_MULTIPLEX, service.on_track_multiplex) + with console.status("Getting tracks...", spinner="dots"): title.tracks.add(service.get_tracks(title), warn_only=True) title.tracks.chapters = service.get_chapters(title) @@ -339,8 +348,13 @@ class dl: non_sdh_sub = deepcopy(subtitle) non_sdh_sub.id += "_stripped" non_sdh_sub.sdh = False - non_sdh_sub.OnMultiplex = lambda: non_sdh_sub.strip_hearing_impaired() title.tracks.add(non_sdh_sub) + events.subscribe( + events.Types.TRACK_MULTIPLEX, + lambda track: ( + track.strip_hearing_impaired() + ) if track.id == non_sdh_sub.id else None + ) with console.status("Sorting tracks by language and bitrate...", spinner="dots"): title.tracks.sort_videos(by_language=v_lang or lang) @@ -636,8 +650,7 @@ class dl: if track.needs_repack: track.repackage() has_repacked = True - if callable(track.OnRepacked): - track.OnRepacked() + events.emit(events.Types.TRACK_REPACKED, track=track) if has_repacked: # we don't want to fill up the log with "Repacked x track" self.log.info("Repacked one or more tracks with FFMPEG") diff --git a/devine/core/manifests/dash.py b/devine/core/manifests/dash.py index ebb2f5b..ec3a731 100644 --- a/devine/core/manifests/dash.py +++ b/devine/core/manifests/dash.py @@ -24,6 +24,7 @@ from requests import Session from devine.core.constants import DOWNLOAD_CANCELLED, DOWNLOAD_LICENCE_ONLY, AnyTrack from devine.core.downloaders import requests as requests_downloader from devine.core.drm import Widevine +from devine.core.events import events from devine.core.tracks import Audio, Subtitle, Tracks, Video from devine.core.utilities import is_close_match, try_ensure_utf8 from devine.core.utils.xml import load_xml @@ -474,8 +475,8 @@ class DASH: max_workers=16 ): file_downloaded = status_update.get("file_downloaded") - if file_downloaded and callable(track.OnSegmentDownloaded): - track.OnSegmentDownloaded(file_downloaded) + if file_downloaded: + events.emit(events.Types.SEGMENT_DOWNLOADED, track=track, segment=file_downloaded) else: downloaded = status_update.get("downloaded") if downloaded and downloaded.endswith("/s"): @@ -514,15 +515,18 @@ class DASH: progress(advance=1) track.path = save_path - if callable(track.OnDownloaded): - track.OnDownloaded() + events.emit(events.Types.TRACK_DOWNLOADED, track=track) if drm: progress(downloaded="Decrypting", completed=0, total=100) drm.decrypt(save_path) track.drm = None - if callable(track.OnDecrypted): - track.OnDecrypted(drm) + events.emit( + events.Types.TRACK_DECRYPTED, + track=track, + drm=drm, + segment=None + ) progress(downloaded="Decrypting", advance=100) save_dir.rmdir() diff --git a/devine/core/manifests/hls.py b/devine/core/manifests/hls.py index 9435ecc..d371030 100644 --- a/devine/core/manifests/hls.py +++ b/devine/core/manifests/hls.py @@ -22,6 +22,7 @@ from requests import Session from devine.core.constants import DOWNLOAD_CANCELLED, DOWNLOAD_LICENCE_ONLY, AnyTrack from devine.core.downloaders import requests as requests_downloader from devine.core.drm import DRM_T, ClearKey, Widevine +from devine.core.events import events from devine.core.tracks import Audio, Subtitle, Tracks, Video from devine.core.utilities import get_binary_path, get_extension, is_close_match, try_ensure_utf8 @@ -282,8 +283,8 @@ class HLS: max_workers=16 ): file_downloaded = status_update.get("file_downloaded") - if file_downloaded and callable(track.OnSegmentDownloaded): - track.OnSegmentDownloaded(file_downloaded) + if file_downloaded: + events.emit(events.Types.SEGMENT_DOWNLOADED, track=track, segment=file_downloaded) else: downloaded = status_update.get("downloaded") if downloaded and downloaded.endswith("/s"): @@ -381,8 +382,12 @@ class HLS: drm.decrypt(merged_path) merged_path.rename(decrypted_path) - if callable(track.OnDecrypted): - track.OnDecrypted(drm, decrypted_path) + events.emit( + events.Types.TRACK_DECRYPTED, + track=track, + drm=drm, + segment=decrypted_path + ) return decrypted_path @@ -537,8 +542,7 @@ class HLS: progress(downloaded="Downloaded") track.path = save_path - if callable(track.OnDownloaded): - track.OnDownloaded() + events.emit(events.Types.TRACK_DOWNLOADED, track=track) @staticmethod def merge_segments(segments: list[Path], save_path: Path) -> int: diff --git a/devine/core/service.py b/devine/core/service.py index 3619fae..27774e6 100644 --- a/devine/core/service.py +++ b/devine/core/service.py @@ -3,10 +3,12 @@ import logging from abc import ABCMeta, abstractmethod from collections.abc import Generator from http.cookiejar import CookieJar +from pathlib import Path from typing import Optional, Union from urllib.parse import urlparse import click +import m3u8 import requests from requests.adapters import HTTPAdapter, Retry from rich.padding import Padding @@ -17,6 +19,7 @@ from devine.core.config import config from devine.core.console import console from devine.core.constants import AnyTrack from devine.core.credential import Credential +from devine.core.drm import DRM_T from devine.core.search_result import SearchResult from devine.core.titles import Title_T, Titles_T from devine.core.tracks import Chapters, Tracks @@ -235,5 +238,53 @@ class Service(metaclass=ABCMeta): option `chapter_fallback_name`. For example, `"Chapter {i:02}"` for "Chapter 01". """ + # Optional Event methods + + def on_segment_downloaded(self, track: AnyTrack, segment: Path) -> None: + """ + Called when one of a Track's Segments has finished downloading. + + Parameters: + track: The Track object that had a Segment downloaded. + segment: The Path to the Segment that was downloaded. + """ + + def on_track_downloaded(self, track: AnyTrack) -> None: + """ + Called when a Track has finished downloading. + + Parameters: + track: The Track object that was downloaded. + """ + + def on_track_decrypted(self, track: AnyTrack, drm: DRM_T, segment: Optional[m3u8.Segment] = None) -> None: + """ + Called when a Track has finished decrypting. + + Parameters: + track: The Track object that was decrypted. + drm: The DRM object it decrypted with. + segment: The HLS segment information that was decrypted. + """ + + def on_track_repacked(self, track: AnyTrack) -> None: + """ + Called when a Track has finished repacking. + + Parameters: + track: The Track object that was repacked. + """ + + def on_track_multiplex(self, track: AnyTrack) -> None: + """ + Called when a Track is about to be Multiplexed into a Container. + + Note: Right now only MKV containers are multiplexed but in the future + this may also be called when multiplexing to other containers like + MP4 via ffmpeg/mp4box. + + Parameters: + track: The Track object that was repacked. + """ __all__ = ("Service",) diff --git a/devine/core/tracks/track.py b/devine/core/tracks/track.py index c774bde..7dadf3f 100644 --- a/devine/core/tracks/track.py +++ b/devine/core/tracks/track.py @@ -12,7 +12,6 @@ from typing import Any, Callable, Iterable, Optional, Union from uuid import UUID from zlib import crc32 -import m3u8 from langcodes import Language from requests import Session @@ -20,6 +19,7 @@ from devine.core.config import config from devine.core.constants import DOWNLOAD_CANCELLED, DOWNLOAD_LICENCE_ONLY from devine.core.downloaders import aria2c, curl_impersonate, requests from devine.core.drm import DRM_T, Widevine +from devine.core.events import events from devine.core.utilities import get_binary_path, get_boxes, try_ensure_utf8 from devine.core.utils.subprocess import ffprobe @@ -122,17 +122,6 @@ class Track: # TODO: Currently using OnFoo event naming, change to just segment_filter self.OnSegmentFilter: Optional[Callable] = None - # Called after one of the Track's segments have downloaded - self.OnSegmentDownloaded: Optional[Callable[[Path], None]] = None - # Called after the Track has downloaded - self.OnDownloaded: Optional[Callable] = None - # Called after the Track or one of its segments have been decrypted - self.OnDecrypted: Optional[Callable[[DRM_T, Optional[m3u8.Segment]], None]] = None - # Called after the Track has been repackaged - self.OnRepacked: Optional[Callable] = None - # Called before the Track is multiplexed - self.OnMultiplex: Optional[Callable] = None - def __repr__(self) -> str: return "{name}({items})".format( name=self.__class__.__name__, @@ -257,15 +246,18 @@ class Track: save_path.with_suffix(f"{save_path.suffix}.aria2__temp").unlink(missing_ok=True) self.path = save_path - if callable(self.OnDownloaded): - self.OnDownloaded() + events.emit(events.Types.TRACK_DOWNLOADED, track=self) if drm: progress(downloaded="Decrypting", completed=0, total=100) drm.decrypt(save_path) self.drm = None - if callable(self.OnDecrypted): - self.OnDecrypted(drm) + events.emit( + events.Types.TRACK_DECRYPTED, + track=self, + drm=drm, + segment=None + ) progress(downloaded="Decrypted", completed=100) if track_type == "Subtitle" and self.codec.name not in ("fVTT", "fTTML"): @@ -299,8 +291,7 @@ class Track: if self.path.stat().st_size <= 3: # Empty UTF-8 BOM == 3 bytes raise IOError("Download failed, the downloaded file is empty.") - if callable(self.OnDownloaded): - self.OnDownloaded(self) + events.emit(events.Types.TRACK_DOWNLOADED, track=self) def delete(self) -> None: if self.path: diff --git a/devine/core/tracks/tracks.py b/devine/core/tracks/tracks.py index 4bdc7fc..179c8a5 100644 --- a/devine/core/tracks/tracks.py +++ b/devine/core/tracks/tracks.py @@ -14,6 +14,7 @@ from rich.tree import Tree from devine.core.config import config from devine.core.console import console from devine.core.constants import LANGUAGE_MAX_DISTANCE, AnyTrack, TrackT +from devine.core.events import events from devine.core.tracks.attachment import Attachment from devine.core.tracks.audio import Audio from devine.core.tracks.chapters import Chapter, Chapters @@ -337,8 +338,7 @@ class Tracks: for i, vt in enumerate(self.videos): if not vt.path or not vt.path.exists(): raise ValueError("Video Track must be downloaded before muxing...") - if callable(vt.OnMultiplex): - vt.OnMultiplex() + events.emit(events.Types.TRACK_MULTIPLEX, track=vt) cl.extend([ "--language", f"0:{vt.language}", "--default-track", f"0:{i == 0}", @@ -350,8 +350,7 @@ class Tracks: for i, at in enumerate(self.audio): if not at.path or not at.path.exists(): raise ValueError("Audio Track must be downloaded before muxing...") - if callable(at.OnMultiplex): - at.OnMultiplex() + events.emit(events.Types.TRACK_MULTIPLEX, track=at) cl.extend([ "--track-name", f"0:{at.get_track_name() or ''}", "--language", f"0:{at.language}", @@ -365,8 +364,7 @@ class Tracks: for st in self.subtitles: if not st.path or not st.path.exists(): raise ValueError("Text Track must be downloaded before muxing...") - if callable(st.OnMultiplex): - st.OnMultiplex() + events.emit(events.Types.TRACK_MULTIPLEX, track=st) default = bool(self.audio and is_close_match(st.language, [self.audio[0].language]) and st.forced) cl.extend([ "--track-name", f"0:{st.get_track_name() or ''}",