diff --git a/devine/commands/dl.py b/devine/commands/dl.py index 61ae360..054c9d9 100644 --- a/devine/commands/dl.py +++ b/devine/commands/dl.py @@ -38,6 +38,7 @@ from rich.table import Table from rich.text import Text from rich.tree import Tree +from devine.core import binaries from devine.core.config import config from devine.core.console import console from devine.core.constants import DOWNLOAD_LICENCE_ONLY, AnyTrack, context_settings @@ -51,7 +52,7 @@ from devine.core.titles import Movie, Song, Title_T from devine.core.titles.episode import Episode from devine.core.tracks import Audio, Subtitle, Tracks, Video from devine.core.tracks.attachment import Attachment -from devine.core.utilities import get_binary_path, get_system_fonts, is_close_match, time_elapsed_since +from devine.core.utilities import get_system_fonts, is_close_match, time_elapsed_since from devine.core.utils.click_types import LANGUAGE_RANGE, QUALITY_LIST, SEASON_RANGE, ContextData, MultipleChoice from devine.core.utils.collections import merge_dict from devine.core.utils.subprocess import ffprobe @@ -198,7 +199,7 @@ class dl: self.proxy_providers.append(Basic(**config.proxy_providers["basic"])) if config.proxy_providers.get("nordvpn"): self.proxy_providers.append(NordVPN(**config.proxy_providers["nordvpn"])) - if get_binary_path("hola-proxy"): + if binaries.HolaProxy: self.proxy_providers.append(Hola()) for proxy_provider in self.proxy_providers: self.log.info(f"Loaded {proxy_provider.__class__.__name__}: {proxy_provider}") diff --git a/devine/commands/search.py b/devine/commands/search.py index 9f0b344..3be25fa 100644 --- a/devine/commands/search.py +++ b/devine/commands/search.py @@ -12,13 +12,13 @@ from rich.rule import Rule from rich.tree import Tree from devine.commands.dl import dl +from devine.core import binaries from devine.core.config import config from devine.core.console import console from devine.core.constants import context_settings from devine.core.proxies import Basic, Hola, NordVPN from devine.core.service import Service from devine.core.services import Services -from devine.core.utilities import get_binary_path from devine.core.utils.click_types import ContextData from devine.core.utils.collections import merge_dict @@ -72,7 +72,7 @@ def search( proxy_providers.append(Basic(**config.proxy_providers["basic"])) if config.proxy_providers.get("nordvpn"): proxy_providers.append(NordVPN(**config.proxy_providers["nordvpn"])) - if get_binary_path("hola-proxy"): + if binaries.HolaProxy: proxy_providers.append(Hola()) for proxy_provider in proxy_providers: log.info(f"Loaded {proxy_provider.__class__.__name__}: {proxy_provider}") diff --git a/devine/commands/serve.py b/devine/commands/serve.py index b025fa1..07690bc 100644 --- a/devine/commands/serve.py +++ b/devine/commands/serve.py @@ -2,9 +2,9 @@ import subprocess import click +from devine.core import binaries from devine.core.config import config from devine.core.constants import context_settings -from devine.core.utilities import get_binary_path @click.command( @@ -29,11 +29,10 @@ def serve(host: str, port: int, caddy: bool) -> None: from pywidevine import serve if caddy: - executable = get_binary_path("caddy") - if not executable: + if not binaries.Caddy: raise click.ClickException("Caddy executable \"caddy\" not found but is required for --caddy.") caddy_p = subprocess.Popen([ - executable, + binaries.Caddy, "run", "--config", str(config.directories.user_configs / "Caddyfile") ]) diff --git a/devine/commands/util.py b/devine/commands/util.py index 49b1f27..c328bac 100644 --- a/devine/commands/util.py +++ b/devine/commands/util.py @@ -4,8 +4,8 @@ from pathlib import Path import click from pymediainfo import MediaInfo +from devine.core import binaries from devine.core.constants import context_settings -from devine.core.utilities import get_binary_path @click.group(short_help="Various helper scripts and programs.", context_settings=context_settings) @@ -38,8 +38,7 @@ def crop(path: Path, aspect: str, letter: bool, offset: int, preview: bool) -> N as it may go from being 2px away from a perfect crop, to 20px over-cropping again due to sub-sampled chroma. """ - executable = get_binary_path("ffmpeg") - if not executable: + if not binaries.FFMPEG: raise click.ClickException("FFmpeg executable \"ffmpeg\" not found but is required.") if path.is_dir(): @@ -87,7 +86,7 @@ def crop(path: Path, aspect: str, letter: bool, offset: int, preview: bool) -> N ]))))] ffmpeg_call = subprocess.Popen([ - executable, "-y", + binaries.FFMPEG, "-y", "-i", str(video_path), "-map", "0:v:0", "-c", "copy", @@ -95,7 +94,7 @@ def crop(path: Path, aspect: str, letter: bool, offset: int, preview: bool) -> N ] + out_path, stdout=subprocess.PIPE) try: if preview: - previewer = get_binary_path("mpv", "ffplay") + previewer = binaries.MPV or binaries.FFPlay if not previewer: raise click.ClickException("MPV/FFplay executables weren't found but are required for previewing.") subprocess.Popen((previewer, "-"), stdin=ffmpeg_call.stdout) @@ -120,8 +119,7 @@ def range_(path: Path, full: bool, preview: bool) -> None: then you're video may have the range set to the wrong value. Flip its range to the opposite value and see if that fixes it. """ - executable = get_binary_path("ffmpeg") - if not executable: + if not binaries.FFMPEG: raise click.ClickException("FFmpeg executable \"ffmpeg\" not found but is required.") if path.is_dir(): @@ -157,7 +155,7 @@ def range_(path: Path, full: bool, preview: bool) -> None: ]))))] ffmpeg_call = subprocess.Popen([ - executable, "-y", + binaries.FFMPEG, "-y", "-i", str(video_path), "-map", "0:v:0", "-c", "copy", @@ -165,7 +163,7 @@ def range_(path: Path, full: bool, preview: bool) -> None: ] + out_path, stdout=subprocess.PIPE) try: if preview: - previewer = get_binary_path("mpv", "ffplay") + previewer = binaries.MPV or binaries.FFPlay if not previewer: raise click.ClickException("MPV/FFplay executables weren't found but are required for previewing.") subprocess.Popen((previewer, "-"), stdin=ffmpeg_call.stdout) @@ -188,8 +186,7 @@ def test(path: Path, map_: str) -> None: You may choose specific streams using the -m/--map parameter. E.g., '0:v:0' to test the first video stream, or '0:a' to test all audio streams. """ - executable = get_binary_path("ffmpeg") - if not executable: + if not binaries.FFMPEG: raise click.ClickException("FFmpeg executable \"ffmpeg\" not found but is required.") if path.is_dir(): @@ -199,7 +196,7 @@ def test(path: Path, map_: str) -> None: for video_path in paths: print("Starting...") p = subprocess.Popen([ - executable, "-hide_banner", + binaries.FFMPEG, "-hide_banner", "-benchmark", "-i", str(video_path), "-map", map_, diff --git a/devine/core/binaries.py b/devine/core/binaries.py new file mode 100644 index 0000000..096dde9 --- /dev/null +++ b/devine/core/binaries.py @@ -0,0 +1,34 @@ +import sys + +from devine.core.utilities import get_binary_path + +__shaka_platform = { + "win32": "win", + "darwin": "osx" +}.get(sys.platform, sys.platform) + +FFMPEG = get_binary_path("ffmpeg") +FFProbe = get_binary_path("ffprobe") +FFPlay = get_binary_path("ffplay") +SubtitleEdit = get_binary_path("SubtitleEdit") +ShakaPackager = get_binary_path( + "shaka-packager", + "packager", + f"packager-{__shaka_platform}", + f"packager-{__shaka_platform}-x64" +) +Aria2 = get_binary_path("aria2c", "aria2") +CCExtractor = get_binary_path( + "ccextractor", + "ccextractorwin", + "ccextractorwinfull" +) +HolaProxy = get_binary_path("hola-proxy") +MPV = get_binary_path("mpv") +Caddy = get_binary_path("caddy") + + +__all__ = ( + "FFMPEG", "FFProbe", "FFPlay", "SubtitleEdit", "ShakaPackager", + "Aria2", "CCExtractor", "HolaProxy", "MPV", "Caddy" +) diff --git a/devine/core/downloaders/aria2c.py b/devine/core/downloaders/aria2c.py index de4d1ef..3e234ee 100644 --- a/devine/core/downloaders/aria2c.py +++ b/devine/core/downloaders/aria2c.py @@ -15,10 +15,11 @@ from requests.cookies import cookiejar_from_dict, get_cookie_header from rich import filesize from rich.text import Text +from devine.core import binaries from devine.core.config import config from devine.core.console import console from devine.core.constants import DOWNLOAD_CANCELLED -from devine.core.utilities import get_binary_path, get_extension, get_free_port +from devine.core.utilities import get_extension, get_free_port def rpc(caller: Callable, secret: str, method: str, params: Optional[list[Any]] = None) -> Any: @@ -87,8 +88,7 @@ def download( if not isinstance(urls, list): urls = [urls] - executable = get_binary_path("aria2c", "aria2") - if not executable: + if not binaries.Aria2: raise EnvironmentError("Aria2c executable not found...") if proxy and not proxy.lower().startswith("http://"): @@ -186,7 +186,7 @@ def download( try: p = subprocess.Popen( [ - executable, + binaries.Aria2, *arguments ], stdin=subprocess.PIPE, diff --git a/devine/core/drm/widevine.py b/devine/core/drm/widevine.py index 266aa1e..0a46762 100644 --- a/devine/core/drm/widevine.py +++ b/devine/core/drm/widevine.py @@ -3,7 +3,6 @@ from __future__ import annotations import base64 import shutil import subprocess -import sys import textwrap from pathlib import Path from typing import Any, Callable, Optional, Union @@ -17,10 +16,11 @@ from pywidevine.pssh import PSSH from requests import Session from rich.text import Text +from devine.core import binaries from devine.core.config import config from devine.core.console import console from devine.core.constants import AnyTrack -from devine.core.utilities import get_binary_path, get_boxes +from devine.core.utilities import get_boxes from devine.core.utils.subprocess import ffprobe @@ -223,9 +223,7 @@ class Widevine: if not self.content_keys: raise ValueError("Cannot decrypt a Track without any Content Keys...") - platform = {"win32": "win", "darwin": "osx"}.get(sys.platform, sys.platform) - executable = get_binary_path("shaka-packager", "packager", f"packager-{platform}", f"packager-{platform}-x64") - if not executable: + if not binaries.ShakaPackager: raise EnvironmentError("Shaka Packager executable not found but is required.") if not path or not path.exists(): raise ValueError("Tried to decrypt a file that does not exist.") @@ -252,7 +250,7 @@ class Widevine: ] p = subprocess.Popen( - [executable, *arguments], + [binaries.ShakaPackager, *arguments], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, universal_newlines=True diff --git a/devine/core/manifests/hls.py b/devine/core/manifests/hls.py index f361c80..c477dcb 100644 --- a/devine/core/manifests/hls.py +++ b/devine/core/manifests/hls.py @@ -19,12 +19,13 @@ from pywidevine.cdm import Cdm as WidevineCdm from pywidevine.pssh import PSSH from requests import Session +from devine.core import binaries 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 +from devine.core.utilities import get_extension, is_close_match, try_ensure_utf8 class HLS: @@ -556,8 +557,7 @@ class HLS: Returns the file size of the merged file. """ - ffmpeg = get_binary_path("ffmpeg") - if not ffmpeg: + if not binaries.FFMPEG: raise EnvironmentError("FFmpeg executable was not found but is required to merge HLS segments.") demuxer_file = segments[0].parent / "ffmpeg_concat_demuxer.txt" @@ -567,7 +567,7 @@ class HLS: ])) subprocess.check_call([ - ffmpeg, "-hide_banner", + binaries.FFMPEG, "-hide_banner", "-loglevel", "panic", "-f", "concat", "-safe", "0", diff --git a/devine/core/proxies/hola.py b/devine/core/proxies/hola.py index 1be75cf..8ab1c12 100644 --- a/devine/core/proxies/hola.py +++ b/devine/core/proxies/hola.py @@ -3,8 +3,8 @@ import re import subprocess from typing import Optional +from devine.core import binaries from devine.core.proxies.proxy import Proxy -from devine.core.utilities import get_binary_path class Hola(Proxy): @@ -13,7 +13,7 @@ class Hola(Proxy): Proxy Service using Hola's direct connections via the hola-proxy project. https://github.com/Snawoot/hola-proxy """ - self.binary = get_binary_path("hola-proxy") + self.binary = binaries.HolaProxy if not self.binary: raise EnvironmentError("hola-proxy executable not found but is required for the Hola proxy provider.") diff --git a/devine/core/tracks/subtitle.py b/devine/core/tracks/subtitle.py index 6fadb27..866900a 100644 --- a/devine/core/tracks/subtitle.py +++ b/devine/core/tracks/subtitle.py @@ -17,8 +17,9 @@ from pycaption.geometry import Layout from pymp4.parser import MP4 from subtitle_filter import Subtitles +from devine.core import binaries from devine.core.tracks.track import Track -from devine.core.utilities import get_binary_path, try_ensure_utf8 +from devine.core.utilities import try_ensure_utf8 class Subtitle(Track): @@ -233,14 +234,13 @@ class Subtitle(Track): output_path = self.path.with_suffix(f".{codec.value.lower()}") - sub_edit_executable = get_binary_path("SubtitleEdit") - if sub_edit_executable and self.codec not in (Subtitle.Codec.fTTML, Subtitle.Codec.fVTT): + if binaries.SubtitleEdit and self.codec not in (Subtitle.Codec.fTTML, Subtitle.Codec.fVTT): sub_edit_format = { Subtitle.Codec.SubStationAlphav4: "AdvancedSubStationAlpha", Subtitle.Codec.TimedTextMarkupLang: "TimedText1.0" }.get(codec, codec.name) sub_edit_args = [ - sub_edit_executable, + binaries.SubtitleEdit, "/Convert", self.path, sub_edit_format, f"/outputfilename:{output_path.name}", "/encoding:utf8" @@ -500,8 +500,7 @@ class Subtitle(Track): if not self.path or not self.path.exists(): raise ValueError("You must download the subtitle track first.") - executable = get_binary_path("SubtitleEdit") - if executable: + if binaries.SubtitleEdit: if self.codec == Subtitle.Codec.SubStationAlphav4: output_format = "AdvancedSubStationAlpha" elif self.codec == Subtitle.Codec.TimedTextMarkupLang: @@ -510,7 +509,7 @@ class Subtitle(Track): output_format = self.codec.name subprocess.run( [ - executable, + binaries.SubtitleEdit, "/Convert", self.path, output_format, "/encoding:utf8", "/overwrite", @@ -539,8 +538,7 @@ class Subtitle(Track): if not self.path or not self.path.exists(): raise ValueError("You must download the subtitle track first.") - executable = get_binary_path("SubtitleEdit") - if not executable: + if not binaries.SubtitleEdit: raise EnvironmentError("SubtitleEdit executable not found...") if self.codec == Subtitle.Codec.SubStationAlphav4: @@ -552,7 +550,7 @@ class Subtitle(Track): subprocess.run( [ - executable, + binaries.SubtitleEdit, "/Convert", self.path, output_format, "/ReverseRtlStartEnd", "/encoding:utf8", diff --git a/devine/core/tracks/track.py b/devine/core/tracks/track.py index f91e678..740b98d 100644 --- a/devine/core/tracks/track.py +++ b/devine/core/tracks/track.py @@ -15,12 +15,13 @@ from zlib import crc32 from langcodes import Language from requests import Session +from devine.core import binaries 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.utilities import get_boxes, try_ensure_utf8 from devine.core.utils.subprocess import ffprobe @@ -470,8 +471,7 @@ class Track: if not self.path or not self.path.exists(): raise ValueError("Cannot repackage a Track that has not been downloaded.") - executable = get_binary_path("ffmpeg") - if not executable: + if not binaries.FFMPEG: raise EnvironmentError("FFmpeg executable \"ffmpeg\" was not found but is required for this call.") original_path = self.path @@ -480,7 +480,7 @@ class Track: def _ffmpeg(extra_args: list[str] = None): subprocess.run( [ - executable, "-hide_banner", + binaries.FFMPEG, "-hide_banner", "-loglevel", "error", "-i", original_path, *(extra_args or []), diff --git a/devine/core/tracks/video.py b/devine/core/tracks/video.py index dd147b4..df19511 100644 --- a/devine/core/tracks/video.py +++ b/devine/core/tracks/video.py @@ -10,10 +10,11 @@ from typing import Any, Optional, Union from langcodes import Language +from devine.core import binaries from devine.core.config import config from devine.core.tracks.subtitle import Subtitle from devine.core.tracks.track import Track -from devine.core.utilities import FPS, get_binary_path, get_boxes +from devine.core.utilities import FPS, get_boxes class Video(Track): @@ -257,8 +258,7 @@ class Video(Track): f"it's codec, {self.codec.value}, is not yet supported." ) - executable = get_binary_path("ffmpeg") - if not executable: + if not binaries.FFMPEG: raise EnvironmentError("FFmpeg executable \"ffmpeg\" was not found but is required for this call.") filter_key = { @@ -270,7 +270,7 @@ class Video(Track): output_path = original_path.with_stem(f"{original_path.stem}_{['limited', 'full'][range_]}_range") subprocess.run([ - executable, "-hide_banner", + binaries.FFMPEG, "-hide_banner", "-loglevel", "panic", "-i", original_path, "-codec", "copy", @@ -288,8 +288,7 @@ class Video(Track): if not self.path: raise ValueError("You must download the track first.") - executable = get_binary_path("ccextractor", "ccextractorwin", "ccextractorwinfull") - if not executable: + if not binaries.CCExtractor: raise EnvironmentError("ccextractor executable was not found.") # ccextractor often fails in weird ways unless we repack @@ -299,7 +298,7 @@ class Video(Track): try: subprocess.run([ - executable, + binaries.CCExtractor, "-trim", "-nobom", "-noru", "-ru1", @@ -380,8 +379,7 @@ class Video(Track): if not self.path or not self.path.exists(): raise ValueError("Cannot clean a Track that has not been downloaded.") - executable = get_binary_path("ffmpeg") - if not executable: + if not binaries.FFMPEG: raise EnvironmentError("FFmpeg executable \"ffmpeg\" was not found but is required for this call.") log = logging.getLogger("x264-clean") @@ -402,7 +400,7 @@ class Video(Track): original_path = self.path cleaned_path = original_path.with_suffix(f".cleaned{original_path.suffix}") subprocess.run([ - executable, "-hide_banner", + binaries.FFMPEG, "-hide_banner", "-loglevel", "panic", "-i", original_path, "-map_metadata", "-1", diff --git a/devine/core/utils/subprocess.py b/devine/core/utils/subprocess.py index 2f2561a..9106346 100644 --- a/devine/core/utils/subprocess.py +++ b/devine/core/utils/subprocess.py @@ -3,11 +3,16 @@ import subprocess from pathlib import Path from typing import Union +from devine.core import binaries + def ffprobe(uri: Union[bytes, Path]) -> dict: """Use ffprobe on the provided data to get stream information.""" + if not binaries.FFProbe: + raise EnvironmentError("FFProbe executable \"ffprobe\" not found but is required.") + args = [ - "ffprobe", + binaries.FFProbe, "-v", "quiet", "-of", "json", "-show_streams"