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

+
-
+
© {{ 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: {}