simplify API calls.

This commit is contained in:
ArabCoders 2025-01-25 17:25:58 +03:00
parent e5f2a09cc7
commit 922d194d7a
17 changed files with 66 additions and 72 deletions

4
.vscode/launch.json vendored
View file

@ -18,6 +18,7 @@
"type": "node", "type": "node",
"cwd": "${workspaceFolder}/ui", "cwd": "${workspaceFolder}/ui",
"env": { "env": {
"NUXT_API_URL": "http://localhost:8081/api/",
"NUXT_PUBLIC_WSS": ":8081/", "NUXT_PUBLIC_WSS": ":8081/",
}, },
"console": "internalConsole" "console": "internalConsole"
@ -34,13 +35,13 @@
"YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads", "YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads",
"YTP_TEMP_PATH": "${workspaceFolder}/var/tmp", "YTP_TEMP_PATH": "${workspaceFolder}/var/tmp",
"PYDEVD_DISABLE_FILE_VALIDATION": "1", "PYDEVD_DISABLE_FILE_VALIDATION": "1",
"YTP_URL_HOST": "http://localhost:8081",
"YTP_MAX_WORKERS": "2", "YTP_MAX_WORKERS": "2",
"YTP_IGNORE_UI": "true", "YTP_IGNORE_UI": "true",
"YTP_PIP_IGNORE_UPDATES": "true", "YTP_PIP_IGNORE_UPDATES": "true",
"YTP_LOG_LEVEL": "DEBUG", "YTP_LOG_LEVEL": "DEBUG",
"YTP_DEBUG": "true", "YTP_DEBUG": "true",
"YTP_YTDL_DEBUG": "false", "YTP_YTDL_DEBUG": "false",
"YTP_ACCESS_LOG": "true",
} }
}, },
{ {
@ -54,7 +55,6 @@
"YTP_CONFIG_PATH": "${workspaceFolder}/var/config", "YTP_CONFIG_PATH": "${workspaceFolder}/var/config",
"YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads", "YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads",
"YTP_TEMP_PATH": "${workspaceFolder}/var/tmp", "YTP_TEMP_PATH": "${workspaceFolder}/var/tmp",
"YTP_URL_HOST": "http://localhost:8081",
"YTP_LOG_LEVEL": "DEBUG", "YTP_LOG_LEVEL": "DEBUG",
} }
}, },

View file

@ -70,7 +70,6 @@ Certain values can be set via environment variables, using the `-e` parameter on
* __YTP_DOWNLOAD_PATH__: path to where the downloads will be saved. Defaults to `/downloads` in the docker image, and `./var/downloads` otherwise. * __YTP_DOWNLOAD_PATH__: path to where the downloads will be saved. Defaults to `/downloads` in the docker image, and `./var/downloads` otherwise.
* __YTP_TEMP_PATH__: path where intermediary download files will be saved. Defaults to `/tmp` in the docker image, and `./var/tmp` otherwise. * __YTP_TEMP_PATH__: path where intermediary download files will be saved. Defaults to `/tmp` in the docker image, and `./var/tmp` otherwise.
* __YTP_TEMP_KEEP__: Whether to keep the Individual video temp directory or remove it. Defaults to `false`. * __YTP_TEMP_KEEP__: Whether to keep the Individual video temp directory or remove it. Defaults to `false`.
* __YTP_URL_PREFIX__: base path for the web server (for use when hosting behind a reverse proxy). Defaults to `/`.
* __YTP_OUTPUT_TEMPLATE__: the template for the filenames of the downloaded videos, formatted according to [this spec](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#output-template). Defaults to `%(title)s.%(ext)s`. This will be the default for all downloads unless the request include output template. * __YTP_OUTPUT_TEMPLATE__: the template for the filenames of the downloaded videos, formatted according to [this spec](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#output-template). Defaults to `%(title)s.%(ext)s`. This will be the default for all downloads unless the request include output template.
* __YTP_OUTPUT_TEMPLATE_CHAPTER__: the template for the filenames of the downloaded videos, when split into chapters via postprocessors, formatted according to [this spec](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#output-template). Defaults to `%(title)s - %(section_number)s %(section_title)s.%(ext)s.` * __YTP_OUTPUT_TEMPLATE_CHAPTER__: the template for the filenames of the downloaded videos, when split into chapters via postprocessors, formatted according to [this spec](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#output-template). Defaults to `%(title)s - %(section_number)s %(section_title)s.%(ext)s.`
* __YTP_KEEP_ARCHIVE__: Whether to keep history of downloaded videos to prevent downloading same file multiple times. Defaults to `true`. * __YTP_KEEP_ARCHIVE__: Whether to keep history of downloaded videos to prevent downloading same file multiple times. Defaults to `true`.
@ -102,8 +101,6 @@ Certain values can be set via environment variables, using the `-e` parameter on
It's advisable to run YTPTube behind a reverse proxy, if authentication and/or HTTPS support are required. It's advisable to run YTPTube behind a reverse proxy, if authentication and/or HTTPS support are required.
When running behind a reverse proxy which remaps the URL (i.e. serves YTPTube under a subdirectory and not under root), don't forget to set the `YTP_URL_PREFIX` environment variable to the correct value.
### NGINX ### NGINX
```nginx ```nginx

View file

@ -109,7 +109,7 @@ class HttpAPI(common):
continue continue
file = os.path.join(root, file) file = os.path.join(root, file)
urlPath = f"{self.config.url_prefix}{file.replace(f'{staticDir}/', '')}" urlPath = f"/{file.replace(f'{staticDir}/', '')}"
content = open(file, "rb").read() content = open(file, "rb").read()
contentType = self.extToMime.get(os.path.splitext(file)[1], MIME.from_file(file)) contentType = self.extToMime.get(os.path.splitext(file)[1], MIME.from_file(file))
@ -149,10 +149,10 @@ class HttpAPI(common):
method = getattr(self, attr_name) method = getattr(self, attr_name)
if hasattr(method, "_http_method") and hasattr(method, "_http_path"): if hasattr(method, "_http_method") and hasattr(method, "_http_path"):
http_path = method._http_path http_path = method._http_path
if http_path.startswith("/") and self.config.url_prefix.endswith("/"): if http_path.startswith("/"):
http_path = method._http_path[1:] http_path = method._http_path[1:]
self.routes.route(method._http_method, self.config.url_prefix + http_path)(method) self.routes.route(method._http_method, f"/{http_path}")(method)
async def on_prepare(request: Request, response: Response): async def on_prepare(request: Request, response: Response):
if "Server" in response.headers: if "Server" in response.headers:
@ -160,14 +160,10 @@ class HttpAPI(common):
if "Origin" in request.headers: if "Origin" in request.headers:
response.headers["Access-Control-Allow-Origin"] = request.headers["Origin"] response.headers["Access-Control-Allow-Origin"] = request.headers["Origin"]
response.headers["Access-Control-Allow-Headers"] = "Content-Type" response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
response.headers["Access-Control-Allow-Methods"] = "PATCH, PUT, POST, DELETE" response.headers["Access-Control-Allow-Methods"] = "GET, PATCH, PUT, POST, DELETE"
if self.config.url_prefix != "/": self.routes.static("/api/download/", self.config.download_path)
self.routes.route("GET", "/")(lambda _: web.HTTPFound(self.config.url_prefix))
self.routes.get(self.config.url_prefix[:-1])(lambda _: web.HTTPFound(self.config.url_prefix))
self.routes.static(f"{self.config.url_prefix}api/download/", self.config.download_path)
self.preloadStatic(app) self.preloadStatic(app)
try: try:
@ -215,6 +211,10 @@ class HttpAPI(common):
return middleware_handler return middleware_handler
@route("OPTIONS", "/{path:.*}")
async def add_coors(self, _: Request) -> Response:
return web.json_response(data={"status": "ok"}, status=web.HTTPOk.status_code)
@route("GET", "api/ping") @route("GET", "api/ping")
async def ping(self, _) -> Response: async def ping(self, _) -> Response:
await self.queue.test() await self.queue.test()
@ -564,9 +564,7 @@ class HttpAPI(common):
raise web.HTTPBadRequest(text="file is required.") raise web.HTTPBadRequest(text="file is required.")
try: try:
text = await Playlist(url=f"{self.config.url_host}{self.config.url_prefix}").make( text = await Playlist(url="/").make(download_path=self.config.download_path, file=file)
download_path=self.config.download_path, file=file
)
if isinstance(text, Response): if isinstance(text, Response):
return text return text
except StreamingError as e: except StreamingError as e:
@ -604,7 +602,7 @@ class HttpAPI(common):
duration = float(duration) duration = float(duration)
try: try:
cls = M3u8(f"{self.config.url_host}{self.config.url_prefix}") cls = M3u8("/")
if "subtitle" in mode: if "subtitle" in mode:
text = await cls.make_subtitle(self.config.download_path, file, duration) text = await cls.make_subtitle(self.config.download_path, file, duration)
else: else:
@ -707,22 +705,6 @@ class HttpAPI(common):
status=web.HTTPOk.status_code, status=web.HTTPOk.status_code,
) )
@route("OPTIONS", "api/add")
async def add_cors(self, _: Request) -> Response:
return web.json_response(data={"status": "ok"}, status=web.HTTPOk.status_code)
@route("OPTIONS", "api/tasks")
async def cors_add_tasks(self, _: Request) -> Response:
return web.json_response(data={"status": "ok"}, status=web.HTTPOk.status_code)
@route("OPTIONS", "api/yt-dlp/convert")
async def cors_ytdlp_convert(self, _: Request) -> Response:
return web.json_response(data={"status": "ok"}, status=web.HTTPOk.status_code)
@route("OPTIONS", "api/delete")
async def delete_cors(self, _: Request) -> Response:
return web.json_response(data={"status": "ok"}, status=web.HTTPOk.status_code)
@route("GET", "/") @route("GET", "/")
async def index(self, _) -> Response: async def index(self, _) -> Response:
if "/index.html" not in self.staticHolder: if "/index.html" not in self.staticHolder:

View file

@ -54,7 +54,7 @@ class HttpSocket(common):
return wrapper return wrapper
def attach(self, app: web.Application): def attach(self, app: web.Application):
self.sio.attach(app, socketio_path=self.config.url_prefix + "socket.io") self.sio.attach(app, socketio_path=self.config.url_socketio)
for attr_name in dir(self): for attr_name in dir(self):
method = getattr(self, attr_name) method = getattr(self, attr_name)

View file

@ -29,11 +29,7 @@ class Config:
temp_keep: bool = False temp_keep: bool = False
"""Keep temporary files after the download is complete.""" """Keep temporary files after the download is complete."""
url_host: str = "" url_socketio: str = "/socket.io"
"""The host to bind the server to."""
url_prefix: str = ""
"""The prefix to use for the server URL."""
url_socketio: str = "{url_prefix}socket.io"
"""The URL to use for the socket.io server.""" """The URL to use for the socket.io server."""
output_template: str = "%(title)s.%(ext)s" output_template: str = "%(title)s.%(ext)s"
@ -192,9 +188,7 @@ class Config:
"output_template", "output_template",
"ytdlp_version", "ytdlp_version",
"version", "version",
"url_host",
"started", "started",
"url_prefix",
"remove_files", "remove_files",
"ui_update_title", "ui_update_title",
"max_workers", "max_workers",
@ -275,9 +269,6 @@ class Config:
if k in self._int_vars: if k in self._int_vars:
setattr(self, k, int(v)) setattr(self, k, int(v))
if not self.url_prefix.endswith("/"):
self.url_prefix += "/"
numeric_level = getattr(logging, self.log_level.upper(), None) numeric_level = getattr(logging, self.log_level.upper(), None)
if not isinstance(numeric_level, int): if not isinstance(numeric_level, int):
raise ValueError(f"Invalid log level '{self.log_level}' specified.") raise ValueError(f"Invalid log level '{self.log_level}' specified.")

View file

@ -16,6 +16,8 @@
</template> </template>
<script setup> <script setup>
import { request } from '~/utils/index'
const props = defineProps({ const props = defineProps({
image: { image: {
type: String, type: String,
@ -38,7 +40,6 @@ const props = defineProps({
const cache = useSessionCache() const cache = useSessionCache()
const toast = useToast() const toast = useToast()
const config = useConfigStore()
const url = ref() const url = ref()
const error = ref(false) const error = ref(false)
const isPreloading = ref(false) const isPreloading = ref(false)
@ -53,8 +54,8 @@ const defaultLoader = async () => {
return return
} }
const response = await fetch(config.app.url_host + config.app.url_prefix + 'api/thumbnail?url=' + encodePath(props.image), { const response = await request('/api/thumbnail?url=' + encodePath(props.image), {
signal: cancelRequest.signal signal: cancelRequest.signal,
}) })
if (200 !== response.status) { if (200 !== response.status) {

View file

@ -24,7 +24,8 @@
</template> </template>
<script setup> <script setup>
const config = useConfigStore() import { request } from '~/utils/index'
const emitter = defineEmits(['closeModel']) const emitter = defineEmits(['closeModel'])
const isLoading = ref(false) const isLoading = ref(false)
const data = ref({}) const data = ref({})
@ -46,10 +47,10 @@ const eventFunc = e => {
onMounted(async () => { onMounted(async () => {
window.addEventListener('keydown', eventFunc) window.addEventListener('keydown', eventFunc)
const url = config.app.url_host + config.app.url_prefix + 'api/url/info?url=' + encodePath(props.link) const url = '/api/url/info?url=' + encodePath(props.link)
try { try {
isLoading.value = true isLoading.value = true
const response = await fetch(url); const response = await request(url, { credentials: 'include' });
data.value = await response.json(); data.value = await response.json();
} catch (e) { } catch (e) {
console.log(e) console.log(e)

View file

@ -99,14 +99,11 @@
<figure class="image is-3by1"> <figure class="image is-3by1">
<span v-if="'finished' === item.status" @click="playVideo(item)" class="play-overlay"> <span v-if="'finished' === item.status" @click="playVideo(item)" class="play-overlay">
<div class="play-icon"></div> <div class="play-icon"></div>
<img <img :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)" v-if="item.extras?.thumbnail" />
:src="config.app.url_host + config.app.url_prefix + 'api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
v-if="item.extras?.thumbnail" />
<img v-else src="/images/placeholder.png" /> <img v-else src="/images/placeholder.png" />
</span> </span>
<template v-else> <template v-else>
<img v-if="item.extras?.thumbnail" <img v-if="item.extras?.thumbnail" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)" />
:src="config.app.url_host + config.app.url_prefix + 'api/thumbnail?url=' + encodePath(item.extras.thumbnail)" />
<img v-else src="/images/placeholder.png" /> <img v-else src="/images/placeholder.png" />
</template> </template>
</figure> </figure>
@ -276,7 +273,7 @@ const playVideo = item => {
video_link.value = makeDownload(config, item, 'm3u8') video_link.value = makeDownload(config, item, 'm3u8')
video_title.value = item.title video_title.value = item.title
if (item.extras?.thumbnail) { if (item.extras?.thumbnail) {
video_thumbnail.value = config.app.url_host + config.app.url_prefix + 'api/thumbnail?url=' + encodePath(item.extras.thumbnail) video_thumbnail.value = '/api/thumbnail?url=' + encodePath(item.extras.thumbnail)
} }
if (item.extras?.channel) { if (item.extras?.channel) {
video_artist.value = item.extras.channel video_artist.value = item.extras.channel

View file

@ -137,6 +137,7 @@
<script setup> <script setup>
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { request } from '~/utils/index'
const emitter = defineEmits(['getInfo']) const emitter = defineEmits(['getInfo'])
const config = useConfigStore() const config = useConfigStore()
@ -155,7 +156,7 @@ const addInProgress = ref(false)
const addDownload = async () => { const addDownload = async () => {
// -- send request to convert cli options to JSON // -- send request to convert cli options to JSON
if (ytdlpConfig.value && ytdlpConfig.value.length > 2 && !ytdlpConfig.value.trim().startsWith('{')) { if (ytdlpConfig.value && ytdlpConfig.value.length > 2 && !ytdlpConfig.value.trim().startsWith('{')) {
const response = await fetch(config.app.url_host + config.app.url_prefix + 'api/yt-dlp/convert', { const response = await request('/api/yt-dlp/convert', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View file

@ -54,10 +54,10 @@
<div v-if="false === hideThumbnail" class="card-image"> <div v-if="false === hideThumbnail" class="card-image">
<figure class="image is-3by1" v-if="item.extras?.thumbnail"> <figure class="image is-3by1" v-if="item.extras?.thumbnail">
<img :alt="item.title" <img :alt="item.title"
:src="config.app.url_host + config.app.url_prefix + 'api/thumbnail?url=' + encodePath(item.extras.thumbnail)" /> :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)" />
</figure> </figure>
<figure class="image is-3by1" v-else> <figure class="image is-3by1" v-else>
<img :src="config.app.url_host + config.app.url_prefix + 'images/placeholder.png'" /> <img :src="'/images/placeholder.png'" />
</figure> </figure>
</div> </div>
<div class="card-content"> <div class="card-content">

View file

@ -190,6 +190,7 @@
<script setup> <script setup>
import { parseExpression } from 'cron-parser' import { parseExpression } from 'cron-parser'
import { request } from '~/utils/index'
const emitter = defineEmits(['cancel', 'submit']); const emitter = defineEmits(['cancel', 'submit']);
const toast = useToast(); const toast = useToast();
@ -242,7 +243,7 @@ const checkInfo = async () => {
// -- send request to convert cli options to JSON // -- send request to convert cli options to JSON
if (form.ytdlp_config && form.ytdlp_config.length > 2 && !form.ytdlp_config.trim().startsWith('{')) { if (form.ytdlp_config && form.ytdlp_config.length > 2 && !form.ytdlp_config.trim().startsWith('{')) {
const response = await fetch(config.app.url_host + config.app.url_prefix + 'api/yt-dlp/convert', { const response = await request('/api/yt-dlp/convert', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View file

@ -98,6 +98,7 @@ import 'assets/css/style.css'
import 'assets/css/all.css' import 'assets/css/all.css'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import moment from "moment"; import moment from "moment";
import { request } from '~/utils/index'
const Year = new Date().getFullYear() const Year = new Date().getFullYear()
const selectedTheme = useStorage('theme', (() => window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')()) const selectedTheme = useStorage('theme', (() => window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')())
@ -159,7 +160,7 @@ const checkCookies = async () => {
try { try {
isChecking.value = true isChecking.value = true
const response = await fetch(config.app.url_host + config.app.url_prefix + 'api/youtube/auth') const response = await request('/api/youtube/auth')
const data = await response.json() const data = await response.json()
if (response.ok) { if (response.ok) {
toast.success('Succuss. ' + data.message) toast.success('Succuss. ' + data.message)

View file

@ -1,5 +1,22 @@
import path from "path"; import path from "path";
let extraNitro = {}
try {
const API_URL = import.meta.env.NUXT_API_URL;
if (API_URL) {
extraNitro = {
devProxy: {
'/api/': {
target: API_URL,
changeOrigin: true
}
}
}
}
}
catch (e) {
}
export default defineNuxtConfig({ export default defineNuxtConfig({
ssr: false, ssr: false,
devtools: { enabled: false }, devtools: { enabled: false },
@ -48,7 +65,8 @@ export default defineNuxtConfig({
nitro: { nitro: {
output: { output: {
publicDir: path.join(__dirname, 'exported') publicDir: path.join(__dirname, 'exported')
} },
...extraNitro,
}, },
telemetry: false, telemetry: false,

View file

@ -122,6 +122,7 @@ div.is-centered {
<script setup> <script setup>
import moment from 'moment' import moment from 'moment'
import { parseExpression } from 'cron-parser' import { parseExpression } from 'cron-parser'
import { request } from '~/utils/index'
const toast = useToast() const toast = useToast()
const config = useConfigStore() const config = useConfigStore()
@ -151,7 +152,8 @@ watch(() => socket.isConnected, async () => {
const reloadContent = async (fromMounted = false) => { const reloadContent = async (fromMounted = false) => {
try { try {
isLoading.value = true isLoading.value = true
const response = await fetch(config.app.url_host + config.app.url_prefix + 'api/tasks') const response = await request('/api/tasks')
if (fromMounted && !response.ok) { if (fromMounted && !response.ok) {
return return
} }
@ -182,7 +184,7 @@ const resetTask = (closeForm = false) => {
} }
const updateTasks = async tasks => { const updateTasks = async tasks => {
const response = await fetch(config.app.url_host + config.app.url_prefix + 'api/tasks', { const response = await request('/api/tasks', {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View file

@ -11,8 +11,6 @@ const CONFIG_KEYS = {
ytdlp_version: '', ytdlp_version: '',
max_workers: 1, max_workers: 1,
version: '', version: '',
url_host: '',
url_prefix: '',
has_cookies: false, has_cookies: false,
basic_mode: true, basic_mode: true,
default_preset: 'default', default_preset: 'default',

View file

@ -11,7 +11,7 @@ export const useSocketStore = defineStore('socket', () => {
const isConnected = ref(false); const isConnected = ref(false);
const connect = () => { const connect = () => {
socket.value = io(runtimeConfig.public.wss) socket.value = io(runtimeConfig.public.wss, { withCredentials: true })
socket.value.on('connect', () => isConnected.value = true); socket.value.on('connect', () => isConnected.value = true);
socket.value.on('disconnect', () => isConnected.value = false); socket.value.on('disconnect', () => isConnected.value = false);

View file

@ -229,7 +229,7 @@ const encodePath = item => {
* And prefix the URL with the API URL and path. * And prefix the URL with the API URL and path.
* *
* @param {string} url - The URL to request * @param {string} url - The URL to request
* @param {object} options - The request options * @param {RequestInit} options - The request options
* *
* @returns {Promise<Response>} - The response from the API * @returns {Promise<Response>} - The response from the API
*/ */
@ -254,7 +254,11 @@ const request = (url, options = {}) => {
options.headers['Accept'] = 'application/json' options.headers['Accept'] = 'application/json'
} }
return fetch(url.startsWith('/') ? runtimeConfig.public.domain + url : url, options) if (url.startsWith('/')) {
options.credentials = 'same-origin'
}
return fetch(url.startsWith('/') ? eTrim(runtimeConfig.public.domain, '/') + '/' + sTrim(url, '/') : url, options)
} }
/** /**
@ -353,7 +357,7 @@ const makeDownload = (config, item, base = 'api/download') => {
baseDir += item.folder + '/'; baseDir += item.folder + '/';
} }
let url = config.app.url_host + encodePath(config.app.url_prefix + baseDir + item.filename); let url = `/${sTrim(baseDir, '/')}${encodePath(item.filename)}`;
return ('m3u8' === base) ? url + '.m3u8' : url; return ('m3u8' === base) ? url + '.m3u8' : url;
} }