feat(binaries): Move all binary definitions to core/binaries file

This simplifies and centralizes all definitions on where these binaries can be found to a singular reference, making it easier to modify, edit, and improve.
This commit is contained in:
rlaphoenix 2024-04-24 05:07:25 +01:00
parent 9768de8bf2
commit 677fd9c56a
13 changed files with 91 additions and 61 deletions

View File

@ -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}")

View File

@ -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}")

View File

@ -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")
])

View File

@ -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_,

34
devine/core/binaries.py Normal file
View File

@ -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"
)

View File

@ -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,

View File

@ -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

View File

@ -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",

View File

@ -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.")

View File

@ -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",

View File

@ -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 []),

View File

@ -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",

View File

@ -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"