minor fixes, rework web service, add features

This commit is contained in:
2023-09-25 04:05:42 +03:00
committed by nikili0n
parent f06d2d3507
commit 31928cca48
9 changed files with 182 additions and 84 deletions

View File

@ -1,4 +1,3 @@
import asyncio
import json
import os
from ast import literal_eval
@ -6,6 +5,7 @@ from ast import literal_eval
import uvicorn
from aio_pika import connect, Message, DeliveryMode
from fastapi import FastAPI, Request, Depends
from loguru import logger
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import JSONResponse, FileResponse, StreamingResponse
from starlette.templating import Jinja2Templates
@ -39,7 +39,19 @@ async def is_task_already_done_or_exist(redis: RedisClient, link: str):
if len(tasks) > 0:
task = tasks[0]
await redis.del_task_from_task_done_queue(task)
return task
async def is_task_in_process(redis: RedisClient, link: str):
messages = await redis.get_tasks()
tasks = [
literal_eval(message.decode('utf-8')) for message in messages
if literal_eval(message.decode('utf-8'))["link"] == link
]
if len(tasks) > 0:
task = tasks[0]
return task
@ -47,28 +59,33 @@ async def is_task_already_done_or_exist(redis: RedisClient, link: str):
async def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.post('/submit/')
async def get_url_for_download_video(request: Request, data: SubmitIn = Depends()):
'''
"""
TODO:
Сабмит должен проверить что задача может быть уже выполненой (отдать ссылку в ответе)
Сабмит должен проверить что задача может быть уже выполненой (отдать ссылку в ответе)
или ещё в работе (сообщить об этом в ответе, можно вывести на форму, что такая ссылка уже скачивается, ожидайте)
Если условия выше провалены, то мы делаем новую задачу в очередь с переданными параметрами и сообщаем об этом клиенту с кодом (200 или 201)
Дополнительно, нужен отдельный метод (ури), который позволит получать статус задачи. Опрашиваться примерно раз в 5с,
Дополнительно, нужен отдельный метод (ури), который позволит получать статус задачи. Опрашиваться примерно раз в 5с,
возможны увелечения тайминга в зависимости от ответа апи (на будущее)
Варианты ответа
1) такой задачи нет (404)
2) такая задача есть и выполняется (200 ли?)
3) такая задача есть и завершена (200 и выдать ссылку на загрузку)
4) такая задача есть и завершена, но с ошибкой (500 и сообщение о том, что можно попробовать выполнить задачу занов,
попутно удалив задачу из выполненых, с очисткой мусора за ней)
'''
4) такая задача есть и завершена, но с ошибкой (500 и сообщение о том, что можно попробовать выполнить задачу заново,
попутно удалив задачу из выполненных, с очисткой мусора за ней)
"""
red = RedisClient()
task_done = await is_task_already_done_or_exist(red, data.link)
task_in_process = await is_task_in_process(red, data.link)
if task_in_process:
return JSONResponse({"result": "Задача в работе. Ожидайте"})
if task_done:
link_to_download_video = str(request.base_url) + "get/?file_path=" + task_done["result"]
return JSONResponse({"result": link_to_download_video})
# TODO: учесть, что если делать запрос CURL\urllib3\etc, в теле может быть несколько ссылок -> должно быть создано несколько задач
async with await connect("amqp://guest:guest@localhost/") as connection:
# Creating a channel
@ -76,7 +93,7 @@ async def get_url_for_download_video(request: Request, data: SubmitIn = Depends(
body = [
{
"link": data.link,
"format": f"bestvideo[ext={data.format.value}]+bestaudio[ext={data.format.value}]/best[ext={data.format.value}]/best",
"format": f"bestvideo[ext={data.video_format.value}]+bestaudio[ext={data.audio_format.value}]/best[ext={data.video_format.value}]/best",
"merge_output_format": data.merge_output_format.value,
"outtmpl": f"downloads/%(extractor_key)s/%(id)s_%(width)sp.%(ext)s",
}, ]
@ -94,31 +111,11 @@ async def get_url_for_download_video(request: Request, data: SubmitIn = Depends(
routing_key='hello',
)
print(f" [x] Sent '{link}'")
while True:
try:
messages = await red.get_task_done_queue()
tasks = [
literal_eval(message.decode('utf-8')) for message in messages
if literal_eval(message.decode('utf-8'))["link"] == link["link"]
]
error_tasks = [tasks.pop(tasks.index(error_task)) for error_task in tasks if error_task["status"] == "error"]
# TODO: если уже была попытка сделать задачу и в редисе она с ошибкой, то переташить её в очередь на выполнение с очисткой состояние об ошибке
if len(error_tasks) > 0:
return JSONResponse({"result": f"STATUS: ERROR {error_tasks[-1]['result']}"})
if len(tasks) > 0:
task = tasks[0]
await red.del_task_from_task_done_queue(task)
break
await asyncio.sleep(5)
except (AttributeError, IndexError):
await asyncio.sleep(5)
continue
link_to_download_video = str(request.base_url) + "get/?file_path=" + task["result"]
# TODO: возможно возвращать идентификаторы задач aka куски ссылок
return JSONResponse({"result": link_to_download_video})
logger.info(f" [x] Sent '{link}'")
# TODO: возможно возвращать идентификаторы задач aka куски ссылок
return JSONResponse(status_code=200, content={"result": f"Задача поставлена в работу, ссылка: {link['link']}"})
# TODO: если уже была попытка сделать задачу и в редисе она с ошибкой, то переташить её в очередь на
# выполнение с очисткой состояние об ошибке
@app.get('/get/', response_class=FileResponse, status_code=200)
@ -133,4 +130,46 @@ async def download_video(file_path):
return StreamingResponse(iterfile(), media_type="video/mp4")
uvicorn.run("src.web.main:app", host="0.0.0.0", log_level="info")
@app.get('/check/', response_class=FileResponse, status_code=200)
async def download_video(request: Request, link: str):
try:
red = RedisClient()
messages_task_done = await red.get_task_done_queue()
messages_tasks = await red.get_tasks()
tasks_done = [
literal_eval(message.decode('utf-8')) for message in messages_task_done
if literal_eval(message.decode('utf-8'))["link"] == link
]
tasks = [
literal_eval(message.decode('utf-8')) for message in messages_tasks
if literal_eval(message.decode('utf-8'))["link"] == link
]
error_tasks = [
tasks_done.pop(tasks_done.index(error_task)) for error_task in tasks_done if error_task["status"] == "error"
]
if len(tasks) > 0:
task = tasks[0]
return JSONResponse(
status_code=202,
content={"result": f"Задача {task['link']} в данный момент в работе, выполняется"}
)
# TODO: если уже была попытка сделать задачу и в редисе она с ошибкой, то переташить её в очередь на выполнение с очисткой состояние об ошибке
if len(error_tasks) > 0:
error_task = error_tasks[0]
await red.del_task_from_task_done_queue(error_task)
return JSONResponse(status_code=510,
content={"result": f"Задача выполнена с ошибкой, попробуйте загрузить еще раз"})
if len(tasks_done) > 0:
link_to_download_video = str(request.base_url) + "get/?file_path=" + tasks_done[0]["result"]
return JSONResponse({"result": link_to_download_video})
except (AttributeError, IndexError):
return JSONResponse(status_code=404, content={"result": "Задача не найдена"})
except Exception as ex:
print(ex)
if __name__ == '__main__':
uvicorn.run("src.web.main:app", host="0.0.0.0", log_level="info")

View File

@ -9,6 +9,7 @@ vext: Video Extension (mp4 > mov > webm > flv > other). If --prefer-free-formats
aext: Audio Extension (m4a > aac > mp3 > ogg > opus > webm > other). If --prefer-free-formats is used, the order changes to ogg > opus > webm > mp3 > m4a > aac
'''
class VideoFormatEnum(Enum):
format_3gp = "3gp"
format_flv = "flv"
@ -16,6 +17,7 @@ class VideoFormatEnum(Enum):
format_mov = "mov"
format_webm = "webm"
class AudioFormatEnum(Enum):
format_aac = "aac"
format_m4a = "m4a"
@ -34,6 +36,7 @@ class MergeOutputFormatEnum(Enum):
format_mp4 = "mp4"
format_webm = "webm"
@dataclass
class SubmitIn:
link: str = Form(...)

View File

@ -71,7 +71,7 @@
</style>
<body>
<form method="post" action="/submit" id="download">
<input type="text" name="link" placeholder="link">
<input type="text" name="link" id="link" placeholder="link">
<input type="text" name="video_format" placeholder="video_format">
<input type="text" name="audio_format" placeholder="audio_format">
<input type="text" name="merge_output_format" placeholder="merge_output_format">
@ -87,10 +87,39 @@
</div>
</body>
<script>
function sendReq() {
document.forms.download.querySelector('[type="submit"]').disabled = true;
document.forms.download.querySelector('.submit-spinner').classList.remove('submit-spinner_hide');
const link = document.getElementById("link").value
const xhr2 = new XMLHttpRequest();
xhr2.open('GET', 'http://0.0.0.0:8000/check/?link=' + link);
xhr2.responseType = 'json';
xhr2.onload = function() {
if (xhr2.status !== 200) {
if ('response' in xhr2 && xhr2.response !== null) {
console.log(xhr2.response)
result.innerHTML = xhr2.response.result;
result.href = xhr2.response.result;
};
setTimeout(sendReq, 5000);
} else if (xhr2.status === 200) {
result.innerHTML = xhr2.response.result;
result.href = xhr2.response.result;
document.forms.download.querySelector('[type="submit"]').disabled = false;
document.forms.download.querySelector('.submit-spinner').classList.add('submit-spinner_hide');
};
};
xhr2.send()
}
function sendForm() {
const xhr = new XMLHttpRequest();
xhr.open('POST', document.forms.download.action);
xhr.responseType = 'json';
xhr.onload = () => {
document.forms.download.querySelector('[type="submit"]').disabled = false;
document.forms.download.querySelector('.submit-spinner').classList.add('submit-spinner_hide');
@ -108,11 +137,17 @@
document.forms.download.querySelector('[type="submit"]').disabled = true;
document.forms.download.querySelector('.submit-spinner').classList.remove('submit-spinner_hide');
xhr.send(new FormData(document.forms.download));
sendReq()
}
// при отправке формы
document.forms.download.addEventListener('submit', (e) => {
e.preventDefault();
sendForm();
});
</script>
</html>
</html>