Add support for running YTPTube from sub-directory. Fix #295

This commit is contained in:
arabcoders 2025-06-05 21:04:00 +03:00
parent 18dea30719
commit 0f0dd51a41
14 changed files with 240 additions and 116 deletions

3
.vscode/launch.json vendored
View file

@ -50,6 +50,9 @@
"cwd": "${workspaceFolder}/ui",
"console": "internalConsole",
"outputCapture": "std",
"env": {
"APP_ENV": "production"
}
},
{
"name": "Python: main.py (Deep)",

View file

@ -124,29 +124,22 @@ other than the shortcut itself. this shortcut missing support for parsing the ht
It's advisable to run YTPTube behind a reverse proxy, if better authentication and/or HTTPS support are required.
### Nginx http server
```nginx
location /ytptube/ {
proxy_pass http://ytptube:8081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
```
> [!NOTE]
> The extra `proxy_set_header` directives are there to make web socket connection work.
### Caddy http server
## Caddy http server
The following example Caddyfile gets a reverse proxy going behind [caddy](https://caddyserver.com).
```caddyfile
# If you are using sub-domain.
youtube.example.com {
reverse_proxy ytptube:8081
}
# If you are using sub-folder, for example: https://example.com/ytptube/
# Make sure to change the `ytptube` to the name of your container.
# Also make sure to set the `YTP_BASE_PATH` environment variable to `/ytptube/`
example.com {
redir /ytptube /ytptube/
route /ytptube/* {
uri strip_prefix ytptube
reverse_proxy ytptube:8081
}
}
@ -317,4 +310,5 @@ Certain configuration values can be set via environment variables, using the `-e
| YTP_PICTURES_BACKENDS | A comma separated list of pictures urls to use. | `empty string` |
| YTP_BROWSER_ENABLED | Whether to enable the file browser | `false` |
| YTP_YTDLP_AUTO_UPDATE | Whether to enable the auto update for yt-dlp | `true` |
| YTP_BASE_PATH | Set this if you are serving YTPTube from sub-folder | `/` |

View file

@ -11,7 +11,7 @@ import uuid
from collections.abc import Awaitable
from datetime import UTC, datetime, timedelta
from pathlib import Path
from urllib.parse import quote, unquote_plus, urlparse
from urllib.parse import unquote_plus, urlparse
import anyio
import httpx
@ -70,7 +70,7 @@ class HttpAPI(Common):
}
"""Map ext to mimetype"""
_frontend_routes = [
_frontend_routes: list[str] = [
"/console",
"/presets",
"/tasks",
@ -97,17 +97,19 @@ class HttpAPI(Common):
self.rootPath = str(Path(__file__).parent.parent.parent)
self.routes = web.RouteTableDef()
self.cache = Cache()
self.app: web.Application | None = None
super().__init__(queue=self.queue, encoder=self.encoder, config=self.config)
@staticmethod
def route(method: str, path: str) -> Awaitable:
def route(method: str, path: str, name: str | None = None) -> Awaitable:
"""
Decorator to mark a method as an HTTP route handler.
Args:
method (str): The HTTP method.
path (str): The path to the route.
name (str | None): The name of the route (optional).
Returns:
Awaitable: The decorated function.
@ -121,6 +123,7 @@ class HttpAPI(Common):
wrapper._http_method = method.upper()
wrapper._http_path = path
wrapper._http_name = name
return wrapper
return decorator
@ -139,7 +142,16 @@ class HttpAPI(Common):
HttpAPI: The instance of the HttpAPI.
"""
app.middlewares.append(HttpAPI.middle_wares(download_path=self.config.download_path))
self.app = app
app.middlewares.append(
HttpAPI.middle_wares(
app=app,
base_path=self.config.base_path.rstrip("/"),
download_path=self.config.download_path,
)
)
if self.config.auth_username and self.config.auth_password:
app.middlewares.append(HttpAPI.basic_auth(self.config.auth_username, self.config.auth_password))
@ -176,6 +188,9 @@ class HttpAPI(Common):
"""
path = req.path
if "/" != self.config.base_path and path.rstrip("/") == self.config.base_path.rstrip("/"):
path: str = f"{self.config.base_path.rstrip('/')}/index.html"
if path not in self._static_holder:
for k in self._static_holder:
if path.startswith(k):
@ -217,13 +232,15 @@ class HttpAPI(Common):
preloaded = 0
base_path: str = self.config.base_path.rstrip("/")
for root, _, files in os.walk(staticDir):
for file in files:
if file.endswith(".map"):
continue
file = os.path.join(root, file)
urlPath = f"/{file.replace(f'{staticDir}/', '')}"
urlPath: str = f"{base_path}/{file.replace(f'{staticDir}/', '')}"
with open(file, "rb") as f:
content = f.read()
@ -237,6 +254,7 @@ class HttpAPI(Common):
if urlPath.endswith("/index.html"):
for path in self._frontend_routes:
path: str = f"{base_path}/{path.lstrip('/')}"
self._static_holder[path] = {"content": content, "content_type": contentType}
app.router.add_get(path, self._static_file)
if "{" not in path:
@ -255,6 +273,13 @@ class HttpAPI(Common):
LOG.info(f"Preloaded '{preloaded}' static files.")
if "/" != self.config.base_path:
LOG.debug(f"adding base_path folder '{self.config.base_path}' to routes.")
app.router.add_get(self.config.base_path.rstrip("/"), self._static_file, name="base_path_static")
app.router.add_get(
f"{self.config.base_path.rstrip('/')}/", self._static_file, name="base_path_static_slash"
)
return self
def add_routes(self, app: web.Application) -> "HttpAPI":
@ -268,7 +293,9 @@ class HttpAPI(Common):
HttpAPI: The instance of the HttpAPI.
"""
registered_options = []
registered_options: list = []
base_path: str = self.config.base_path.rstrip("/")
for attr_name in dir(self):
method = getattr(self, attr_name)
@ -277,7 +304,12 @@ class HttpAPI(Common):
if http_path.startswith("/"):
http_path = method._http_path[1:]
self.routes.route(method._http_method, f"/{http_path}")(method)
opts = {}
if hasattr(method, "_http_name") and method._http_name:
opts["name"] = method._http_name
LOG.debug(f"Registering route {method._http_method} {base_path}/{http_path}' {opts}.")
self.routes.route(method._http_method, f"{base_path}/{http_path}", **opts)(method)
if http_path in registered_options:
continue
@ -285,10 +317,13 @@ class HttpAPI(Common):
async def options_handler(_: Request) -> Response:
return web.Response(status=204)
self.routes.route("OPTIONS", f"/{http_path}")(options_handler)
if "name" in opts:
opts["name"] = f"{opts['name']}_options"
self.routes.route("OPTIONS", f"{base_path}/{http_path}", **opts)(options_handler)
registered_options.append(http_path)
self.routes.static("/api/download/", self.config.download_path)
self.routes.static(f"{base_path}/api/download/", self.config.download_path, name="download_static")
self._preload_static(app)
try:
@ -386,19 +421,24 @@ class HttpAPI(Common):
return middleware_handler
@staticmethod
def middle_wares(download_path: str) -> Awaitable:
def middle_wares(app: web.Application, base_path: str, download_path: str) -> Awaitable:
@web.middleware
async def middleware_handler(request: Request, handler: RequestHandler) -> Response:
if request.path.startswith("/api/download"):
static_path = str(app.router["download_static"].url_for(filename=""))
if request.path.startswith(static_path):
realFile, status = get_file(
download_path=download_path,
file=request.path.replace("/api/download/", ""),
file=request.path.replace(static_path, ""),
)
if web.HTTPFound.status_code == status:
return Response(
status=status,
headers={
"Location": f"/api/download/{quote(str(realFile).replace(download_path, '').strip('/'))}"
"Location": str(
app.router["download_static"].url_for(
filename=str(realFile).replace(download_path, "").strip("/")
)
)
},
)
@ -413,6 +453,17 @@ class HttpAPI(Common):
status=web.HTTPInternalServerError.status_code,
)
contentType: str | None = response.headers.get("content-type", None)
if contentType and "/" != base_path and contentType.startswith("text/html"):
rewrite_path: str = base_path.rstrip("/")
content: str = (
response.body.decode("utf-8")
.replace('<base href="/">', f'<base href="{rewrite_path}/">')
.replace('baseURL:""', f'baseURL:"{rewrite_path}/"')
)
response.body = content.encode("utf-8")
if isinstance(response, web.FileResponse):
try:
ff_info = await ffprobe(response._path)
@ -425,7 +476,7 @@ class HttpAPI(Common):
return middleware_handler
@route("GET", "api/ping")
@route("GET", "api/ping", "ping")
async def ping(self, _: Request) -> Response:
"""
Ping the server.
@ -440,7 +491,7 @@ class HttpAPI(Common):
await self.queue.test()
return web.json_response(data={"status": "pong"}, status=web.HTTPOk.status_code)
@route("POST", "api/yt-dlp/convert")
@route("POST", "api/yt-dlp/convert", "yt_dlp_convert")
async def yt_dlp_convert(self, request: Request) -> Response:
"""
Convert the yt-dlp args to a dict.
@ -495,7 +546,7 @@ class HttpAPI(Common):
status=web.HTTPBadRequest.status_code,
)
@route("GET", "api/yt-dlp/url/info")
@route("GET", "api/yt-dlp/url/info", "get_info")
async def get_info(self, request: Request) -> Response:
"""
Get the video info.
@ -588,7 +639,7 @@ class HttpAPI(Common):
status=web.HTTPInternalServerError.status_code,
)
@route("GET", "api/yt-dlp/archive/recheck")
@route("GET", "api/yt-dlp/archive/recheck", "archive_recheck")
async def archive_recheck(self, _) -> Response:
"""
Recheck the manual archive entries.
@ -669,7 +720,7 @@ class HttpAPI(Common):
return web.json_response(data=response, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
@route("GET", "api/history/add")
@route("GET", "api/history/add", "quick_add")
async def quick_add(self, request: Request) -> Response:
"""
Add a URL to the download queue.
@ -710,7 +761,7 @@ class HttpAPI(Common):
dumps=self.encoder.encode,
)
@route("POST", "api/history")
@route("POST", "api/history", "history_item_add")
async def history_item_add(self, request: Request) -> Response:
"""
Add a URL to the download queue.
@ -744,7 +795,7 @@ class HttpAPI(Common):
return web.json_response(data=response, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
@route("GET", "api/logs")
@route("GET", "api/logs", "logs")
async def logs(self, request: Request) -> Response:
"""
Get recent logs
@ -783,7 +834,7 @@ class HttpAPI(Common):
dumps=self.encoder.encode,
)
@route("GET", "api/conditions")
@route("GET", "api/conditions", "conditions")
async def conditions(self, _: Request) -> Response:
"""
Get the conditions
@ -801,7 +852,7 @@ class HttpAPI(Common):
dumps=self.encoder.encode,
)
@route("PUT", "api/conditions")
@route("PUT", "api/conditions", "conditions_add")
async def conditions_add(self, request: Request) -> Response:
"""
Save Conditions.
@ -872,7 +923,7 @@ class HttpAPI(Common):
await self._notify.emit(Events.CONDITIONS_UPDATE, data=items)
return web.json_response(data=items, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
@route("POST", "api/conditions/test")
@route("POST", "api/conditions/test", "conditions_test")
async def conditions_test(self, request: Request) -> Response:
"""
Test condition against URL.
@ -949,7 +1000,7 @@ class HttpAPI(Common):
dumps=self.encoder.encode,
)
@route("GET", "api/presets")
@route("GET", "api/presets", "presets")
async def presets(self, request: Request) -> Response:
"""
Get the presets.
@ -970,7 +1021,7 @@ class HttpAPI(Common):
return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
@route("PUT", "api/presets")
@route("PUT", "api/presets", "presets_add")
async def presets_add(self, request: Request) -> Response:
"""
Add presets.
@ -1030,7 +1081,7 @@ class HttpAPI(Common):
await self._notify.emit(Events.PRESETS_UPDATE, data=presets)
return web.json_response(data=presets, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
@route("GET", "api/tasks")
@route("GET", "api/tasks", "tasks")
async def tasks(self, _: Request) -> Response:
"""
Get the tasks.
@ -1046,7 +1097,7 @@ class HttpAPI(Common):
data=Tasks.get_instance().get_all(), status=web.HTTPOk.status_code, dumps=self.encoder.encode
)
@route("PUT", "api/tasks")
@route("PUT", "api/tasks", "tasks_add")
async def tasks_add(self, request: Request) -> Response:
"""
Add tasks to the queue.
@ -1112,7 +1163,7 @@ class HttpAPI(Common):
return web.json_response(data=tasks, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
@route("DELETE", "api/history")
@route("DELETE", "api/history", "history_delete")
async def history_delete(self, request: Request) -> Response:
"""
Delete an item from the queue.
@ -1140,7 +1191,7 @@ class HttpAPI(Common):
dumps=self.encoder.encode,
)
@route("POST", "api/history/{id}")
@route("POST", "api/history/{id}", "history_item_update")
async def history_item_update(self, request: Request) -> Response:
"""
Update an item in the history.
@ -1187,7 +1238,7 @@ class HttpAPI(Common):
dumps=self.encoder.encode,
)
@route("GET", "api/history")
@route("GET", "api/history", "history")
async def history(self, _: Request) -> Response:
"""
Get the history.
@ -1207,7 +1258,7 @@ class HttpAPI(Common):
return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
@route("GET", "api/workers")
@route("GET", "api/workers", "pool_list")
async def pool_list(self, _) -> Response:
"""
Get the workers status.
@ -1245,7 +1296,7 @@ class HttpAPI(Common):
dumps=lambda obj: json.dumps(obj, default=lambda o: f"<<non-serializable: {type(o).__qualname__}>>"),
)
@route("POST", "api/workers")
@route("POST", "api/workers", "pool_start")
async def pool_restart(self, _) -> Response:
"""
Restart the workers pool.
@ -1264,7 +1315,7 @@ class HttpAPI(Common):
return web.json_response({"message": "Workers pool being restarted."}, status=web.HTTPOk.status_code)
@route("PATCH", "api/workers/{id}")
@route("PATCH", "api/workers/{id}", "worker_restart")
async def worker_restart(self, request: Request) -> Response:
"""
Restart a worker.
@ -1287,7 +1338,7 @@ class HttpAPI(Common):
return web.json_response({"status": "restarted" if status else "in_error_state"}, status=web.HTTPOk.status_code)
@route("DELETE", "api/workers/{id}")
@route("DELETE", "api/workers/{id}", "worker_stop")
async def worker_stop(self, request: Request) -> Response:
"""
Stop a worker.
@ -1310,7 +1361,7 @@ class HttpAPI(Common):
return web.json_response({"status": "stopped" if status else "in_error_state"}, status=web.HTTPOk.status_code)
@route("GET", "api/player/playlist/{file:.*}.m3u8")
@route("GET", "api/player/playlist/{file:.*}.m3u8", "playlist")
async def playlist(self, request: Request) -> Response:
"""
Get the playlist.
@ -1327,13 +1378,19 @@ class HttpAPI(Common):
if not file:
return web.json_response(data={"error": "file is required"}, status=web.HTTPBadRequest.status_code)
base_path: str = self.config.base_path.rstrip("/")
try:
realFile, status = get_file(download_path=self.config.download_path, file=file)
if web.HTTPFound.status_code == status:
return Response(
status=web.HTTPFound.status_code,
headers={
"Location": f"/api/player/playlist/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}.m3u8"
"Location": str(
self.app.router["playlist"].url_for(
file=str(realFile).replace(self.config.download_path, "").strip("/")
)
),
},
)
@ -1341,7 +1398,7 @@ class HttpAPI(Common):
return web.json_response(data={"error": f"File '{file}' does not exist."}, status=status)
return web.Response(
text=await Playlist(download_path=self.config.download_path, url="/").make(file=realFile),
text=await Playlist(download_path=self.config.download_path, url=f"{base_path}/").make(file=realFile),
headers={
"Content-Type": "application/x-mpegURL",
"Cache-Control": "no-cache",
@ -1352,7 +1409,7 @@ class HttpAPI(Common):
except StreamingError as e:
return web.json_response(data={"error": str(e)}, status=web.HTTPNotFound.status_code)
@route("GET", "api/player/m3u8/{mode}/{file:.*}.m3u8")
@route("GET", "api/player/m3u8/{mode}/{file:.*}.m3u8", "m3u8")
async def m3u8(self, request: Request) -> Response:
"""
Get the m3u8 file.
@ -1383,15 +1440,22 @@ class HttpAPI(Common):
duration = float(duration)
base_path: str = self.config.base_path.rstrip("/")
try:
cls = M3u8(download_path=self.config.download_path, url="/")
cls = M3u8(download_path=self.config.download_path, url=f"{base_path}/")
realFile, status = get_file(download_path=self.config.download_path, file=file)
if web.HTTPFound.status_code == status:
return Response(
status=status,
headers={
"Location": f"/api/player/m3u8/{mode}/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}.m3u8"
"Location": str(
self.app.router["m3u8"].url_for(
mode=mode,
file=str(realFile).replace(self.config.download_path, "").strip("/"),
)
),
},
)
@ -1416,7 +1480,7 @@ class HttpAPI(Common):
status=web.HTTPOk.status_code,
)
@route("GET", r"api/player/segments/{segment:\d+}/{file:.*}.ts")
@route("GET", r"api/player/segments/{segment:\d+}/{file:.*}.ts", "segments")
async def segments(self, request: Request) -> Response:
"""
Get the segments.
@ -1445,7 +1509,12 @@ class HttpAPI(Common):
return Response(
status=status,
headers={
"Location": f"/api/player/segments/{segment}/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}.ts"
"Location": str(
self.app.router["segments"].url_for(
segment=segment,
file=str(realFile).replace(self.config.download_path, "").strip("/"),
)
),
},
)
@ -1487,7 +1556,7 @@ class HttpAPI(Common):
return resp
@route("GET", "api/player/subtitle/{file:.*}.vtt")
@route("GET", "api/player/subtitle/{file:.*}.vtt", "subtitles")
async def subtitles(self, request: Request) -> Response:
"""
Get the subtitles.
@ -1509,7 +1578,11 @@ class HttpAPI(Common):
return Response(
status=status,
headers={
"Location": f"/api/player/subtitle/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}.vtt"
"Location": str(
self.app.router["subtitles"].url_for(
file=str(realFile).replace(self.config.download_path, "").strip("/")
)
),
},
)
@ -1540,7 +1613,7 @@ class HttpAPI(Common):
status=web.HTTPOk.status_code,
)
@route("GET", "/")
@route("GET", "/", "index")
async def index(self, _: Request) -> Response:
"""
Get the index file.
@ -1566,7 +1639,7 @@ class HttpAPI(Common):
status=web.HTTPOk.status_code,
)
@route("GET", "api/thumbnail")
@route("GET", "api/thumbnail", "get_thumbnail")
async def get_thumbnail(self, request: Request) -> Response:
"""
Get the thumbnail.
@ -1620,7 +1693,7 @@ class HttpAPI(Common):
data={"error": "failed to retrieve the thumbnail."}, status=web.HTTPInternalServerError.status_code
)
@route("GET", "api/random/background")
@route("GET", "api/random/background", "get_background")
async def get_background(self, request: Request) -> Response:
"""
Get random background.
@ -1694,7 +1767,7 @@ class HttpAPI(Common):
status=web.HTTPInternalServerError.status_code,
)
@route("GET", "api/file/ffprobe/{file:.*}")
@route("GET", "api/file/ffprobe/{file:.*}", "ffprobe")
async def get_ffprobe(self, request: Request) -> Response:
"""
Get the ffprobe data.
@ -1716,7 +1789,11 @@ class HttpAPI(Common):
return Response(
status=web.HTTPFound.status_code,
headers={
"Location": f"/api/file/ffprobe/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}"
"Location": str(
self.app.router["ffprobe"].url_for(
file=str(realFile).replace(self.config.download_path, "").strip("/")
)
),
},
)
@ -1729,7 +1806,7 @@ class HttpAPI(Common):
except Exception as e:
return web.json_response(data={"error": str(e)}, status=web.HTTPInternalServerError.status_code)
@route("GET", "api/file/info/{file:.*}")
@route("GET", "api/file/info/{file:.*}", "file_info")
async def get_file_info(self, request: Request) -> Response:
"""
Get file info
@ -1751,7 +1828,11 @@ class HttpAPI(Common):
return Response(
status=web.HTTPFound.status_code,
headers={
"Location": f"/api/file/info/{quote(str(realFile).replace(self.config.download_path, '').strip('/'))}"
"Location": str(
self.app.router["file_info"].url_for(
file=str(realFile).replace(self.config.download_path, "").strip("/")
)
),
},
)
@ -1780,7 +1861,7 @@ class HttpAPI(Common):
LOG.exception(e)
return web.json_response(data={"error": str(e)}, status=web.HTTPInternalServerError.status_code)
@route("GET", "api/notifications")
@route("GET", "api/notifications", "notifications")
async def notifications(self, _: Request) -> Response:
"""
Get the notification targets.
@ -1801,7 +1882,7 @@ class HttpAPI(Common):
dumps=self.encoder.encode,
)
@route("PUT", "api/notifications")
@route("PUT", "api/notifications", "notification_add")
async def notification_add(self, request: Request) -> Response:
"""
Add notification targets.
@ -1857,7 +1938,7 @@ class HttpAPI(Common):
return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
@route("POST", "api/notifications/test")
@route("POST", "api/notifications/test", "notification_test")
async def notification_test(self, _: Request) -> Response:
"""
Test the notification.
@ -1875,7 +1956,7 @@ class HttpAPI(Common):
return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
@route("GET", "api/file/browser/{path:.*}")
@route("GET", "api/file/browser/{path:.*}", "file_browser")
async def file_browser(self, request: Request) -> Response:
"""
Get the file browser.

View file

@ -50,6 +50,7 @@ class HttpSocket(Common):
self.queue = queue or DownloadQueue.get_instance()
self._notify = EventBus.get_instance()
#logger=True, engineio_logger=True,
self.sio = sio or socketio.AsyncServer(cors_allowed_origins="*")
encoder = encoder or Encoder()
@ -77,7 +78,7 @@ class HttpSocket(Common):
LOG.debug("Shutting down socket server.")
def attach(self, app: web.Application):
self.sio.attach(app, socketio_path=self.config.url_socketio)
self.sio.attach(app, socketio_path=f"{self.config.base_path.rstrip('/')}/socket.io")
for attr_name in dir(self):
method = getattr(self, attr_name)

View file

@ -969,24 +969,28 @@ async def tail_log(file: str, emitter: callable, sleep_time: float = 0.5):
if not pathlib.Path(file).exists():
return
async with await open_file(file, "rb") as f:
await f.seek(0, os.SEEK_END)
while True:
line = await f.readline()
if not line:
await asyncio_sleep(sleep_time)
continue
try:
async with await open_file(file, "rb") as f:
await f.seek(0, os.SEEK_END)
while True:
line = await f.readline()
if not line:
await asyncio_sleep(sleep_time)
continue
msg = line.decode(errors="replace")
dt_match = DATETIME_PATTERN.match(msg)
msg = line.decode(errors="replace")
dt_match = DATETIME_PATTERN.match(msg)
await emitter(
{
"id": sha256(line if isinstance(line, bytes) else line.encode()).hexdigest(),
"line": msg[dt_match.end() :] if dt_match else msg,
"datetime": dt_match.group(1) if dt_match else None,
}
)
await emitter(
{
"id": sha256(line if isinstance(line, bytes) else line.encode()).hexdigest(),
"line": msg[dt_match.end() :] if dt_match else msg,
"datetime": dt_match.group(1) if dt_match else None,
}
)
except Exception as e:
LOG.error(f"Error while tailing log file '{file!s}': {e!s}")
return
def load_cookies(file: str) -> tuple[bool, MozillaCookieJar]:

View file

@ -28,9 +28,6 @@ class Config:
temp_keep: bool = False
"""Keep temporary files after the download is complete."""
url_socketio: str = "/socket.io"
"""The URL to use for the socket.io server."""
output_template: str = "%(title)s.%(ext)s"
"""The output template to use for the downloaded files."""
@ -55,11 +52,15 @@ class Config:
log_level_file: str = "info"
"""The log level to use for the file logging."""
base_path: str = "/"
"""The base path to use for the application."""
max_workers: int = 1
"""The maximum number of workers to use for downloading."""
streamer_vcodec: str = "libx264"
"""The video codec to use for streaming."""
streamer_acodec: str = "aac"
"""The audio codec to use for streaming."""
@ -221,6 +222,7 @@ class Config:
"browser_enabled",
"ytdlp_cli",
"file_logging",
"base_path",
)
"The variables that are relevant to the frontend."

View file

@ -23,6 +23,7 @@ from library.Tasks import Tasks
LOG = logging.getLogger("app")
MIME = magic.Magic(mime=True)
class Main:
def __init__(self):
self._config = Config.get_instance()
@ -99,14 +100,16 @@ class Main:
def started(_):
LOG.info("=" * 40)
LOG.info(f"YTPTube v{self._config.version} - started on http://{self._config.host}:{self._config.port}")
LOG.info(
f"YTPTube v{self._config.version} - started on http://{self._config.host}:{self._config.port}{self._config.base_path}"
)
LOG.info("=" * 40)
HTTP_LOGGER = None
if self._config.access_log:
from library.HttpAPI import LOG as HTTP_LOGGER
HTTP_LOGGER.addFilter(lambda record: "GET /api/ping" not in record.getMessage())
HTTP_LOGGER.addFilter(lambda record: f"GET {self._app.router['ping'].url_for()}" not in record.getMessage())
web.run_app(
self._app,

View file

@ -108,7 +108,7 @@
</td>
<td class="is-vcentered">
<div v-if="item.extras.thumbnail">
<FloatingImage :image="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
<FloatingImage :image="uri('/api/thumbnail?url=' + encodePath(item.extras.thumbnail))"
:title="item.title">
<div class="is-text-overflow">
<NuxtLink target="_blank" :href="item.url">{{ item.title }}</NuxtLink>
@ -232,19 +232,19 @@
<figure class="image is-3by1">
<span v-if="'finished' === item.status && item.filename" @click="playVideo(item)" class="play-overlay">
<div class="play-icon"></div>
<img @load="e => pImg(e)" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
<img @load="e => pImg(e)" :src="uri('/api/thumbnail?url=' + encodePath(item.extras.thumbnail))"
v-if="item.extras?.thumbnail" />
<img v-else src="/images/placeholder.png" />
</span>
<span v-else-if="isEmbedable(item.url)" @click="embed_url = getEmbedable(item.url)" class="play-overlay">
<div class="play-icon embed-icon"></div>
<img @load="e => pImg(e)" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
<img @load="e => pImg(e)" :src="uri('/api/thumbnail?url=' + encodePath(item.extras.thumbnail))"
v-if="item.extras?.thumbnail" />
<img v-else src="/images/placeholder.png" />
</span>
<template v-else>
<img @load="e => pImg(e)" v-if="item.extras?.thumbnail"
:src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)" />
:src="uri('/api/thumbnail?url=' + encodePath(item.extras.thumbnail))" />
<img v-else src="/images/placeholder.png" />
</template>
</figure>
@ -378,7 +378,7 @@
<script setup>
import moment from 'moment'
import { useStorage } from '@vueuse/core'
import { makeDownload, formatBytes } from '~/utils/index'
import { makeDownload, formatBytes, uri } from '~/utils/index'
import { isEmbedable, getEmbedable } from '~/utils/embedable'
const emitter = defineEmits(['getInfo', 'add_new'])

View file

@ -65,8 +65,20 @@
:id="'checkbox-' + item._id" :value="item._id">
</label>
</td>
<td class="is-text-overflow is-vcentered" v-tooltip="item.title">
<NuxtLink target="_blank" :href="item.url">{{ item.title }}</NuxtLink>
<td class="is-text-overflow is-vcentered">
<div v-if="item.extras?.thumbnail">
<FloatingImage :image="uri('/api/thumbnail?url=' + encodePath(item.extras.thumbnail))"
:title="item.title">
<div class="is-text-overflow">
<NuxtLink target="_blank" :href="item.url">{{ item.title }}</NuxtLink>
</div>
</FloatingImage>
</div>
<template v-else>
<div class="is-text-overflow" v-tooltip="item.title">
<NuxtLink target="_blank" :href="item.url">{{ item.title }}</NuxtLink>
</div>
</template>
</td>
<td class="has-text-centered is-text-overflow is-unselectable">
<span class="icon-text">
@ -137,13 +149,13 @@
<figure class="image is-3by1">
<span v-if="isEmbedable(item.url)" @click="embed_url = getEmbedable(item.url)" class="play-overlay">
<div class="play-icon embed-icon"></div>
<img @load="e => pImg(e)" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
<img @load="e => pImg(e)" :src="uri('/api/thumbnail?url=' + encodePath(item.extras.thumbnail))"
v-if="item.extras?.thumbnail" />
<img v-else src="/images/placeholder.png" />
</span>
<template v-else>
<img @load="e => pImg(e)" v-if="item.extras?.thumbnail"
:src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)" />
:src="uri('/api/thumbnail?url=' + encodePath(item.extras.thumbnail))" />
<img v-else src="/images/placeholder.png" />
</template>
</figure>

View file

@ -10,7 +10,7 @@
</style>
<template>
<div v-if="infoLoaded">
<video class="player" ref="video" :poster="thumbnail" :title="title" playsinline controls crossorigin="anonymous"
<video class="player" ref="video" :poster="uri(thumbnail)" :title="title" playsinline controls crossorigin="anonymous"
preload="auto" autoplay>
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
:type="source.type" />

View file

@ -30,9 +30,8 @@ export default defineNuxtConfig({
],
runtimeConfig: {
public: {
domain: '/',
APP_ENV: process.env.APP_ENV || "production",
wss: process.env.VUE_APP_BASE_URL ?? '',
version: '2.0.0',
sentry: process.env.NUXT_PUBLIC_SENTRY_DSN ?? '',
}
},
@ -40,6 +39,7 @@ export default defineNuxtConfig({
transpile: ['vue-toastification'],
},
app: {
baseURL: 'dev' == process.env.APP_ENV ? '/' : '',
buildAssetsDir: "assets",
head: {
"meta": [
@ -47,7 +47,8 @@ export default defineNuxtConfig({
{ "name": "viewport", "content": "width=device-width, initial-scale=1.0, maximum-scale=1.0" },
{ "name": "theme-color", "content": "#000000" },
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico?v=100' }]
base: { "href": "/" },
link: [{ rel: 'icon', type: 'image/x-icon', href: 'favicon.ico?v=100' }]
},
pageTransition: { name: 'page', mode: 'out-in' }
},
@ -66,7 +67,7 @@ export default defineNuxtConfig({
nitro: {
output: {
publicDir: path.join(__dirname, 'exported')
publicDir: path.join(__dirname, 'dev' == process.env.APP_ENV ? 'dist' : 'exported')
},
...extraNitro,
},

View file

@ -94,7 +94,7 @@
<td class="is-text-overflow is-vcentered">
<div class="field is-grouped">
<div class="control is-text-overflow is-expanded">
<a :href="`/browser/${item.path}`" v-if="'dir' === item.type" @click.prevent="handleClick(item)">
<a :href="uri(`/browser/${item.path}`)" v-if="'dir' === item.type" @click.prevent="handleClick(item)">
{{ item.name }}
</a>
<a :href="makeDownload({}, { filename: item.path, folder: '' })"
@ -321,7 +321,7 @@ const reloadContent = async (dir = '/', fromMounted = false) => {
}
const title = `File Browser: /${dir}`
const stateUrl = `/browser/${dir}`
const stateUrl = uri(`/browser/${dir}`)
if (false === fromMounted) {
history.pushState({ path: dir, title: title }, title, stateUrl)

View file

@ -7,11 +7,20 @@ export const useSocketStore = defineStore('socket', () => {
const stateStore = useStateStore()
const toast = useNotification()
const socket = ref(null);
const isConnected = ref(false);
const socket = ref(null)
const isConnected = ref(false)
const connect = () => {
socket.value = io(runtimeConfig.public.wss, { withCredentials: true })
let opts = { withCredentials: true }
let url = runtimeConfig.public.wss
if ('dev' !== runtimeConfig.public.APP_ENV) {
url = window.origin;
opts.path = `${runtimeConfig.app.baseURL.replace(/\/$/, '')}/socket.io`;
}
socket.value = io(url, opts)
socket.value.on('connect', () => isConnected.value = true);
socket.value.on('disconnect', () => isConnected.value = false);

View file

@ -1,4 +1,5 @@
const toast = useNotification()
const runtimeConfig = useRuntimeConfig()
const AG_SEPARATOR = '.'
/**
@ -237,7 +238,6 @@ const encodePath = item => {
* @returns {Promise<Response>} - The response from the API
*/
const request = (url, options = {}) => {
const runtimeConfig = useRuntimeConfig()
options = options || {}
options.method = options.method || 'GET'
options.headers = options.headers || {}
@ -257,7 +257,7 @@ const request = (url, options = {}) => {
options.credentials = 'same-origin'
}
return fetch(url.startsWith('/') ? eTrim(runtimeConfig.public.domain, '/') + '/' + sTrim(url, '/') : url, options)
return fetch(url.startsWith('/') ? uri(url) : url, options)
}
/**
@ -360,7 +360,8 @@ const makeDownload = (config, item, base = 'api/download') => {
}
let url = `/${sTrim(baseDir, '/')}${encodePath(item.filename)}`;
return ('m3u8' === base) ? url + '.m3u8' : url;
return uri(('m3u8' === base) ? url + '.m3u8' : url);
}
/**
@ -492,6 +493,18 @@ const cleanObject = (item, fields = []) => {
return cleaned
}
const uri = u => {
if (!u || '/' === runtimeConfig.app.baseURL || !u.startsWith('/')) {
return u
}
if (true === u.startsWith(runtimeConfig.app.baseURL)) {
return u
}
return eTrim(runtimeConfig.app.baseURL, '/') + '/' + sTrim(u, '/');
}
export {
ag_set,
ag,
@ -519,4 +532,5 @@ export {
has_data,
toggleClass,
cleanObject,
uri,
}