switch to uv instead of pipenv, and partially fix the code to work on windows.
This commit is contained in:
parent
e7256e65ac
commit
6d774a6d9c
14 changed files with 1219 additions and 1505 deletions
48
.github/workflows/update-yt-dlp.yml
vendored
48
.github/workflows/update-yt-dlp.yml
vendored
|
|
@ -1,48 +0,0 @@
|
||||||
name: update-yt-dlp
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
logLevel:
|
|
||||||
description: "Log level"
|
|
||||||
required: true
|
|
||||||
default: "warning"
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- info
|
|
||||||
- warning
|
|
||||||
- debug
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-yt-dlp:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: "dev"
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: "3.11"
|
|
||||||
- name: Update yt-dlp
|
|
||||||
id: ytdlp_update
|
|
||||||
run: |
|
|
||||||
pip install pipenv
|
|
||||||
pipenv sync
|
|
||||||
VER=`pipenv run pip list -o | awk '$1 == "yt-dlp" {print $3}'`
|
|
||||||
if [ -n "$VER" ]; then
|
|
||||||
echo "YTLDLP_VER=${VER}" >> "$GITHUB_OUTPUT"
|
|
||||||
pipenv update yt-dlp
|
|
||||||
fi
|
|
||||||
- name: Create Pull Request
|
|
||||||
uses: peter-evans/create-pull-request@v5
|
|
||||||
with:
|
|
||||||
title: "[yt-dlp] automated update to ${{ steps.ytdlp_update.outputs.YTLDLP_VER }}"
|
|
||||||
commit-message: "Update yt-dlp to ${{ steps.ytdlp_update.outputs.YTLDLP_VER }}"
|
|
||||||
body: "This is an automated request to update yt-dlp dependency to ${{ steps.ytdlp_update.outputs.YTLDLP_VER }}"
|
|
||||||
delete-branch: true
|
|
||||||
|
|
||||||
|
|
||||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
|
@ -15,9 +15,11 @@
|
||||||
"ahas",
|
"ahas",
|
||||||
"ahash",
|
"ahash",
|
||||||
"aiocron",
|
"aiocron",
|
||||||
|
"anyio",
|
||||||
"arrowless",
|
"arrowless",
|
||||||
"attl",
|
"attl",
|
||||||
"autonumber",
|
"autonumber",
|
||||||
|
"brotlicffi",
|
||||||
"consoletitle",
|
"consoletitle",
|
||||||
"cookiesfrombrowser",
|
"cookiesfrombrowser",
|
||||||
"copyts",
|
"copyts",
|
||||||
|
|
@ -53,15 +55,19 @@
|
||||||
"noprogress",
|
"noprogress",
|
||||||
"onefile",
|
"onefile",
|
||||||
"pathex",
|
"pathex",
|
||||||
|
"platformdirs",
|
||||||
"plexmatch",
|
"plexmatch",
|
||||||
"postprocessor",
|
"postprocessor",
|
||||||
"preferredcodec",
|
"preferredcodec",
|
||||||
"preferredquality",
|
"preferredquality",
|
||||||
"printtraffic",
|
"printtraffic",
|
||||||
|
"pycryptodome",
|
||||||
|
"pypi",
|
||||||
"pywebview",
|
"pywebview",
|
||||||
"quicktime",
|
"quicktime",
|
||||||
"rtime",
|
"rtime",
|
||||||
"smhd",
|
"smhd",
|
||||||
|
"socketio",
|
||||||
"timespec",
|
"timespec",
|
||||||
"tmpfilename",
|
"tmpfilename",
|
||||||
"upgrader",
|
"upgrader",
|
||||||
|
|
|
||||||
18
Dockerfile
18
Dockerfile
|
|
@ -14,15 +14,14 @@ ENV PYTHONFAULTHANDLER=1
|
||||||
ENV PIP_NO_CACHE_DIR=off
|
ENV PIP_NO_CACHE_DIR=off
|
||||||
ENV PIP_CACHE_DIR=/root/.cache/pip
|
ENV PIP_CACHE_DIR=/root/.cache/pip
|
||||||
|
|
||||||
# Use sed to strip carriage-return characters from the entrypoint script (in case building on Windows)
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN apk add --update coreutils curl gcc g++ musl-dev libffi-dev openssl-dev curl make && pip install pipenv
|
RUN apk add --update coreutils curl gcc g++ musl-dev libffi-dev openssl-dev curl make && pip install uv
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /opt/
|
||||||
|
|
||||||
ARG PIPENV_FLAGS="--deploy"
|
COPY ./pyproject.toml ./uv.lock ./
|
||||||
COPY ./Pipfile* .
|
RUN --mount=type=cache,target=/root/.cache/pip uv --no-cache venv --system-site-packages --relocatable ./python && \
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip PIPENV_VENV_IN_PROJECT=1 pipenv install ${PIPENV_FLAGS}
|
VIRTUAL_ENV=/opt/python uv sync --link-mode=copy --active
|
||||||
|
|
||||||
FROM python:3.13-alpine
|
FROM python:3.13-alpine
|
||||||
|
|
||||||
|
|
@ -51,16 +50,13 @@ RUN sed -i 's/\r$//g' /entrypoint.sh && chmod +x /entrypoint.sh
|
||||||
|
|
||||||
COPY --chown=app:app ./app /app/app
|
COPY --chown=app:app ./app /app/app
|
||||||
COPY --chown=app:app --from=node_builder /app/exported /app/ui/exported
|
COPY --chown=app:app --from=node_builder /app/exported /app/ui/exported
|
||||||
COPY --chown=app:app --from=python_builder /app/.venv /opt/python
|
COPY --chown=app:app --from=python_builder /opt/python /opt/python
|
||||||
COPY --chown=app:app --from=ghcr.io/arabcoders/alpine-mp4box /usr/bin/mp4box /usr/bin/mp4box
|
COPY --chown=app:app --from=ghcr.io/arabcoders/alpine-mp4box /usr/bin/mp4box /usr/bin/mp4box
|
||||||
COPY --chown=app:app ./healthcheck.sh /usr/local/bin/healthcheck
|
COPY --chown=app:app ./healthcheck.sh /usr/local/bin/healthcheck
|
||||||
|
|
||||||
ENV PATH="/opt/python/bin:$PATH"
|
ENV PATH="/opt/python/bin:$PATH"
|
||||||
|
|
||||||
RUN chown -R app:app /config /downloads && chmod +x /usr/local/bin/healthcheck && \
|
RUN chown -R app:app /config /downloads && chmod +x /usr/local/bin/healthcheck /usr/bin/mp4box
|
||||||
sed -i 's$#!\/app\/\.venv\/bin\/python$#!/opt/python/bin/python$' /opt/python/bin/* && \
|
|
||||||
sed -i "s%'\/app\/\.venv'%'/opt/python'%" /opt/python/bin/activate* && \
|
|
||||||
chmod +x /usr/bin/mp4box
|
|
||||||
|
|
||||||
VOLUME /config
|
VOLUME /config
|
||||||
VOLUME /downloads
|
VOLUME /downloads
|
||||||
|
|
|
||||||
32
Pipfile
32
Pipfile
|
|
@ -1,32 +0,0 @@
|
||||||
[[source]]
|
|
||||||
url = "https://pypi.org/simple"
|
|
||||||
verify_ssl = true
|
|
||||||
name = "pypi"
|
|
||||||
|
|
||||||
[packages]
|
|
||||||
python-socketio = ">=5.11.1"
|
|
||||||
aiohttp = ">=3.9.3"
|
|
||||||
caribou = ">=0.3.0"
|
|
||||||
coloredlogs = ">=15.0.1"
|
|
||||||
aiocron = ">=1.8"
|
|
||||||
python-dotenv = ">=1.0.1"
|
|
||||||
python-magic = ">=0.4.27"
|
|
||||||
debugpy = ">=1.8.1"
|
|
||||||
httpx = "*"
|
|
||||||
async-timeout = "*"
|
|
||||||
pyjson5 = "*"
|
|
||||||
curl_cffi = "0.7.1"
|
|
||||||
pysubs2 = "*"
|
|
||||||
regex = "*"
|
|
||||||
mutagen = "*"
|
|
||||||
brotli = "*"
|
|
||||||
brotlicffi = "*"
|
|
||||||
anyio = "*"
|
|
||||||
pycryptodome = "*"
|
|
||||||
yt-dlp = "*"
|
|
||||||
platformdirs = "*"
|
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
|
|
||||||
[requires]
|
|
||||||
python_version = "*"
|
|
||||||
1383
Pipfile.lock
generated
1383
Pipfile.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -226,13 +226,14 @@ class HttpAPI(Common):
|
||||||
HttpAPI: The instance of the HttpAPI.
|
HttpAPI: The instance of the HttpAPI.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
staticDir = os.path.join(self.rootPath, "ui", "exported")
|
staticDir = Path(os.path.join(self.rootPath, "ui", "exported")).absolute()
|
||||||
if not os.path.exists(staticDir):
|
if not staticDir.exists():
|
||||||
staticDir = os.path.join(self.rootPath, "..", "ui", "exported")
|
staticDir = Path(os.path.join(self.rootPath, "..", "ui", "exported")).absolute()
|
||||||
if not os.path.exists(staticDir):
|
if not staticDir.exists():
|
||||||
msg = f"Could not find the frontend UI static assets. '{staticDir}'."
|
msg = f"Could not find the frontend UI static assets. '{staticDir}'."
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
staticDir = str(staticDir.as_posix())
|
||||||
preloaded = 0
|
preloaded = 0
|
||||||
|
|
||||||
base_path: str = self.config.base_path.rstrip("/")
|
base_path: str = self.config.base_path.rstrip("/")
|
||||||
|
|
@ -242,7 +243,7 @@ class HttpAPI(Common):
|
||||||
if file.endswith(".map"):
|
if file.endswith(".map"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file = os.path.join(root, file)
|
file = str(Path(os.path.join(root, file)).as_posix())
|
||||||
urlPath: str = f"{base_path}/{file.replace(f'{staticDir}/', '')}"
|
urlPath: str = f"{base_path}/{file.replace(f'{staticDir}/', '')}"
|
||||||
|
|
||||||
with open(file, "rb") as f:
|
with open(file, "rb") as f:
|
||||||
|
|
@ -708,7 +709,7 @@ class HttpAPI(Common):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tasks.append(
|
tasks.append(
|
||||||
asyncio.get_event_loop().run_in_executor(None, lambda id=id, url=url: info_wrapper(id=id, url=url))
|
asyncio.get_event_loop().run_in_executor(None, lambda i=id, url=url: info_wrapper(id=i, url=url))
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(tasks) > 0:
|
if len(tasks) > 0:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import errno
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pty
|
|
||||||
import shlex
|
import shlex
|
||||||
import time
|
import time
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
@ -121,23 +120,47 @@ class HttpSocket(Common):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
master_fd, slave_fd = pty.openpty()
|
try:
|
||||||
|
import pty
|
||||||
|
|
||||||
|
master_fd, slave_fd = pty.openpty()
|
||||||
|
stdin_arg = asyncio.subprocess.DEVNULL
|
||||||
|
stdout_arg = stderr_arg = slave_fd
|
||||||
|
use_pty = True
|
||||||
|
except ImportError:
|
||||||
|
use_pty = False
|
||||||
|
master_fd = slave_fd = None
|
||||||
|
stdin_arg = asyncio.subprocess.DEVNULL
|
||||||
|
stdout_arg = asyncio.subprocess.PIPE
|
||||||
|
stderr_arg = asyncio.subprocess.STDOUT
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
*args,
|
*args,
|
||||||
cwd=self.config.download_path,
|
cwd=self.config.download_path,
|
||||||
stdin=asyncio.subprocess.DEVNULL,
|
stdin=stdin_arg,
|
||||||
stdout=slave_fd,
|
stdout=stdout_arg,
|
||||||
stderr=slave_fd,
|
stderr=stderr_arg,
|
||||||
env=_env,
|
env=_env,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
if use_pty:
|
||||||
os.close(slave_fd)
|
try:
|
||||||
except Exception as e:
|
os.close(slave_fd)
|
||||||
LOG.error(f"Error closing PTY. '{e!s}'.")
|
except Exception as e:
|
||||||
|
LOG.error(f"Error closing PTY. '{e!s}'.")
|
||||||
|
|
||||||
|
async def reader(sid: str):
|
||||||
|
if use_pty is False:
|
||||||
|
assert proc.stdout is not None
|
||||||
|
async for raw_line in proc.stdout:
|
||||||
|
line = raw_line.rstrip(b"\n")
|
||||||
|
await self._notify.emit(
|
||||||
|
Events.CLI_OUTPUT,
|
||||||
|
data={"type": "stdout", "line": line.decode("utf-8", errors="replace")},
|
||||||
|
to=sid,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
async def read_pty(sid: str):
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
buffer = b""
|
buffer = b""
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -172,7 +195,7 @@ class HttpSocket(Common):
|
||||||
LOG.error(f"Error closing PTY. '{e!s}'.")
|
LOG.error(f"Error closing PTY. '{e!s}'.")
|
||||||
|
|
||||||
# Start reading output from PTY
|
# Start reading output from PTY
|
||||||
read_task = asyncio.create_task(read_pty(sid=sid))
|
read_task = asyncio.create_task(reader(sid=sid))
|
||||||
|
|
||||||
# Wait until process finishes
|
# Wait until process finishes
|
||||||
returncode = await proc.wait()
|
returncode = await proc.wait()
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,9 @@ class Config:
|
||||||
_ytdlp_cli_mutable: str = ""
|
_ytdlp_cli_mutable: str = ""
|
||||||
"""The command line options to use for yt-dlp."""
|
"""The command line options to use for yt-dlp."""
|
||||||
|
|
||||||
|
is_native: bool = False
|
||||||
|
"Is the application running in webview."
|
||||||
|
|
||||||
pictures_backends: list[str] = [
|
pictures_backends: list[str] = [
|
||||||
"https://unsplash.it/1920/1080?random",
|
"https://unsplash.it/1920/1080?random",
|
||||||
"https://picsum.photos/1920/1080",
|
"https://picsum.photos/1920/1080",
|
||||||
|
|
@ -175,6 +178,7 @@ class Config:
|
||||||
"started",
|
"started",
|
||||||
"ytdlp_cli",
|
"ytdlp_cli",
|
||||||
"_ytdlp_cli_mutable",
|
"_ytdlp_cli_mutable",
|
||||||
|
"is_native",
|
||||||
)
|
)
|
||||||
"The variables that are immutable."
|
"The variables that are immutable."
|
||||||
|
|
||||||
|
|
@ -223,6 +227,7 @@ class Config:
|
||||||
"ytdlp_cli",
|
"ytdlp_cli",
|
||||||
"file_logging",
|
"file_logging",
|
||||||
"base_path",
|
"base_path",
|
||||||
|
"is_native",
|
||||||
)
|
)
|
||||||
"The variables that are relevant to the frontend."
|
"The variables that are relevant to the frontend."
|
||||||
|
|
||||||
|
|
@ -230,9 +235,9 @@ class Config:
|
||||||
"The manager instance."
|
"The manager instance."
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_instance():
|
def get_instance(is_native: bool = False) -> "Config":
|
||||||
"""Static access method."""
|
"""Static access method."""
|
||||||
return Config() if not Config.__instance else Config.__instance
|
return Config(is_native) if not Config.__instance else Config.__instance
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_manager() -> SyncManager:
|
def get_manager() -> SyncManager:
|
||||||
|
|
@ -241,7 +246,7 @@ class Config:
|
||||||
|
|
||||||
return Config._manager
|
return Config._manager
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, is_native: bool = False):
|
||||||
"""Virtually private constructor."""
|
"""Virtually private constructor."""
|
||||||
if Config.__instance is not None:
|
if Config.__instance is not None:
|
||||||
msg = "This class is a singleton. Use Config.get_instance() instead."
|
msg = "This class is a singleton. Use Config.get_instance() instead."
|
||||||
|
|
@ -251,12 +256,15 @@ class Config:
|
||||||
|
|
||||||
baseDefaultPath: str = str(Path(__file__).parent.parent.parent.absolute())
|
baseDefaultPath: str = str(Path(__file__).parent.parent.parent.absolute())
|
||||||
|
|
||||||
|
self.is_native = is_native
|
||||||
self.temp_path = os.environ.get("YTP_TEMP_PATH", None) or os.path.join(baseDefaultPath, "var", "tmp")
|
self.temp_path = os.environ.get("YTP_TEMP_PATH", None) or os.path.join(baseDefaultPath, "var", "tmp")
|
||||||
self.config_path = os.environ.get("YTP_CONFIG_PATH", None) or os.path.join(baseDefaultPath, "var", "config")
|
self.config_path = os.environ.get("YTP_CONFIG_PATH", None) or os.path.join(baseDefaultPath, "var", "config")
|
||||||
self.download_path = os.environ.get("YTP_DOWNLOAD_PATH", None) or os.path.join(
|
self.download_path = os.environ.get("YTP_DOWNLOAD_PATH", None) or os.path.join(
|
||||||
baseDefaultPath, "var", "downloads"
|
baseDefaultPath, "var", "downloads"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
conf_store = str(Path(self.config_path).as_posix())
|
||||||
|
|
||||||
envFile: str = os.path.join(self.config_path, ".env")
|
envFile: str = os.path.join(self.config_path, ".env")
|
||||||
|
|
||||||
if os.path.exists(envFile):
|
if os.path.exists(envFile):
|
||||||
|
|
@ -326,7 +334,7 @@ class Config:
|
||||||
LOG.error(f"Error starting debugpy server at '0.0.0.0:{self.debugpy_port}'. {e}")
|
LOG.error(f"Error starting debugpy server at '0.0.0.0:{self.debugpy_port}'. {e}")
|
||||||
|
|
||||||
ytdl_options = {}
|
ytdl_options = {}
|
||||||
opts_file: str = os.path.join(self.config_path, "ytdlp.cli")
|
opts_file: str = os.path.join(conf_store, "ytdlp.cli")
|
||||||
if os.path.exists(opts_file) and os.path.getsize(opts_file) > 2:
|
if os.path.exists(opts_file) and os.path.getsize(opts_file) > 2:
|
||||||
LOG.info(f"Loading yt-dlp custom options from '{opts_file}'.")
|
LOG.info(f"Loading yt-dlp custom options from '{opts_file}'.")
|
||||||
with open(opts_file) as f:
|
with open(opts_file) as f:
|
||||||
|
|
@ -358,7 +366,7 @@ class Config:
|
||||||
|
|
||||||
if self.keep_archive:
|
if self.keep_archive:
|
||||||
LOG.info("keep archive option is enabled.")
|
LOG.info("keep archive option is enabled.")
|
||||||
archive_file: str = os.path.join(self.config_path, "archive.log")
|
archive_file: str = os.path.join(conf_store, "archive.log")
|
||||||
self._ytdlp_cli_mutable += f"\n--download-archive {archive_file}"
|
self._ytdlp_cli_mutable += f"\n--download-archive {archive_file}"
|
||||||
|
|
||||||
if cookies_file := ytdl_options.get("cookiefile", None):
|
if cookies_file := ytdl_options.get("cookiefile", None):
|
||||||
|
|
@ -371,7 +379,7 @@ class Config:
|
||||||
ytdl_options.pop("cookiefile", None)
|
ytdl_options.pop("cookiefile", None)
|
||||||
|
|
||||||
if not ytdl_options.get("cookiefile", None):
|
if not ytdl_options.get("cookiefile", None):
|
||||||
cookies_file: str = os.path.join(self.config_path, "cookies.txt")
|
cookies_file: str = os.path.join(conf_store, "cookies.txt")
|
||||||
if os.path.exists(cookies_file) and os.path.getsize(cookies_file) > 2:
|
if os.path.exists(cookies_file) and os.path.getsize(cookies_file) > 2:
|
||||||
LOG.info(f"Using cookies from '{cookies_file}'.")
|
LOG.info(f"Using cookies from '{cookies_file}'.")
|
||||||
load_cookies(cookies_file)
|
load_cookies(cookies_file)
|
||||||
|
|
@ -392,12 +400,12 @@ class Config:
|
||||||
msg = f"Invalid file log level '{self.log_level_file}' specified."
|
msg = f"Invalid file log level '{self.log_level_file}' specified."
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
loggingPath = os.path.join(self.config_path, "logs")
|
loggingPath = os.path.join(conf_store, "logs")
|
||||||
if not os.path.exists(loggingPath):
|
if not os.path.exists(loggingPath):
|
||||||
os.makedirs(loggingPath, exist_ok=True)
|
os.makedirs(loggingPath, exist_ok=True)
|
||||||
|
|
||||||
handler = TimedRotatingFileHandler(
|
handler = TimedRotatingFileHandler(
|
||||||
filename=os.path.join(self.config_path, "logs", "app.log"),
|
filename=os.path.join(conf_store, "logs", "app.log"),
|
||||||
when="midnight",
|
when="midnight",
|
||||||
backupCount=3,
|
backupCount=3,
|
||||||
)
|
)
|
||||||
|
|
@ -406,7 +414,7 @@ class Config:
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
logging.getLogger().addHandler(handler)
|
logging.getLogger().addHandler(handler)
|
||||||
|
|
||||||
key_file: str = os.path.join(self.config_path, "secret.key")
|
key_file: str = os.path.join(conf_store, "secret.key")
|
||||||
|
|
||||||
if os.path.exists(key_file) and os.path.getsize(key_file) > 5:
|
if os.path.exists(key_file) and os.path.getsize(key_file) > 5:
|
||||||
with open(key_file, "rb") as f:
|
with open(key_file, "rb") as f:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import caribou
|
import caribou
|
||||||
|
|
@ -27,8 +28,8 @@ ROOT_PATH: Path = Path(__file__).parent.absolute()
|
||||||
|
|
||||||
|
|
||||||
class Main:
|
class Main:
|
||||||
def __init__(self):
|
def __init__(self, is_native: bool = False):
|
||||||
self._config = Config.get_instance()
|
self._config = Config.get_instance(is_native=is_native)
|
||||||
self._app = web.Application()
|
self._app = web.Application()
|
||||||
|
|
||||||
self._check_folders()
|
self._check_folders()
|
||||||
|
|
@ -118,7 +119,7 @@ class Main:
|
||||||
self._app,
|
self._app,
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
reuse_port=True,
|
reuse_port="win32" != sys.platform,
|
||||||
loop=asyncio.get_event_loop(),
|
loop=asyncio.get_event_loop(),
|
||||||
access_log=HTTP_LOGGER,
|
access_log=HTTP_LOGGER,
|
||||||
print=started,
|
print=started,
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ def app_start(host: str, port: int) -> None:
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
Main().start(host, port, cb=lambda: ready.set())
|
Main(is_native=True).start(host, port, cb=lambda: ready.set())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,41 @@
|
||||||
|
[project]
|
||||||
|
name = "ytptube"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "A WebUI for yt-dlp with concurrent downloads support, presets and scheduled tasks and many more."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
authors = [{ name = "Abdulmohsen", email = "contact@arabcoders.org" }]
|
||||||
|
dependencies = [
|
||||||
|
# Windows‐only
|
||||||
|
"tzdata; sys_platform == 'win32'",
|
||||||
|
"python-magic-bin; sys_platform == 'win32'",
|
||||||
|
# Non‐Windows
|
||||||
|
"python-magic>=0.4.27; sys_platform != 'win32'",
|
||||||
|
|
||||||
|
# Cross‐platform
|
||||||
|
"python-socketio>=5.11.1",
|
||||||
|
"aiohttp>=3.9.3",
|
||||||
|
"caribou>=0.3.0",
|
||||||
|
"coloredlogs>=15.0.1",
|
||||||
|
"aiocron>=1.8",
|
||||||
|
"python-dotenv>=1.0.1",
|
||||||
|
"debugpy>=1.8.1",
|
||||||
|
"httpx",
|
||||||
|
"async-timeout",
|
||||||
|
"pyjson5",
|
||||||
|
"curl_cffi==0.7.1",
|
||||||
|
"pysubs2",
|
||||||
|
"regex",
|
||||||
|
"mutagen",
|
||||||
|
"brotli",
|
||||||
|
"brotlicffi",
|
||||||
|
"anyio",
|
||||||
|
"pycryptodome",
|
||||||
|
"yt-dlp",
|
||||||
|
"platformdirs",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
# Exclude a variety of commonly ignored directories.
|
# Exclude a variety of commonly ignored directories.
|
||||||
exclude = [
|
exclude = [
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item is-hidden-mobile">
|
<div class="navbar-item is-hidden-mobile" v-if="false === config.app.is_native">
|
||||||
<button class="button is-dark" @click="reloadPage">
|
<button class="button is-dark" @click="reloadPage">
|
||||||
<span class="icon"><i class="fas fa-refresh" /></span>
|
<span class="icon"><i class="fas fa-refresh" /></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ const CONFIG_KEYS = {
|
||||||
browser_enabled: false,
|
browser_enabled: false,
|
||||||
ytdlp_cli: '',
|
ytdlp_cli: '',
|
||||||
file_logging: false,
|
file_logging: false,
|
||||||
|
is_native: false,
|
||||||
},
|
},
|
||||||
presets: [
|
presets: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue