mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +08:00
Merge pull request #3590 from InfinityPacer/feature/recommend
This commit is contained in:
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends
|
|||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain.bangumi import BangumiChain
|
from app.chain.bangumi import BangumiChain
|
||||||
|
from app.chain.recommend import RecommendChain
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
|
|
||||||
@@ -17,10 +18,7 @@ def calendar(page: int = 1,
|
|||||||
"""
|
"""
|
||||||
浏览Bangumi每日放送
|
浏览Bangumi每日放送
|
||||||
"""
|
"""
|
||||||
medias = BangumiChain().calendar()
|
return RecommendChain().bangumi_calendar(page=page, count=count)
|
||||||
if medias:
|
|
||||||
return [media.to_dict() for media in medias[(page - 1) * count: page * count]]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/credits/{bangumiid}", summary="查询Bangumi演职员表", response_model=List[schemas.MediaPerson])
|
@router.get("/credits/{bangumiid}", summary="查询Bangumi演职员表", response_model=List[schemas.MediaPerson])
|
||||||
|
|||||||
+10
-38
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends
|
|||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain.douban import DoubanChain
|
from app.chain.douban import DoubanChain
|
||||||
|
from app.chain.recommend import RecommendChain
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
from app.schemas import MediaType
|
from app.schemas import MediaType
|
||||||
@@ -40,10 +41,7 @@ def movie_showing(page: int = 1,
|
|||||||
"""
|
"""
|
||||||
浏览豆瓣正在热映
|
浏览豆瓣正在热映
|
||||||
"""
|
"""
|
||||||
movies = DoubanChain().movie_showing(page=page, count=count)
|
return RecommendChain().douban_movie_showing(page=page, count=count)
|
||||||
if movies:
|
|
||||||
return [media.to_dict() for media in movies]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/movies", summary="豆瓣电影", response_model=List[schemas.MediaInfo])
|
@router.get("/movies", summary="豆瓣电影", response_model=List[schemas.MediaInfo])
|
||||||
@@ -55,11 +53,7 @@ def douban_movies(sort: str = "R",
|
|||||||
"""
|
"""
|
||||||
浏览豆瓣电影信息
|
浏览豆瓣电影信息
|
||||||
"""
|
"""
|
||||||
movies = DoubanChain().douban_discover(mtype=MediaType.MOVIE,
|
return RecommendChain().douban_movies(sort=sort, tags=tags, page=page, count=count)
|
||||||
sort=sort, tags=tags, page=page, count=count)
|
|
||||||
if movies:
|
|
||||||
return [media.to_dict() for media in movies]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tvs", summary="豆瓣剧集", response_model=List[schemas.MediaInfo])
|
@router.get("/tvs", summary="豆瓣剧集", response_model=List[schemas.MediaInfo])
|
||||||
@@ -71,11 +65,7 @@ def douban_tvs(sort: str = "R",
|
|||||||
"""
|
"""
|
||||||
浏览豆瓣剧集信息
|
浏览豆瓣剧集信息
|
||||||
"""
|
"""
|
||||||
tvs = DoubanChain().douban_discover(mtype=MediaType.TV,
|
return RecommendChain().douban_tvs(sort=sort, tags=tags, page=page, count=count)
|
||||||
sort=sort, tags=tags, page=page, count=count)
|
|
||||||
if tvs:
|
|
||||||
return [media.to_dict() for media in tvs]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
|
@router.get("/movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
|
||||||
@@ -85,10 +75,7 @@ def movie_top250(page: int = 1,
|
|||||||
"""
|
"""
|
||||||
浏览豆瓣剧集信息
|
浏览豆瓣剧集信息
|
||||||
"""
|
"""
|
||||||
movies = DoubanChain().movie_top250(page=page, count=count)
|
return RecommendChain().douban_movie_top250(page=page, count=count)
|
||||||
if movies:
|
|
||||||
return [media.to_dict() for media in movies]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
|
@router.get("/tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
|
||||||
@@ -98,10 +85,7 @@ def tv_weekly_chinese(page: int = 1,
|
|||||||
"""
|
"""
|
||||||
中国每周剧集口碑榜
|
中国每周剧集口碑榜
|
||||||
"""
|
"""
|
||||||
tvs = DoubanChain().tv_weekly_chinese(page=page, count=count)
|
return RecommendChain().douban_tv_weekly_chinese(page=page, count=count)
|
||||||
if tvs:
|
|
||||||
return [media.to_dict() for media in tvs]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
|
@router.get("/tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
|
||||||
@@ -111,10 +95,7 @@ def tv_weekly_global(page: int = 1,
|
|||||||
"""
|
"""
|
||||||
全球每周剧集口碑榜
|
全球每周剧集口碑榜
|
||||||
"""
|
"""
|
||||||
tvs = DoubanChain().tv_weekly_global(page=page, count=count)
|
return RecommendChain().douban_tv_weekly_global(page=page, count=count)
|
||||||
if tvs:
|
|
||||||
return [media.to_dict() for media in tvs]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo])
|
@router.get("/tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo])
|
||||||
@@ -124,10 +105,7 @@ def tv_animation(page: int = 1,
|
|||||||
"""
|
"""
|
||||||
热门动画剧集
|
热门动画剧集
|
||||||
"""
|
"""
|
||||||
tvs = DoubanChain().tv_animation(page=page, count=count)
|
return RecommendChain().douban_tv_animation(page=page, count=count)
|
||||||
if tvs:
|
|
||||||
return [media.to_dict() for media in tvs]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/movie_hot", summary="豆瓣热门电影", response_model=List[schemas.MediaInfo])
|
@router.get("/movie_hot", summary="豆瓣热门电影", response_model=List[schemas.MediaInfo])
|
||||||
@@ -137,10 +115,7 @@ def movie_hot(page: int = 1,
|
|||||||
"""
|
"""
|
||||||
热门电影
|
热门电影
|
||||||
"""
|
"""
|
||||||
movies = DoubanChain().movie_hot(page=page, count=count)
|
return RecommendChain().douban_movie_hot(page=page, count=count)
|
||||||
if movies:
|
|
||||||
return [media.to_dict() for media in movies]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tv_hot", summary="豆瓣热门电视剧", response_model=List[schemas.MediaInfo])
|
@router.get("/tv_hot", summary="豆瓣热门电视剧", response_model=List[schemas.MediaInfo])
|
||||||
@@ -150,10 +125,7 @@ def tv_hot(page: int = 1,
|
|||||||
"""
|
"""
|
||||||
热门电视剧
|
热门电视剧
|
||||||
"""
|
"""
|
||||||
tvs = DoubanChain().tv_hot(page=page, count=count)
|
return RecommendChain().douban_tv_hot(page=page, count=count)
|
||||||
if tvs:
|
|
||||||
return [media.to_dict() for media in tvs]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/credits/{doubanid}/{type_name}", summary="豆瓣演员阵容", response_model=List[schemas.MediaPerson])
|
@router.get("/credits/{doubanid}/{type_name}", summary="豆瓣演员阵容", response_model=List[schemas.MediaPerson])
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from typing import List, Any
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
|
from app.chain.recommend import RecommendChain
|
||||||
from app.chain.tmdb import TmdbChain
|
from app.chain.tmdb import TmdbChain
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
from app.schemas.types import MediaType
|
from app.schemas.types import MediaType
|
||||||
@@ -108,14 +109,10 @@ def tmdb_movies(sort_by: str = "popularity.desc",
|
|||||||
"""
|
"""
|
||||||
浏览TMDB电影信息
|
浏览TMDB电影信息
|
||||||
"""
|
"""
|
||||||
movies = TmdbChain().tmdb_discover(mtype=MediaType.MOVIE,
|
return RecommendChain().tmdb_movies(sort_by=sort_by,
|
||||||
sort_by=sort_by,
|
|
||||||
with_genres=with_genres,
|
with_genres=with_genres,
|
||||||
with_original_language=with_original_language,
|
with_original_language=with_original_language,
|
||||||
page=page)
|
page=page)
|
||||||
if not movies:
|
|
||||||
return []
|
|
||||||
return [movie.to_dict() for movie in movies]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tvs", summary="TMDB剧集", response_model=List[schemas.MediaInfo])
|
@router.get("/tvs", summary="TMDB剧集", response_model=List[schemas.MediaInfo])
|
||||||
@@ -127,26 +124,19 @@ def tmdb_tvs(sort_by: str = "popularity.desc",
|
|||||||
"""
|
"""
|
||||||
浏览TMDB剧集信息
|
浏览TMDB剧集信息
|
||||||
"""
|
"""
|
||||||
tvs = TmdbChain().tmdb_discover(mtype=MediaType.TV,
|
return RecommendChain().tmdb_tvs(sort_by=sort_by,
|
||||||
sort_by=sort_by,
|
|
||||||
with_genres=with_genres,
|
with_genres=with_genres,
|
||||||
with_original_language=with_original_language,
|
with_original_language=with_original_language,
|
||||||
page=page)
|
page=page)
|
||||||
if not tvs:
|
|
||||||
return []
|
|
||||||
return [tv.to_dict() for tv in tvs]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
|
@router.get("/trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
|
||||||
def tmdb_trending(page: int = 1,
|
def tmdb_trending(page: int = 1,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
浏览TMDB剧集信息
|
TMDB流行趋势
|
||||||
"""
|
"""
|
||||||
infos = TmdbChain().tmdb_trending(page=page)
|
return RecommendChain().tmdb_trending(page=page)
|
||||||
if not infos:
|
|
||||||
return []
|
|
||||||
return [info.to_dict() for info in infos]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{tmdbid}/{season}", summary="TMDB季所有集", response_model=List[schemas.TmdbEpisode])
|
@router.get("/{tmdbid}/{season}", summary="TMDB季所有集", response_model=List[schemas.TmdbEpisode])
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from cachetools import TTLCache
|
||||||
|
from cachetools.keys import hashkey
|
||||||
|
|
||||||
|
from app.chain import ChainBase
|
||||||
|
from app.chain.bangumi import BangumiChain
|
||||||
|
from app.chain.douban import DoubanChain
|
||||||
|
from app.chain.tmdb import TmdbChain
|
||||||
|
from app.log import logger
|
||||||
|
from app.schemas import MediaType
|
||||||
|
from app.utils.common import log_execution_time
|
||||||
|
from app.utils.singleton import Singleton
|
||||||
|
|
||||||
|
# 推荐相关的专用缓存
|
||||||
|
recommend_ttl = 6 * 3600
|
||||||
|
recommend_cache = TTLCache(maxsize=256, ttl=recommend_ttl)
|
||||||
|
|
||||||
|
|
||||||
|
# 推荐缓存装饰器,避免偶发网络获取数据为空时,页面由于缓存为空长时间渲染异常问题
|
||||||
|
def cached_with_empty_check(func: Callable):
|
||||||
|
"""
|
||||||
|
缓存装饰器,用于缓存函数的返回结果,仅在结果非空时进行缓存
|
||||||
|
|
||||||
|
:param func: 被装饰的函数
|
||||||
|
:return: 包装后的函数
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# 使用 cachetools 缓存,构造缓存键
|
||||||
|
cache_key = hashkey(*args, **kwargs)
|
||||||
|
if cache_key in recommend_cache:
|
||||||
|
return recommend_cache[cache_key]
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
# 如果返回值为空,则不缓存
|
||||||
|
if result in [None, [], {}]:
|
||||||
|
return result
|
||||||
|
recommend_cache[cache_key] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class RecommendChain(ChainBase, metaclass=Singleton):
|
||||||
|
"""
|
||||||
|
推荐处理链,单例运行
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.tmdbchain = TmdbChain()
|
||||||
|
self.doubanchain = DoubanChain()
|
||||||
|
self.bangumichain = BangumiChain()
|
||||||
|
|
||||||
|
def refresh_recommend(self):
|
||||||
|
"""
|
||||||
|
刷新推荐
|
||||||
|
"""
|
||||||
|
self.tmdb_movies()
|
||||||
|
self.tmdb_tvs()
|
||||||
|
self.tmdb_trending()
|
||||||
|
self.bangumi_calendar()
|
||||||
|
self.douban_movies()
|
||||||
|
self.douban_tvs()
|
||||||
|
self.douban_movie_top250()
|
||||||
|
self.douban_tv_weekly_chinese()
|
||||||
|
self.douban_tv_weekly_global()
|
||||||
|
self.douban_tv_animation()
|
||||||
|
self.douban_movie_hot()
|
||||||
|
self.douban_tv_hot()
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def tmdb_movies(self, sort_by: str = "popularity.desc", with_genres: str = "",
|
||||||
|
with_original_language: str = "", page: int = 1) -> Any:
|
||||||
|
"""
|
||||||
|
TMDB热门电影
|
||||||
|
"""
|
||||||
|
movies = self.tmdbchain.tmdb_discover(mtype=MediaType.MOVIE,
|
||||||
|
sort_by=sort_by,
|
||||||
|
with_genres=with_genres,
|
||||||
|
with_original_language=with_original_language,
|
||||||
|
page=page)
|
||||||
|
return [movie.to_dict() for movie in movies] if movies else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def tmdb_tvs(self, sort_by: str = "popularity.desc", with_genres: str = "",
|
||||||
|
with_original_language: str = "", page: int = 1) -> Any:
|
||||||
|
"""
|
||||||
|
TMDB热门电视剧
|
||||||
|
"""
|
||||||
|
tvs = self.tmdbchain.tmdb_discover(mtype=MediaType.TV,
|
||||||
|
sort_by=sort_by,
|
||||||
|
with_genres=with_genres,
|
||||||
|
with_original_language=with_original_language,
|
||||||
|
page=page)
|
||||||
|
return [tv.to_dict() for tv in tvs] if tvs else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def tmdb_trending(self, page: int = 1) -> Any:
|
||||||
|
"""
|
||||||
|
TMDB流行趋势
|
||||||
|
"""
|
||||||
|
infos = self.tmdbchain.tmdb_trending(page=page)
|
||||||
|
return [info.to_dict() for info in infos] if infos else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def bangumi_calendar(self, page: int = 1, count: int = 30) -> Any:
|
||||||
|
"""
|
||||||
|
Bangumi每日放送
|
||||||
|
"""
|
||||||
|
medias = self.bangumichain.calendar()
|
||||||
|
return [media.to_dict() for media in medias[(page - 1) * count: page * count]] if medias else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def douban_movie_showing(self, page: int = 1, count: int = 30) -> Any:
|
||||||
|
"""
|
||||||
|
豆瓣正在热映
|
||||||
|
"""
|
||||||
|
movies = self.doubanchain.movie_showing(page=page, count=count)
|
||||||
|
return [media.to_dict() for media in movies] if movies else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def douban_movies(self, sort: str = "R", tags: str = "", page: int = 1, count: int = 30) -> Any:
|
||||||
|
"""
|
||||||
|
豆瓣最新电影
|
||||||
|
"""
|
||||||
|
movies = self.doubanchain.douban_discover(mtype=MediaType.MOVIE,
|
||||||
|
sort=sort, tags=tags, page=page, count=count)
|
||||||
|
return [media.to_dict() for media in movies] if movies else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def douban_tvs(self, sort: str = "R", tags: str = "", page: int = 1, count: int = 30) -> Any:
|
||||||
|
"""
|
||||||
|
豆瓣最新电视剧
|
||||||
|
"""
|
||||||
|
tvs = self.doubanchain.douban_discover(mtype=MediaType.TV,
|
||||||
|
sort=sort, tags=tags, page=page, count=count)
|
||||||
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def douban_movie_top250(self, page: int = 1, count: int = 30) -> Any:
|
||||||
|
"""
|
||||||
|
豆瓣电影TOP250
|
||||||
|
"""
|
||||||
|
movies = self.doubanchain.movie_top250(page=page, count=count)
|
||||||
|
return [media.to_dict() for media in movies] if movies else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def douban_tv_weekly_chinese(self, page: int = 1, count: int = 30) -> Any:
|
||||||
|
"""
|
||||||
|
豆瓣国产剧集榜
|
||||||
|
"""
|
||||||
|
tvs = self.doubanchain.tv_weekly_chinese(page=page, count=count)
|
||||||
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def douban_tv_weekly_global(self, page: int = 1, count: int = 30) -> Any:
|
||||||
|
"""
|
||||||
|
豆瓣全球剧集榜
|
||||||
|
"""
|
||||||
|
tvs = self.doubanchain.tv_weekly_global(page=page, count=count)
|
||||||
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def douban_tv_animation(self, page: int = 1, count: int = 30) -> Any:
|
||||||
|
"""
|
||||||
|
豆瓣热门动漫
|
||||||
|
"""
|
||||||
|
tvs = self.doubanchain.tv_animation(page=page, count=count)
|
||||||
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def douban_movie_hot(self, page: int = 1, count: int = 30) -> Any:
|
||||||
|
"""
|
||||||
|
豆瓣热门电影
|
||||||
|
"""
|
||||||
|
movies = self.doubanchain.movie_hot(page=page, count=count)
|
||||||
|
return [media.to_dict() for media in movies] if movies else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached_with_empty_check
|
||||||
|
def douban_tv_hot(self, page: int = 1, count: int = 30) -> Any:
|
||||||
|
"""
|
||||||
|
豆瓣热门电视剧
|
||||||
|
"""
|
||||||
|
tvs = self.doubanchain.tv_hot(page=page, count=count)
|
||||||
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
+26
-1
@@ -1,5 +1,6 @@
|
|||||||
import time
|
import time
|
||||||
from typing import Any
|
from functools import wraps
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
from app.schemas import ImmediateException
|
from app.schemas import ImmediateException
|
||||||
|
|
||||||
@@ -36,3 +37,27 @@ def retry(ExceptionToCheck: Any,
|
|||||||
return f_retry
|
return f_retry
|
||||||
|
|
||||||
return deco_retry
|
return deco_retry
|
||||||
|
|
||||||
|
|
||||||
|
def log_execution_time(logger: Any = None):
|
||||||
|
"""
|
||||||
|
记录函数执行时间的装饰器
|
||||||
|
:param logger: 日志记录器对象,用于记录异常信息
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: Callable):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
start_time = time.time()
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
end_time = time.time()
|
||||||
|
msg = f"{func.__name__} execution time: {end_time - start_time:.2f} seconds"
|
||||||
|
if logger:
|
||||||
|
logger.debug(msg)
|
||||||
|
else:
|
||||||
|
print(msg)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|||||||
Reference in New Issue
Block a user