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

View file

@ -109,7 +109,7 @@ class HttpAPI(common):
continue
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()
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)
if hasattr(method, "_http_method") and hasattr(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:]
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):
if "Server" in response.headers:
@ -160,14 +160,10 @@ class HttpAPI(common):
if "Origin" in request.headers:
response.headers["Access-Control-Allow-Origin"] = request.headers["Origin"]
response.headers["Access-Control-Allow-Headers"] = "Content-Type"
response.headers["Access-Control-Allow-Methods"] = "PATCH, PUT, POST, DELETE"
response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
response.headers["Access-Control-Allow-Methods"] = "GET, PATCH, PUT, POST, DELETE"
if self.config.url_prefix != "/":
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.routes.static("/api/download/", self.config.download_path)
self.preloadStatic(app)
try:
@ -215,6 +211,10 @@ class HttpAPI(common):
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")
async def ping(self, _) -> Response:
await self.queue.test()
@ -564,9 +564,7 @@ class HttpAPI(common):
raise web.HTTPBadRequest(text="file is required.")
try:
text = await Playlist(url=f"{self.config.url_host}{self.config.url_prefix}").make(
download_path=self.config.download_path, file=file
)
text = await Playlist(url="/").make(download_path=self.config.download_path, file=file)
if isinstance(text, Response):
return text
except StreamingError as e:
@ -604,7 +602,7 @@ class HttpAPI(common):
duration = float(duration)
try:
cls = M3u8(f"{self.config.url_host}{self.config.url_prefix}")
cls = M3u8("/")
if "subtitle" in mode:
text = await cls.make_subtitle(self.config.download_path, file, duration)
else:
@ -707,22 +705,6 @@ class HttpAPI(common):
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", "/")
async def index(self, _) -> Response:
if "/index.html" not in self.staticHolder:

View file

@ -54,7 +54,7 @@ class HttpSocket(common):
return wrapper
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):
method = getattr(self, attr_name)

View file

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

View file

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

View file

@ -24,7 +24,8 @@
</template>
<script setup>
const config = useConfigStore()
import { request } from '~/utils/index'
const emitter = defineEmits(['closeModel'])
const isLoading = ref(false)
const data = ref({})
@ -46,10 +47,10 @@ const eventFunc = e => {
onMounted(async () => {
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 {
isLoading.value = true
const response = await fetch(url);
const response = await request(url, { credentials: 'include' });
data.value = await response.json();
} catch (e) {
console.log(e)

View file

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

View file

@ -137,6 +137,7 @@
<script setup>
import { useStorage } from '@vueuse/core'
import { request } from '~/utils/index'
const emitter = defineEmits(['getInfo'])
const config = useConfigStore()
@ -155,7 +156,7 @@ const addInProgress = ref(false)
const addDownload = async () => {
// -- send request to convert cli options to JSON
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',
headers: {
'Content-Type': 'application/json',

View file

@ -54,10 +54,10 @@
<div v-if="false === hideThumbnail" class="card-image">
<figure class="image is-3by1" v-if="item.extras?.thumbnail">
<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 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>
</div>
<div class="card-content">

View file

@ -190,6 +190,7 @@
<script setup>
import { parseExpression } from 'cron-parser'
import { request } from '~/utils/index'
const emitter = defineEmits(['cancel', 'submit']);
const toast = useToast();
@ -242,7 +243,7 @@ const checkInfo = async () => {
// -- send request to convert cli options to JSON
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',
headers: {
'Content-Type': 'application/json',

View file

@ -98,6 +98,7 @@ import 'assets/css/style.css'
import 'assets/css/all.css'
import { useStorage } from '@vueuse/core'
import moment from "moment";
import { request } from '~/utils/index'
const Year = new Date().getFullYear()
const selectedTheme = useStorage('theme', (() => window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')())
@ -159,7 +160,7 @@ const checkCookies = async () => {
try {
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()
if (response.ok) {
toast.success('Succuss. ' + data.message)

View file

@ -1,5 +1,22 @@
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({
ssr: false,
devtools: { enabled: false },
@ -48,7 +65,8 @@ export default defineNuxtConfig({
nitro: {
output: {
publicDir: path.join(__dirname, 'exported')
}
},
...extraNitro,
},
telemetry: false,

View file

@ -122,6 +122,7 @@ div.is-centered {
<script setup>
import moment from 'moment'
import { parseExpression } from 'cron-parser'
import { request } from '~/utils/index'
const toast = useToast()
const config = useConfigStore()
@ -151,7 +152,8 @@ watch(() => socket.isConnected, async () => {
const reloadContent = async (fromMounted = false) => {
try {
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) {
return
}
@ -182,7 +184,7 @@ const resetTask = (closeForm = false) => {
}
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',
headers: {
'Content-Type': 'application/json',

View file

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

View file

@ -11,7 +11,7 @@ export const useSocketStore = defineStore('socket', () => {
const isConnected = ref(false);
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('disconnect', () => isConnected.value = false);

View file

@ -229,7 +229,7 @@ const encodePath = item => {
* And prefix the URL with the API URL and path.
*
* @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
*/
@ -254,7 +254,11 @@ const request = (url, options = {}) => {
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 + '/';
}
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;
}