minor fixes, added result processing
This commit is contained in:
@ -1,16 +1,16 @@
|
||||
import asyncio
|
||||
import concurrent.futures as pool
|
||||
import subprocess
|
||||
import traceback
|
||||
|
||||
from functools import partial
|
||||
|
||||
from fastapi import HTTPException
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from src.core.async_queue import AsyncQueue
|
||||
from src.core.rabbitmq import get_messages
|
||||
from src.core.rabbitmq import get_messages, publish_message_with_task_done
|
||||
from src.core.redis_client import RedisClient
|
||||
from src.core.result import Result, ResultTypeEnum
|
||||
from src.exceptions.download_exceptions import SiteNotImplementedException
|
||||
from src.exceptions.download_exceptions import FileAlreadyExistException, SiteNotImplementedException
|
||||
from src.parsers.MyMail.my_mail_parser import MyMailParser
|
||||
from src.parsers.Yappy.yappy_parser import YappyParser
|
||||
from src.parsers.base_parser import BaseParser
|
||||
@ -39,55 +39,82 @@ class MasterService:
|
||||
async def create_workers(self):
|
||||
while True:
|
||||
video_params = await self.queue.get()
|
||||
#TODO: позднее написать функцию для определения парсера автоматически
|
||||
# TODO: позднее написать функцию для определения парсера автоматически
|
||||
redis = RedisClient()
|
||||
# TODO: проверить что в редисе задача либо уже выполнена, т.е. сразу отдать ссылку, либо что она ранее была закончена с ошибкой
|
||||
# и проверять словарь self.currently_underway, для надёжности
|
||||
await redis.del_task_from_queue_and_add_to_tasks(task=video_params)
|
||||
self.currently_underway[video_params['link']] = video_params
|
||||
|
||||
download_task = self.loop.run_in_executor(self.executor, partial(
|
||||
MasterService.video_processing_executor, video_params=video_params
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
result = await download_task
|
||||
await redis.del_task_from_tasks_and_add_to_task_done(task={"link": video_params["link"], "result": result})
|
||||
result: Result = await download_task
|
||||
if result.result_type in [ResultTypeEnum.DONE, ResultTypeEnum.EXIST]:
|
||||
await redis.del_task_from_tasks_and_add_to_task_done(task=result.value)
|
||||
await publish_message_with_task_done(task=result.value)
|
||||
self.queue.task_done()
|
||||
else:
|
||||
error_message = {
|
||||
"link": video_params["link"],
|
||||
"result": result.value,
|
||||
"status": "error"
|
||||
}
|
||||
await redis.del_task_from_tasks_and_add_to_task_done(task=error_message)
|
||||
await publish_message_with_task_done(task=error_message)
|
||||
|
||||
if video_params['link'] in self.currently_underway:
|
||||
del self.currently_underway[video_params['link']]
|
||||
# TODO process result
|
||||
'''
|
||||
Result.Done \ Result.Exist - уведомить что задача выполнена, и отослать во вторую очередь сообщений RabbitMQ сообщение об этом
|
||||
Result.Error - в таблице Редиса для выполненых задач, пометить, что это ошибка и уведомить об этом по второй очереди сообщений
|
||||
и потом почистить self.currently_underway
|
||||
'''
|
||||
self.queue.task_done()
|
||||
# Result.Done \ Result.Exist - уведомить что задача выполнена, и отослать во вторую очередь сообщений
|
||||
# RabbitMQ сообщение об этом
|
||||
# Result.Error - в таблице Редиса для выполненых задач, пометить, что это ошибка и уведомить об этом
|
||||
# по второй очереди сообщений и потом почистить self.currently_underway
|
||||
|
||||
@staticmethod
|
||||
def video_download(video_params: dict):
|
||||
downloader: BaseParser | YappyParser | MyMailParser = MasterService.get_parser(video_params)
|
||||
try:
|
||||
result = downloader.video_download()
|
||||
return result
|
||||
except SiteNotImplementedException as ex:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ex.message
|
||||
)
|
||||
result = downloader.video_download()
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_parser(params: dict):
|
||||
parser_mapping = {
|
||||
"MyMailRu": MyMailParser(params),
|
||||
"base": BaseParser(params),
|
||||
"Yappy": YappyParser(params),
|
||||
}
|
||||
return parser_mapping[params["parser"]]
|
||||
try:
|
||||
domain = urlparse(params["link"]).netloc
|
||||
parser_mapping = {
|
||||
"my.mail.ru": MyMailParser(params),
|
||||
"www.youtube.com": BaseParser(params),
|
||||
"vk.com": BaseParser(params),
|
||||
"ok.ru": BaseParser(params),
|
||||
"likee.video": BaseParser(params),
|
||||
"dzen.ru": BaseParser(params),
|
||||
"yappy.media": YappyParser(params),
|
||||
}
|
||||
return parser_mapping[domain]
|
||||
except KeyError:
|
||||
raise SiteNotImplementedException
|
||||
|
||||
@staticmethod
|
||||
def video_processing_executor(video_params: dict):
|
||||
try:
|
||||
result = MasterService.video_download(video_params=video_params)
|
||||
return result
|
||||
return Result(result_type=ResultTypeEnum.DONE, value={
|
||||
"link": video_params["link"],
|
||||
"result": result,
|
||||
"status": "done"
|
||||
})
|
||||
except FileAlreadyExistException as ex:
|
||||
return Result(result_type=ResultTypeEnum.EXIST, value={
|
||||
"link": video_params["link"],
|
||||
"result": ex.message,
|
||||
"status": "exist"
|
||||
})
|
||||
except SiteNotImplementedException as ex:
|
||||
return Result(result_type=ResultTypeEnum.EXCEPTION, value=ex.default_message)
|
||||
|
||||
except Exception as ex:
|
||||
return Result(result_type=ResultTypeEnum.EXCEPTION, value=ex)
|
||||
return Result(result_type=ResultTypeEnum.EXCEPTION, value=traceback.format_exc())
|
||||
# TODO upload to server
|
||||
|
||||
|
||||
@ -95,5 +122,3 @@ def executor_initializer():
|
||||
import setproctitle
|
||||
setproctitle.setproctitle(f'video_downloader_executor_process')
|
||||
return True
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@ import asyncio
|
||||
import json
|
||||
from functools import partial
|
||||
|
||||
from aio_pika import connect
|
||||
from aio_pika import connect, Message, DeliveryMode
|
||||
from aio_pika.abc import AbstractIncomingMessage
|
||||
|
||||
|
||||
@ -26,3 +26,24 @@ async def get_messages(inner_queue) -> None:
|
||||
|
||||
print(" [*] Waiting for messages. To exit press CTRL+C")
|
||||
await asyncio.Future()
|
||||
|
||||
|
||||
async def publish_message_with_task_done(task: dict) -> None:
|
||||
queue_name = "tasks_done"
|
||||
async with await connect("amqp://guest:guest@localhost/") as connection:
|
||||
# Creating channel
|
||||
channel = await connection.channel()
|
||||
|
||||
# Will take no more than 10 messages in advance
|
||||
await channel.set_qos(prefetch_count=1)
|
||||
|
||||
# Declaring queue
|
||||
queue = await channel.declare_queue(queue_name)
|
||||
message = Message(
|
||||
json.dumps(task, indent=4).encode('utf-8'), delivery_mode=DeliveryMode.PERSISTENT,
|
||||
)
|
||||
await channel.default_exchange.publish(
|
||||
message,
|
||||
routing_key=queue_name,
|
||||
)
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
import types
|
||||
from enum import Enum
|
||||
from inspect import Traceback
|
||||
|
||||
|
||||
class ResultTypeEnum(Enum):
|
||||
@ -8,9 +10,9 @@ class ResultTypeEnum(Enum):
|
||||
|
||||
|
||||
class Result:
|
||||
def __init__(self, result_type: ResultTypeEnum, value: Exception | bool = None):
|
||||
def __init__(self, result_type: ResultTypeEnum, value: str | dict = None):
|
||||
self.result_type = result_type
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return f'Result: {self.result_type.value}. Traceback: {self.value if self.value else None}'
|
||||
return f'Result: {self.result_type.value}. Value: {self.value if self.value else None}'
|
||||
|
@ -4,3 +4,9 @@ from src.exceptions.base_exception import DefaultException
|
||||
class SiteNotImplementedException(DefaultException):
|
||||
default_message = "This site is not supported"
|
||||
error_code = "SiteNotImplemented"
|
||||
|
||||
|
||||
class FileAlreadyExistException(DefaultException):
|
||||
default_message = "This file already exist"
|
||||
error_code = "FileAlreadyExist"
|
||||
|
||||
|
@ -1,12 +1,10 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
|
||||
from playwright.sync_api import Playwright
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
from src.core.result import Result, ResultTypeEnum
|
||||
from src.exceptions.download_exceptions import FileAlreadyExistException
|
||||
from src.parsers.base_parser import BaseParser
|
||||
|
||||
|
||||
@ -33,7 +31,7 @@ class MyMailParser(BaseParser):
|
||||
link, title, cookies = self.get_video_link(playwright)
|
||||
|
||||
if os.path.exists(os.path.join(os.getcwd() + f"/downloads/MyMailRu/{title}.mp4")):
|
||||
return f"MyMailRu/{title}.mp4"#Result(result_type=ResultTypeEnum.EXIST)
|
||||
raise FileAlreadyExistException(message=f"MyMailRu/{title}.mp4")
|
||||
|
||||
self.make_sure_path_exists()
|
||||
video_response = requests.get(link, cookies=cookies)
|
||||
|
@ -1,11 +1,9 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from src.core.result import ResultTypeEnum, Result
|
||||
from src.exceptions.download_exceptions import FileAlreadyExistException
|
||||
from src.parsers.base_parser import BaseParser
|
||||
|
||||
|
||||
@ -26,7 +24,7 @@ class YappyParser(BaseParser):
|
||||
link, title = self.get_video_link()
|
||||
|
||||
if os.path.exists(os.path.join(os.getcwd() + f"/downloads/Yappy/{title}.mp4")):
|
||||
return f"Yappy/{title}.mp4"
|
||||
raise FileAlreadyExistException(message=f"Yappy/{title}.mp4")
|
||||
|
||||
video_response = requests.get(link)
|
||||
self.make_sure_path_exists()
|
||||
|
@ -1,11 +1,8 @@
|
||||
import errno
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from src.core.result import ResultTypeEnum, Result
|
||||
from src.core.ydl import VideoDownloader
|
||||
from src.exceptions.download_exceptions import SiteNotImplementedException
|
||||
from src.exceptions.download_exceptions import FileAlreadyExistException
|
||||
|
||||
|
||||
class BaseParser:
|
||||
@ -24,20 +21,12 @@ class BaseParser:
|
||||
}
|
||||
downloader = VideoDownloader(link=self.params["link"], ydl_opts=ydl_opts)
|
||||
video_info = downloader.get_info()
|
||||
#TODO Добавить динамеческое имя директории сервиса для проверки дублирования
|
||||
if os.path.exists(
|
||||
os.path.join(os.getcwd() + f"Youtube/{video_info['id']}_{video_info['width']}.{video_info['ext']}")
|
||||
):
|
||||
return Result(result_type=ResultTypeEnum.EXIST)
|
||||
try:
|
||||
downloader.ydl_opts["quiet"] = False
|
||||
result = downloader.download()
|
||||
return f"{video_info['extractor_key']}/{result['id']}_{result['width']}p.{result['ext']}"
|
||||
except SiteNotImplementedException as ex:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ex.message
|
||||
)
|
||||
path_to_video = f"{video_info['extractor_key']}/{video_info['id']}_{video_info['width']}p.{video_info['ext']}"
|
||||
if os.path.exists(os.path.join(os.getcwd() + "/downloads/" + path_to_video)):
|
||||
raise FileAlreadyExistException(message=path_to_video)
|
||||
downloader.ydl_opts["quiet"] = False
|
||||
downloader.download()
|
||||
return path_to_video
|
||||
|
||||
def make_sure_path_exists(self,):
|
||||
try:
|
||||
|
@ -5,12 +5,13 @@ from ast import literal_eval
|
||||
|
||||
import uvicorn
|
||||
from aio_pika import connect, Message, DeliveryMode
|
||||
from fastapi import FastAPI, Request, Form, HTTPException
|
||||
from fastapi import FastAPI, Request, Depends
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import JSONResponse, FileResponse, StreamingResponse
|
||||
from starlette.templating import Jinja2Templates
|
||||
|
||||
from src.core.redis_client import RedisClient
|
||||
from src.web.schemes.submit import SubmitIn
|
||||
|
||||
app = FastAPI(
|
||||
title="video_downloader", openapi_url=f"/api/v1/openapi.json"
|
||||
@ -27,25 +28,43 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
|
||||
async def is_task_already_done_or_exist(redis: RedisClient, link: str):
|
||||
messages = await redis.get_task_done_queue()
|
||||
|
||||
tasks = [
|
||||
literal_eval(message.decode('utf-8')) for message in messages
|
||||
if literal_eval(message.decode('utf-8'))["link"] == link
|
||||
and literal_eval(message.decode('utf-8'))["status"] in ["done", "exist"]
|
||||
]
|
||||
|
||||
if len(tasks) > 0:
|
||||
task = tasks[0]
|
||||
await redis.del_task_from_task_done_queue(task)
|
||||
return task
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def index(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
|
||||
@app.post('/submit/')
|
||||
async def get_url_for_download_video(request: Request, link: str = Form(...)):
|
||||
connection = await connect("amqp://guest:guest@localhost/")
|
||||
async def get_url_for_download_video(request: Request, data: SubmitIn = Depends()):
|
||||
red = RedisClient()
|
||||
task_done = await is_task_already_done_or_exist(red, data.link)
|
||||
if task_done:
|
||||
link_to_download_video = str(request.base_url) + "get/?file_path=" + task_done["result"]
|
||||
return JSONResponse({"result": link_to_download_video})
|
||||
|
||||
async with connection:
|
||||
async with await connect("amqp://guest:guest@localhost/") as connection:
|
||||
# Creating a channel
|
||||
channel = await connection.channel()
|
||||
body = [
|
||||
|
||||
{
|
||||
"link": link,
|
||||
"parser": "base",
|
||||
"format": "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
|
||||
"merge_output_format": "mp4",
|
||||
"link": data.link,
|
||||
"format": f"bestvideo[ext={data.format.value}]+bestaudio[ext={data.format.value}]/best[ext={data.format.value}]/best",
|
||||
"merge_output_format": data.merge_output_format.value,
|
||||
"outtmpl": f"downloads/%(extractor_key)s/%(id)s_%(width)sp.%(ext)s",
|
||||
}, ]
|
||||
# Sending the message
|
||||
@ -63,13 +82,19 @@ async def get_url_for_download_video(request: Request, link: str = Form(...)):
|
||||
)
|
||||
|
||||
print(f" [x] Sent '{link}'")
|
||||
red = RedisClient()
|
||||
|
||||
while True:
|
||||
try:
|
||||
mes = await red.get_task_done_queue()
|
||||
task = literal_eval(list(mes)[0].decode('utf-8'))
|
||||
if task["link"] == link["link"]:
|
||||
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"]
|
||||
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)
|
||||
|
30
src/web/schemes/submit.py
Normal file
30
src/web/schemes/submit.py
Normal file
@ -0,0 +1,30 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from fastapi import Form
|
||||
|
||||
|
||||
class FormatEnum(Enum):
|
||||
format_3gp = "3gp"
|
||||
format_aac = "aac"
|
||||
format_flv = "flv"
|
||||
format_m4a = "m4a"
|
||||
format_mp3 = "mp3"
|
||||
format_mp4 = "mp4"
|
||||
format_ogg = "ogg"
|
||||
|
||||
|
||||
class MergeOutputFormatEnum(Enum):
|
||||
format_avi = "avi"
|
||||
format_flv = "flv"
|
||||
format_mkv = "mkv"
|
||||
format_mov = "mov"
|
||||
format_mp4 = "mp4"
|
||||
format_webm = "webm"
|
||||
|
||||
@dataclass
|
||||
class SubmitIn:
|
||||
link: str = Form(...)
|
||||
format: FormatEnum = Form(...)
|
||||
merge_output_format: MergeOutputFormatEnum = Form(...)
|
||||
|
@ -71,7 +71,9 @@
|
||||
</style>
|
||||
<body>
|
||||
<form method="post" action="/submit" id="download">
|
||||
<input type="text" name="link">
|
||||
<input type="text" name="link" placeholder="link">
|
||||
<input type="text" name="format" placeholder="format">
|
||||
<input type="text" name="merge_output_format" placeholder="merge_output_format">
|
||||
<button type="submit" class="custom-btn btn-1"><span class="submit-spinner submit-spinner_hide"></span> Download</button>
|
||||
</form>
|
||||
<div class="col">
|
||||
|
Reference in New Issue
Block a user