From a96be4c0feb151cca55085cabc1600bbb9c990df Mon Sep 17 00:00:00 2001
From: arabcoders
Date: Sun, 14 Sep 2025 19:43:52 +0300
Subject: [PATCH] Add docs to footer
---
.vscode/settings.json | 1 +
LICENSE | 2 +-
app/routes/api/docs.py | 119 +++++++++++++++++
ui/app/components/DLFields.vue | 2 +-
ui/app/components/History.vue | 6 +-
ui/app/components/Markdown.vue | 238 +++++++++++++++++++++++++++++++++
ui/app/components/Queue.vue | 4 +-
ui/app/layouts/default.vue | 14 +-
ui/package.json | 6 +-
ui/pnpm-lock.yaml | 52 +++++++
10 files changed, 432 insertions(+), 12 deletions(-)
create mode 100644 app/routes/api/docs.py
create mode 100644 ui/app/components/Markdown.vue
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3113d396..5c75efb6 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -141,6 +141,7 @@
"sstr",
"startswith",
"SUPPRESSHELP",
+ "tablehead",
"tebibytes",
"testpaths",
"threadsafe",
diff --git a/LICENSE b/LICENSE
index 8e9af564..c8f664a0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2024 ArabCoders
+Copyright (c) 2025 ArabCoders
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/app/routes/api/docs.py b/app/routes/api/docs.py
new file mode 100644
index 00000000..fe0eccf9
--- /dev/null
+++ b/app/routes/api/docs.py
@@ -0,0 +1,119 @@
+import logging
+import time
+from datetime import UTC, datetime
+from typing import Any
+
+import httpx
+from aiohttp import web
+from aiohttp.web import Request, Response
+from yt_dlp.utils.networking import random_user_agent
+
+from app.library.cache import Cache
+from app.library.config import Config
+from app.library.router import add_route, route
+from app.library.YTDLPOpts import YTDLPOpts
+
+LOG: logging.Logger = logging.getLogger(__name__)
+
+STATIC_FILES = ["README.md", "FAQ.md", "API.md", "sc_short.png"]
+EXT_TO_MIME: dict = {
+ ".md": "text/markdown",
+ ".png": "image/png",
+}
+
+
+@route("GET", "api/docs/{file}", name="get_doc")
+async def get_doc(request: Request, config: Config, cache: Cache) -> Response:
+ """
+ Get the thumbnail.
+
+ Args:
+ request (Request): The request object.
+ config (Config): The configuration object.
+ cache (Cache): The cache object.
+
+ Returns:
+ Response: The response object.
+
+ """
+ if not (file := request.path):
+ return web.json_response(
+ data={
+ "error": "Doc file is is required.",
+ "matcher": request.match_info,
+ },
+ status=web.HTTPForbidden.status_code,
+ )
+
+ file = file.removeprefix("/api/docs/") if file.startswith("/api/docs/") else file.removeprefix("/")
+ if file not in STATIC_FILES:
+ return web.json_response(
+ data={
+ "error": "Doc file not found.",
+ "file": file,
+ "st": STATIC_FILES,
+ },
+ status=web.HTTPNotFound.status_code,
+ )
+
+ cache_key = f"doc:{file}"
+ if dct := cache.get(cache_key):
+ LOG.debug(f"Serving doc '{file}' from cache.")
+ return web.Response(**dct)
+
+ url = f"https://raw.githubusercontent.com/arabcoders/ytptube/refs/heads/dev/{file}"
+
+ try:
+ ytdlp_args: dict = YTDLPOpts.get_instance().preset(name=config.default_preset).get_all()
+ opts: dict[str, Any] = {
+ "headers": {
+ "User-Agent": request.headers.get("User-Agent", ytdlp_args.get("user_agent", random_user_agent())),
+ },
+ }
+ if proxy := ytdlp_args.get("proxy"):
+ opts["proxy"] = proxy
+
+ try:
+ from httpx_curl_cffi import AsyncCurlTransport, CurlOpt
+
+ opts["transport"] = AsyncCurlTransport(
+ impersonate="chrome",
+ default_headers=True,
+ curl_options={CurlOpt.FRESH_CONNECT: True},
+ )
+ opts.pop("headers", None)
+ except Exception:
+ pass
+
+ async with httpx.AsyncClient(**opts) as client:
+ LOG.debug(f"Fetching doc from '{url}'.")
+ response = await client.request(method="GET", url=url, follow_redirects=True)
+ dct = {
+ "body": response.content,
+ "headers": {
+ "Content-Type": EXT_TO_MIME.get(file[file.rfind(".") :], "text/plain"),
+ "Pragma": "public",
+ "Access-Control-Allow-Origin": "*",
+ "Cache-Control": f"public, max-age={time.time() + 3600}",
+ "Expires": time.strftime(
+ "%a, %d %b %Y %H:%M:%S GMT",
+ datetime.fromtimestamp(time.time() + 3600, tz=UTC).timetuple(),
+ ),
+ },
+ }
+
+ cache.set(cache_key, dct, ttl=3600)
+
+ return web.Response(**dct)
+ except Exception as e:
+ LOG.error(f"Failed to request doc from '{url}'.'. '{e!s}'.")
+ return web.json_response(data={"error": "Failed to get doc."}, status=web.HTTPInternalServerError.status_code)
+
+
+for file in STATIC_FILES:
+ add_route(
+ method="GET",
+ path=f"{file}",
+ handler=get_doc,
+ name=f"get_{file.replace('.', '_')}",
+ )
diff --git a/ui/app/components/DLFields.vue b/ui/app/components/DLFields.vue
index 64464b14..ab8b9b48 100644
--- a/ui/app/components/DLFields.vue
+++ b/ui/app/components/DLFields.vue
@@ -164,7 +164,7 @@
diff --git a/ui/app/components/Queue.vue b/ui/app/components/Queue.vue
index 50ab04f4..145d1835 100644
--- a/ui/app/components/Queue.vue
+++ b/ui/app/components/Queue.vue
@@ -203,13 +203,13 @@
-
pImg(e)"
+
pImg(e)"
:src="uri('/api/thumbnail?id=' + item._id + '&url=' + encodePath(item.extras.thumbnail))"
v-if="item.extras?.thumbnail" />
-
pImg(e)" v-if="item.extras?.thumbnail"
+
pImg(e)" v-if="item.extras?.thumbnail"
:src="uri('/api/thumbnail?id=' + item._id + '&url=' + encodePath(item.extras.thumbnail))" />
diff --git a/ui/app/layouts/default.vue b/ui/app/layouts/default.vue
index 7c4b79bf..4ef125bc 100644
--- a/ui/app/layouts/default.vue
+++ b/ui/app/layouts/default.vue
@@ -121,24 +121,28 @@
connection is functional.
+ doc.file = ''" :file="doc.file" v-if="doc.file" />
-
+
© {{ Year }} - YTPTube
-
({{ config?.app?.app_version || 'unknown' }})
- yt-dlp
- ({{ config?.app?.ytdlp_version || 'unknown' }})
+ ({{ config?.app?.ytdlp_version || 'unknown' }})
- CHANGELOG
+ - FAQ
+ - README
+ - API
-
+
@@ -161,6 +165,7 @@ import type { YTDLPOption } from '~/types/ytdlp'
import { useDialog } from '~/composables/useDialog'
import Dialog from '~/components/Dialog.vue'
import Shutdown from '~/components/shutdown.vue'
+import Markdown from '~/components/Markdown.vue'
const Year = new Date().getFullYear()
const selectedTheme = useStorage('theme', 'auto')
@@ -174,6 +179,7 @@ const bg_opacity = useStorage('random_bg_opacity', 0.95)
const showMenu = ref(false)
const isMobile = useMediaQuery({ maxWidth: 1024 })
const app_shutdown = ref(false)
+const doc = ref<{ file: string }>({ file: '' })
const applyPreferredColorScheme = (scheme: string) => {
if (!scheme || scheme === 'auto') {
diff --git a/ui/package.json b/ui/package.json
index 04e67121..358ef69f 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -33,7 +33,11 @@
"socket.io-client": "^4.8.1",
"vue": "^3.5.21",
"vue-router": "^4.5.1",
- "vue-toastification": "2.0.0-rc.5"
+ "vue-toastification": "2.0.0-rc.5",
+ "marked": "^16.2.1",
+ "marked-alert": "^2.1.2",
+ "marked-base-url": "^1.1.7",
+ "marked-gfm-heading-id": "^4.1.2"
},
"pnpm": {
"onlyBuiltDependencies": [
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 8ac03892..d3cd7f05 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -35,6 +35,18 @@ importers:
hls.js:
specifier: ^1.6.12
version: 1.6.12
+ marked:
+ specifier: ^16.2.1
+ version: 16.2.1
+ marked-alert:
+ specifier: ^2.1.2
+ version: 2.1.2(marked@16.2.1)
+ marked-base-url:
+ specifier: ^1.1.7
+ version: 1.1.7(marked@16.2.1)
+ marked-gfm-heading-id:
+ specifier: ^4.1.2
+ version: 4.1.2(marked@16.2.1)
moment:
specifier: ^2.30.1
version: 2.30.1
@@ -2402,6 +2414,9 @@ packages:
git-url-parse@16.1.0:
resolution: {integrity: sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw==}
+ github-slugger@2.0.0:
+ resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
+
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -2762,6 +2777,26 @@ packages:
magicast@0.3.5:
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
+ marked-alert@2.1.2:
+ resolution: {integrity: sha512-EFNRZ08d8L/iEIPLTlQMDjvwIsj03gxWCczYTht6DCiHJIZhMk4NK5gtPY9UqAYb09eV5VGT+jD4lp396E0I+w==}
+ peerDependencies:
+ marked: '>=7.0.0'
+
+ marked-base-url@1.1.7:
+ resolution: {integrity: sha512-CJOfpG2/XOEp8UuI5H0tbELxuS1v8Ud705jamEIpWBQDdkda1i+LrafxLn41rlxhGEeJqo27b/hBFVYHWOYccw==}
+ peerDependencies:
+ marked: '>= 4 < 17'
+
+ marked-gfm-heading-id@4.1.2:
+ resolution: {integrity: sha512-EQ1WiEGHJh0C8viU+hbXbhHyWTDgEia2i96fiSemm2wdYER6YBw/9QI5TB6YFTqFfmMOxBFXPcPJtlgD0fVV2w==}
+ peerDependencies:
+ marked: '>=13 <17'
+
+ marked@16.2.1:
+ resolution: {integrity: sha512-r3UrXED9lMlHF97jJByry90cwrZBBvZmjG1L68oYfuPMW+uDTnuMbyJDymCWwbTE+f+3LhpNDKfpR3a3saFyjA==}
+ engines: {node: '>= 20'}
+ hasBin: true
+
mdn-data@2.0.28:
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
@@ -6638,6 +6673,8 @@ snapshots:
dependencies:
git-up: 8.1.1
+ github-slugger@2.0.0: {}
+
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -6985,6 +7022,21 @@ snapshots:
'@babel/types': 7.28.4
source-map-js: 1.2.1
+ marked-alert@2.1.2(marked@16.2.1):
+ dependencies:
+ marked: 16.2.1
+
+ marked-base-url@1.1.7(marked@16.2.1):
+ dependencies:
+ marked: 16.2.1
+
+ marked-gfm-heading-id@4.1.2(marked@16.2.1):
+ dependencies:
+ github-slugger: 2.0.0
+ marked: 16.2.1
+
+ marked@16.2.1: {}
+
mdn-data@2.0.28: {}
mdn-data@2.12.2: {}