feat: supported link convert page

This commit is contained in:
Hehesheng 2024-06-08 12:12:37 +08:00
parent 33d45cf2de
commit 200d7749b7
5 changed files with 73 additions and 50 deletions

View File

@ -7,7 +7,7 @@ import asyncio
import traceback import traceback
import hashlib import hashlib
import collections import collections
from typing import Union, Optional, Callable from typing import Union, Optional
import diskcache import diskcache
from fastapi import Request from fastapi import Request
@ -45,19 +45,17 @@ class MediaChunkHolder(object):
requesters: list[Request] = [] requesters: list[Request] = []
unique_id: str = "" unique_id: str = ""
info: ChunkInfo info: ChunkInfo
callback: Callable = None
@staticmethod @staticmethod
def generate_id(chat_id: int, msg_id: int, start: int) -> str: def generate_id(chat_id: int, msg_id: int, start: int) -> str:
return f"{chat_id}:{msg_id}:{start}" return f"{chat_id}:{msg_id}:{start}"
def __init__(self, chat_id: int, msg_id: int, start: int, target_len: int, callback: Callable = None) -> None: def __init__(self, chat_id: int, msg_id: int, start: int, target_len: int) -> None:
self.unique_id = MediaChunkHolder.generate_id(chat_id, msg_id, start) self.unique_id = MediaChunkHolder.generate_id(chat_id, msg_id, start)
self.info = ChunkInfo(hashlib.md5(self.unique_id.encode()).hexdigest(), chat_id, msg_id, start, target_len) self.info = ChunkInfo(hashlib.md5(self.unique_id.encode()).hexdigest(), chat_id, msg_id, start, target_len)
self.mem = bytes() self.mem = bytes()
self.length = len(self.mem) self.length = len(self.mem)
self.waiters = collections.deque() self.waiters = collections.deque()
self.callback = callback
def __repr__(self) -> str: def __repr__(self) -> str:
return f"MediaChunk,{self.info},unique_id:{self.unique_id}" return f"MediaChunk,{self.info},unique_id:{self.unique_id}"
@ -136,13 +134,6 @@ class MediaChunkHolder(object):
except ValueError: except ValueError:
pass pass
def set_done(self) -> None:
if self.callback is None:
return
callback = self.callback
self.callback = None
callback(self)
def try_clear_waiter_and_requester(self) -> bool: def try_clear_waiter_and_requester(self) -> bool:
if not self.is_completed(): if not self.is_completed():
return False return False
@ -236,15 +227,7 @@ class MediaChunkHolderManager(object):
logger.warning(f"remove chunk,{err=},{traceback.format_exc()}") logger.warning(f"remove chunk,{err=},{traceback.format_exc()}")
def create_media_chunk_holder(self, chat_id: int, msg_id: int, start: int, target_len: int) -> MediaChunkHolder: def create_media_chunk_holder(self, chat_id: int, msg_id: int, start: int, target_len: int) -> MediaChunkHolder:
def holder_completed_callback(holder: MediaChunkHolder): return MediaChunkHolder(chat_id, msg_id, start, target_len)
cache_holder = self.incompleted_chunk.pop(holder.chunk_id, None)
if cache_holder is None:
logger.warning(f"the holder not in mem, {holder}")
return
logger.info(f"cache new chunk:{holder}")
self.disk_chunk_cache.set(holder.chunk_id, holder)
return MediaChunkHolder(chat_id, msg_id, start, target_len, callback=holder_completed_callback)
def get_media_chunk(self, msg: types.Message, start: int, lru: bool = True) -> Optional[MediaChunkHolder]: def get_media_chunk(self, msg: types.Message, start: int, lru: bool = True) -> Optional[MediaChunkHolder]:
res = self._get_media_chunk_cache(msg, start) res = self._get_media_chunk_cache(msg, start)
@ -276,3 +259,14 @@ class MediaChunkHolderManager(object):
if dummy is None: if dummy is None:
return return
self._remove_pop_chunk(dummy) self._remove_pop_chunk(dummy)
def move_media_chunk_to_disk(self, holder: MediaChunkHolder) -> bool:
cache_holder = self.incompleted_chunk.pop(holder.chunk_id, None)
if cache_holder is None:
logger.warning(f"the holder not in mem, {holder}")
return False
if not holder.is_completed():
logger.error(f"chunk not completed, but move to disk:{holder=}")
logger.info(f"cache new chunk:{holder}")
self.disk_chunk_cache.set(holder.chunk_id, holder)
return True

View File

@ -335,7 +335,8 @@ class TgFileSystemClient(object):
else: else:
if not media_holder.try_clear_waiter_and_requester(): if not media_holder.try_clear_waiter_and_requester():
logger.error("I think never run here.") logger.error("I think never run here.")
media_holder.set_done() if not self.media_chunk_manager.move_media_chunk_to_disk(media_holder):
logger.warning(f"move to disk failed, {media_holder=}")
logger.debug(f"downloaded chunk:{offset=},{target_size=},{media_holder}") logger.debug(f"downloaded chunk:{offset=},{target_size=},{media_holder}")
finally: finally:
pass pass

View File

@ -19,6 +19,12 @@ class TgFileSystemClientManager(object):
param: configParse.TgToFileSystemParameter param: configParse.TgToFileSystemParameter
clients: dict[str, TgFileSystemClient] = {} clients: dict[str, TgFileSystemClient] = {}
@classmethod
def get_instance(cls):
if not hasattr(TgFileSystemClientManager, "_instance"):
TgFileSystemClientManager._instance = TgFileSystemClientManager(configParse.get_TgToFileSystemParameter())
return TgFileSystemClientManager._instance
def __init__(self, param: configParse.TgToFileSystemParameter) -> None: def __init__(self, param: configParse.TgToFileSystemParameter) -> None:
self.param = param self.param = param
self.db = UserManager() self.db = UserManager()

View File

@ -15,23 +15,20 @@ from pydantic import BaseModel
import configParse import configParse
from backend import apiutils from backend import apiutils
from backend import api_implement as api
from backend.TgFileSystemClientManager import TgFileSystemClientManager from backend.TgFileSystemClientManager import TgFileSystemClientManager
logger = logging.getLogger(__file__.split("/")[-1]) logger = logging.getLogger(__file__.split("/")[-1])
clients_mgr: TgFileSystemClientManager = None
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
for handler in logging.getLogger().handlers: clients_mgr = TgFileSystemClientManager.get_instance()
if isinstance(handler, logging.handlers.TimedRotatingFileHandler): res = await clients_mgr.get_status()
handler.suffix = "%Y-%m-%d" logger.info(f"init clients manager:{res}")
global clients_mgr
param = configParse.get_TgToFileSystemParameter()
clients_mgr = TgFileSystemClientManager(param)
yield yield
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
app.add_middleware( app.add_middleware(
@ -57,6 +54,7 @@ class TgToFileListRequestBody(BaseModel):
async def search_tg_file_list(body: TgToFileListRequestBody): async def search_tg_file_list(body: TgToFileListRequestBody):
try: try:
param = configParse.get_TgToFileSystemParameter() param = configParse.get_TgToFileSystemParameter()
clients_mgr = TgFileSystemClientManager.get_instance()
res = hints.TotalList() res = hints.TotalList()
res_type = "msg" res_type = "msg"
client = await clients_mgr.get_client_force(body.token) client = await clients_mgr.get_client_force(body.token)
@ -91,6 +89,7 @@ async def search_tg_file_list(body: TgToFileListRequestBody):
@apiutils.atimeit @apiutils.atimeit
async def get_tg_file_list(body: TgToFileListRequestBody): async def get_tg_file_list(body: TgToFileListRequestBody):
try: try:
clients_mgr = TgFileSystemClientManager.get_instance()
res = hints.TotalList() res = hints.TotalList()
res_type = "chat" res_type = "chat"
client = await clients_mgr.get_client_force(body.token) client = await clients_mgr.get_client_force(body.token)
@ -138,6 +137,7 @@ async def get_tg_file_media_stream(token: str, cid: int, mid: int, request: Requ
} }
range_header = request.headers.get("range") range_header = request.headers.get("range")
try: try:
clients_mgr = TgFileSystemClientManager.get_instance()
client = await clients_mgr.get_client_force(token) client = await clients_mgr.get_client_force(token)
msg = await client.get_message(chat_id, msg_id) msg = await client.get_message(chat_id, msg_id)
file_size = msg.media.document.size file_size = msg.media.document.size
@ -189,12 +189,14 @@ async def get_tg_file_media(chat_id: int|str, msg_id: int, file_name: str, sign:
@app.get("/tg/api/v1/client/login") @app.get("/tg/api/v1/client/login")
@apiutils.atimeit @apiutils.atimeit
async def login_new_tg_file_client(): async def login_new_tg_file_client():
clients_mgr = TgFileSystemClientManager.get_instance()
url = await clients_mgr.login_clients() url = await clients_mgr.login_clients()
return {"url": url} return {"url": url}
@app.get("/tg/api/v1/client/status") @app.get("/tg/api/v1/client/status")
async def get_tg_file_client_status(request: Request): async def get_tg_file_client_status(request: Request):
clients_mgr = TgFileSystemClientManager.get_instance()
return await clients_mgr.get_status() return await clients_mgr.get_status()
@ -202,27 +204,7 @@ async def get_tg_file_client_status(request: Request):
@apiutils.atimeit @apiutils.atimeit
async def convert_tg_msg_link_media_stream(link: str): async def convert_tg_msg_link_media_stream(link: str):
try: try:
link_slice = link.split("/") url = await api.link_convert(link)
if len(link_slice) < 5:
raise RuntimeError("link format invalid")
chat_id_or_name, msg_id = link_slice[-2:]
is_msg_id = msg_id.isascii() and msg_id.isdecimal()
if not is_msg_id:
raise RuntimeError("message id invalid")
msg_id = int(msg_id)
is_chat_name = chat_id_or_name.isascii() and not chat_id_or_name.isdecimal()
is_chat_id = chat_id_or_name.isascii() and chat_id_or_name.isdecimal()
if not is_chat_name and not is_chat_id:
raise RuntimeError("chat id invalid")
client = clients_mgr.get_first_client()
if client is None:
raise RuntimeError("client not ready, login first pls.")
if is_chat_id:
chat_id_or_name = int(chat_id_or_name)
msg = await client.get_message(chat_id_or_name, msg_id)
file_name = apiutils.get_message_media_name(msg)
param = configParse.get_TgToFileSystemParameter()
url = f"{param.base.exposed_url}/tg/api/v1/file/get/{utils.get_peer_id(msg.peer_id)}/{msg.id}/{file_name}?sign={client.sign}"
logger.info(f"{link}: link convert to: {url}") logger.info(f"{link}: link convert to: {url}")
return Response(json.dumps({"url": url}), status_code=status.HTTP_200_OK) return Response(json.dumps({"url": url}), status_code=status.HTTP_200_OK)
except Exception as err: except Exception as err:
@ -247,6 +229,7 @@ class TgToChatListRequestBody(BaseModel):
@apiutils.atimeit @apiutils.atimeit
async def get_tg_client_chat_list(body: TgToChatListRequestBody, request: Request): async def get_tg_client_chat_list(body: TgToChatListRequestBody, request: Request):
try: try:
clients_mgr = TgFileSystemClientManager.get_instance()
res = hints.TotalList() res = hints.TotalList()
res_type = "chat" res_type = "chat"
client = await clients_mgr.get_client_force(body.token) client = await clients_mgr.get_client_force(body.token)

39
backend/api_implement.py Normal file
View File

@ -0,0 +1,39 @@
import traceback
import logging
from telethon import types, hints, utils
import configParse
from backend import apiutils
from backend.TgFileSystemClientManager import TgFileSystemClientManager
logger = logging.getLogger(__file__.split("/")[-1])
async def link_convert(link: str) -> str:
clients_mgr = TgFileSystemClientManager.get_instance()
link_slice = link.split("/")
if len(link_slice) < 5:
raise RuntimeError("link format invalid")
chat_id_or_name, msg_id = link_slice[-2:]
is_msg_id = msg_id.isascii() and msg_id.isdecimal()
if not is_msg_id:
raise RuntimeError("message id invalid")
msg_id = int(msg_id)
is_chat_name = chat_id_or_name.isascii() and not chat_id_or_name.isdecimal()
is_chat_id = chat_id_or_name.isascii() and chat_id_or_name.isdecimal()
if not is_chat_name and not is_chat_id:
raise RuntimeError("chat id invalid")
client = clients_mgr.get_first_client()
if client is None:
raise RuntimeError("client not ready, login first pls.")
if is_chat_id:
chat_id_or_name = int(chat_id_or_name)
msg = await client.get_message(chat_id_or_name, msg_id)
file_name = apiutils.get_message_media_name(msg)
param = configParse.get_TgToFileSystemParameter()
url = (
f"{param.base.exposed_url}/tg/api/v1/file/get/{utils.get_peer_id(msg.peer_id)}/{msg.id}/{file_name}?sign={client.sign}"
)
return url