refactor(Track): Move from OnXyz callables to Event observer

Fixes #85
This commit is contained in:
rlaphoenix 2024-04-02 13:03:53 +01:00
parent 226b609ff5
commit 5a12cb33e2
6 changed files with 100 additions and 39 deletions

View File

@ -43,6 +43,7 @@ from devine.core.console import console
from devine.core.constants import DOWNLOAD_LICENCE_ONLY, AnyTrack, context_settings from devine.core.constants import DOWNLOAD_LICENCE_ONLY, AnyTrack, context_settings
from devine.core.credential import Credential from devine.core.credential import Credential
from devine.core.drm import DRM_T, Widevine 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.proxies import Basic, Hola, NordVPN
from devine.core.service import Service from devine.core.service import Service
from devine.core.services import Services from devine.core.services import Services
@ -324,6 +325,14 @@ class dl:
with console.status(f"Delaying by {delay} seconds..."): with console.status(f"Delaying by {delay} seconds..."):
time.sleep(delay) 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"): with console.status("Getting tracks...", spinner="dots"):
title.tracks.add(service.get_tracks(title), warn_only=True) title.tracks.add(service.get_tracks(title), warn_only=True)
title.tracks.chapters = service.get_chapters(title) title.tracks.chapters = service.get_chapters(title)
@ -339,8 +348,13 @@ class dl:
non_sdh_sub = deepcopy(subtitle) non_sdh_sub = deepcopy(subtitle)
non_sdh_sub.id += "_stripped" non_sdh_sub.id += "_stripped"
non_sdh_sub.sdh = False non_sdh_sub.sdh = False
non_sdh_sub.OnMultiplex = lambda: non_sdh_sub.strip_hearing_impaired()
title.tracks.add(non_sdh_sub) 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"): with console.status("Sorting tracks by language and bitrate...", spinner="dots"):
title.tracks.sort_videos(by_language=v_lang or lang) title.tracks.sort_videos(by_language=v_lang or lang)
@ -636,8 +650,7 @@ class dl:
if track.needs_repack: if track.needs_repack:
track.repackage() track.repackage()
has_repacked = True has_repacked = True
if callable(track.OnRepacked): events.emit(events.Types.TRACK_REPACKED, track=track)
track.OnRepacked()
if has_repacked: if has_repacked:
# we don't want to fill up the log with "Repacked x track" # we don't want to fill up the log with "Repacked x track"
self.log.info("Repacked one or more tracks with FFMPEG") self.log.info("Repacked one or more tracks with FFMPEG")

View File

@ -24,6 +24,7 @@ from requests import Session
from devine.core.constants import DOWNLOAD_CANCELLED, DOWNLOAD_LICENCE_ONLY, AnyTrack from devine.core.constants import DOWNLOAD_CANCELLED, DOWNLOAD_LICENCE_ONLY, AnyTrack
from devine.core.downloaders import requests as requests_downloader from devine.core.downloaders import requests as requests_downloader
from devine.core.drm import Widevine from devine.core.drm import Widevine
from devine.core.events import events
from devine.core.tracks import Audio, Subtitle, Tracks, Video from devine.core.tracks import Audio, Subtitle, Tracks, Video
from devine.core.utilities import is_close_match, try_ensure_utf8 from devine.core.utilities import is_close_match, try_ensure_utf8
from devine.core.utils.xml import load_xml from devine.core.utils.xml import load_xml
@ -474,8 +475,8 @@ class DASH:
max_workers=16 max_workers=16
): ):
file_downloaded = status_update.get("file_downloaded") file_downloaded = status_update.get("file_downloaded")
if file_downloaded and callable(track.OnSegmentDownloaded): if file_downloaded:
track.OnSegmentDownloaded(file_downloaded) events.emit(events.Types.SEGMENT_DOWNLOADED, track=track, segment=file_downloaded)
else: else:
downloaded = status_update.get("downloaded") downloaded = status_update.get("downloaded")
if downloaded and downloaded.endswith("/s"): if downloaded and downloaded.endswith("/s"):
@ -514,15 +515,18 @@ class DASH:
progress(advance=1) progress(advance=1)
track.path = save_path track.path = save_path
if callable(track.OnDownloaded): events.emit(events.Types.TRACK_DOWNLOADED, track=track)
track.OnDownloaded()
if drm: if drm:
progress(downloaded="Decrypting", completed=0, total=100) progress(downloaded="Decrypting", completed=0, total=100)
drm.decrypt(save_path) drm.decrypt(save_path)
track.drm = None track.drm = None
if callable(track.OnDecrypted): events.emit(
track.OnDecrypted(drm) events.Types.TRACK_DECRYPTED,
track=track,
drm=drm,
segment=None
)
progress(downloaded="Decrypting", advance=100) progress(downloaded="Decrypting", advance=100)
save_dir.rmdir() save_dir.rmdir()

View File

@ -22,6 +22,7 @@ from requests import Session
from devine.core.constants import DOWNLOAD_CANCELLED, DOWNLOAD_LICENCE_ONLY, AnyTrack from devine.core.constants import DOWNLOAD_CANCELLED, DOWNLOAD_LICENCE_ONLY, AnyTrack
from devine.core.downloaders import requests as requests_downloader from devine.core.downloaders import requests as requests_downloader
from devine.core.drm import DRM_T, ClearKey, Widevine 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.tracks import Audio, Subtitle, Tracks, Video
from devine.core.utilities import get_binary_path, get_extension, is_close_match, try_ensure_utf8 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 max_workers=16
): ):
file_downloaded = status_update.get("file_downloaded") file_downloaded = status_update.get("file_downloaded")
if file_downloaded and callable(track.OnSegmentDownloaded): if file_downloaded:
track.OnSegmentDownloaded(file_downloaded) events.emit(events.Types.SEGMENT_DOWNLOADED, track=track, segment=file_downloaded)
else: else:
downloaded = status_update.get("downloaded") downloaded = status_update.get("downloaded")
if downloaded and downloaded.endswith("/s"): if downloaded and downloaded.endswith("/s"):
@ -381,8 +382,12 @@ class HLS:
drm.decrypt(merged_path) drm.decrypt(merged_path)
merged_path.rename(decrypted_path) merged_path.rename(decrypted_path)
if callable(track.OnDecrypted): events.emit(
track.OnDecrypted(drm, decrypted_path) events.Types.TRACK_DECRYPTED,
track=track,
drm=drm,
segment=decrypted_path
)
return decrypted_path return decrypted_path
@ -537,8 +542,7 @@ class HLS:
progress(downloaded="Downloaded") progress(downloaded="Downloaded")
track.path = save_path track.path = save_path
if callable(track.OnDownloaded): events.emit(events.Types.TRACK_DOWNLOADED, track=track)
track.OnDownloaded()
@staticmethod @staticmethod
def merge_segments(segments: list[Path], save_path: Path) -> int: def merge_segments(segments: list[Path], save_path: Path) -> int:

View File

@ -3,10 +3,12 @@ import logging
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from collections.abc import Generator from collections.abc import Generator
from http.cookiejar import CookieJar from http.cookiejar import CookieJar
from pathlib import Path
from typing import Optional, Union from typing import Optional, Union
from urllib.parse import urlparse from urllib.parse import urlparse
import click import click
import m3u8
import requests import requests
from requests.adapters import HTTPAdapter, Retry from requests.adapters import HTTPAdapter, Retry
from rich.padding import Padding from rich.padding import Padding
@ -17,6 +19,7 @@ from devine.core.config import config
from devine.core.console import console from devine.core.console import console
from devine.core.constants import AnyTrack from devine.core.constants import AnyTrack
from devine.core.credential import Credential from devine.core.credential import Credential
from devine.core.drm import DRM_T
from devine.core.search_result import SearchResult from devine.core.search_result import SearchResult
from devine.core.titles import Title_T, Titles_T from devine.core.titles import Title_T, Titles_T
from devine.core.tracks import Chapters, Tracks 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". 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",) __all__ = ("Service",)

View File

@ -12,7 +12,6 @@ from typing import Any, Callable, Iterable, Optional, Union
from uuid import UUID from uuid import UUID
from zlib import crc32 from zlib import crc32
import m3u8
from langcodes import Language from langcodes import Language
from requests import Session 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.constants import DOWNLOAD_CANCELLED, DOWNLOAD_LICENCE_ONLY
from devine.core.downloaders import aria2c, curl_impersonate, requests from devine.core.downloaders import aria2c, curl_impersonate, requests
from devine.core.drm import DRM_T, Widevine 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.utilities import get_binary_path, get_boxes, try_ensure_utf8
from devine.core.utils.subprocess import ffprobe from devine.core.utils.subprocess import ffprobe
@ -122,17 +122,6 @@ class Track:
# TODO: Currently using OnFoo event naming, change to just segment_filter # TODO: Currently using OnFoo event naming, change to just segment_filter
self.OnSegmentFilter: Optional[Callable] = None 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: def __repr__(self) -> str:
return "{name}({items})".format( return "{name}({items})".format(
name=self.__class__.__name__, name=self.__class__.__name__,
@ -257,15 +246,18 @@ class Track:
save_path.with_suffix(f"{save_path.suffix}.aria2__temp").unlink(missing_ok=True) save_path.with_suffix(f"{save_path.suffix}.aria2__temp").unlink(missing_ok=True)
self.path = save_path self.path = save_path
if callable(self.OnDownloaded): events.emit(events.Types.TRACK_DOWNLOADED, track=self)
self.OnDownloaded()
if drm: if drm:
progress(downloaded="Decrypting", completed=0, total=100) progress(downloaded="Decrypting", completed=0, total=100)
drm.decrypt(save_path) drm.decrypt(save_path)
self.drm = None self.drm = None
if callable(self.OnDecrypted): events.emit(
self.OnDecrypted(drm) events.Types.TRACK_DECRYPTED,
track=self,
drm=drm,
segment=None
)
progress(downloaded="Decrypted", completed=100) progress(downloaded="Decrypted", completed=100)
if track_type == "Subtitle" and self.codec.name not in ("fVTT", "fTTML"): 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 if self.path.stat().st_size <= 3: # Empty UTF-8 BOM == 3 bytes
raise IOError("Download failed, the downloaded file is empty.") raise IOError("Download failed, the downloaded file is empty.")
if callable(self.OnDownloaded): events.emit(events.Types.TRACK_DOWNLOADED, track=self)
self.OnDownloaded(self)
def delete(self) -> None: def delete(self) -> None:
if self.path: if self.path:

View File

@ -14,6 +14,7 @@ from rich.tree import Tree
from devine.core.config import config from devine.core.config import config
from devine.core.console import console from devine.core.console import console
from devine.core.constants import LANGUAGE_MAX_DISTANCE, AnyTrack, TrackT 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.attachment import Attachment
from devine.core.tracks.audio import Audio from devine.core.tracks.audio import Audio
from devine.core.tracks.chapters import Chapter, Chapters from devine.core.tracks.chapters import Chapter, Chapters
@ -337,8 +338,7 @@ class Tracks:
for i, vt in enumerate(self.videos): for i, vt in enumerate(self.videos):
if not vt.path or not vt.path.exists(): if not vt.path or not vt.path.exists():
raise ValueError("Video Track must be downloaded before muxing...") raise ValueError("Video Track must be downloaded before muxing...")
if callable(vt.OnMultiplex): events.emit(events.Types.TRACK_MULTIPLEX, track=vt)
vt.OnMultiplex()
cl.extend([ cl.extend([
"--language", f"0:{vt.language}", "--language", f"0:{vt.language}",
"--default-track", f"0:{i == 0}", "--default-track", f"0:{i == 0}",
@ -350,8 +350,7 @@ class Tracks:
for i, at in enumerate(self.audio): for i, at in enumerate(self.audio):
if not at.path or not at.path.exists(): if not at.path or not at.path.exists():
raise ValueError("Audio Track must be downloaded before muxing...") raise ValueError("Audio Track must be downloaded before muxing...")
if callable(at.OnMultiplex): events.emit(events.Types.TRACK_MULTIPLEX, track=at)
at.OnMultiplex()
cl.extend([ cl.extend([
"--track-name", f"0:{at.get_track_name() or ''}", "--track-name", f"0:{at.get_track_name() or ''}",
"--language", f"0:{at.language}", "--language", f"0:{at.language}",
@ -365,8 +364,7 @@ class Tracks:
for st in self.subtitles: for st in self.subtitles:
if not st.path or not st.path.exists(): if not st.path or not st.path.exists():
raise ValueError("Text Track must be downloaded before muxing...") raise ValueError("Text Track must be downloaded before muxing...")
if callable(st.OnMultiplex): events.emit(events.Types.TRACK_MULTIPLEX, track=st)
st.OnMultiplex()
default = bool(self.audio and is_close_match(st.language, [self.audio[0].language]) and st.forced) default = bool(self.audio and is_close_match(st.language, [self.audio[0].language]) and st.forced)
cl.extend([ cl.extend([
"--track-name", f"0:{st.get_track_name() or ''}", "--track-name", f"0:{st.get_track_name() or ''}",