Compare commits

...

15 Commits

Author SHA1 Message Date
jxxghp
5f96a562d4 v1.3.2 2023-10-12 20:11:22 +08:00
jxxghp
cefbd70469 fix #807 2023-10-12 20:06:58 +08:00
jxxghp
30c9c66087 fix 豆瓣来源订阅 2023-10-12 19:59:47 +08:00
jxxghp
1ecbc2f0be fix bug 2023-10-12 18:08:09 +08:00
jxxghp
884a0feb62 fix bug 2023-10-12 17:56:15 +08:00
jxxghp
5f44f07515 fixme 2023-10-12 17:51:20 +08:00
jxxghp
a902b79684 fix #800 2023-10-12 17:07:26 +08:00
jxxghp
4e13f59b36 fix #804 2023-10-12 16:03:36 +08:00
jxxghp
cbccac87f0 feat 清理无效的插件事件响应 2023-10-12 12:48:40 +08:00
jxxghp
eb3c09a3d3 fix bug 2023-10-12 11:50:22 +08:00
jxxghp
2a9a36ac88 feat 辅助识别异步接口 && ChatGPT插件支持辅助名称识别 2023-10-12 11:41:57 +08:00
jxxghp
af2f52a050 fix 优先级规则支持杜比全景声 2023-10-12 09:46:48 +08:00
jxxghp
7a61fa1ee2 feat 订阅支持更多过滤规则 2023-10-12 08:58:59 +08:00
jxxghp
ac3009d58f fix bug 2023-10-12 08:40:56 +08:00
jxxghp
e835feb056 更新 douban.py 2023-10-11 18:11:13 +08:00
39 changed files with 715 additions and 280 deletions

View File

@@ -1,14 +1,12 @@
from typing import List, Any
from fastapi import APIRouter, Depends, Response
from sqlalchemy.orm import Session
from app import schemas
from app.chain.douban import DoubanChain
from app.core.config import settings
from app.core.context import MediaInfo
from app.core.security import verify_token
from app.db import get_db
from app.schemas import MediaType
from app.utils.http import RequestUtils
@@ -32,13 +30,12 @@ def douban_img(imgurl: str) -> Any:
@router.get("/recognize/{doubanid}", summary="豆瓣ID识别", response_model=schemas.Context)
def recognize_doubanid(doubanid: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据豆瓣ID识别媒体信息
"""
# 识别媒体信息
context = DoubanChain(db).recognize_by_doubanid(doubanid=doubanid)
context = DoubanChain().recognize_by_doubanid(doubanid=doubanid)
if context:
return context.to_dict()
else:
@@ -48,12 +45,11 @@ def recognize_doubanid(doubanid: str,
@router.get("/showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo])
def movie_showing(page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣正在热映
"""
movies = DoubanChain(db).movie_showing(page=page, count=count)
movies = DoubanChain().movie_showing(page=page, count=count)
if not movies:
return []
medias = [MediaInfo(douban_info=movie) for movie in movies]
@@ -65,13 +61,12 @@ def douban_movies(sort: str = "R",
tags: str = "",
page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣电影信息
"""
movies = DoubanChain(db).douban_discover(mtype=MediaType.MOVIE,
sort=sort, tags=tags, page=page, count=count)
movies = DoubanChain().douban_discover(mtype=MediaType.MOVIE,
sort=sort, tags=tags, page=page, count=count)
if not movies:
return []
medias = [MediaInfo(douban_info=movie) for movie in movies]
@@ -86,13 +81,12 @@ def douban_tvs(sort: str = "R",
tags: str = "",
page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣剧集信息
"""
tvs = DoubanChain(db).douban_discover(mtype=MediaType.TV,
sort=sort, tags=tags, page=page, count=count)
tvs = DoubanChain().douban_discover(mtype=MediaType.TV,
sort=sort, tags=tags, page=page, count=count)
if not tvs:
return []
medias = [MediaInfo(douban_info=tv) for tv in tvs]
@@ -106,59 +100,54 @@ def douban_tvs(sort: str = "R",
@router.get("/movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
def movie_top250(page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣剧集信息
"""
movies = DoubanChain(db).movie_top250(page=page, count=count)
movies = DoubanChain().movie_top250(page=page, count=count)
return [MediaInfo(douban_info=movie).to_dict() for movie in movies]
@router.get("/tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
def tv_weekly_chinese(page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
中国每周剧集口碑榜
"""
tvs = DoubanChain(db).tv_weekly_chinese(page=page, count=count)
tvs = DoubanChain().tv_weekly_chinese(page=page, count=count)
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
@router.get("/tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
def tv_weekly_global(page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
全球每周剧集口碑榜
"""
tvs = DoubanChain(db).tv_weekly_global(page=page, count=count)
tvs = DoubanChain().tv_weekly_global(page=page, count=count)
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
@router.get("/tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo])
def tv_animation(page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
热门动画剧集
"""
tvs = DoubanChain(db).tv_animation(page=page, count=count)
tvs = DoubanChain().tv_animation(page=page, count=count)
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
@router.get("/{doubanid}", summary="查询豆瓣详情", response_model=schemas.MediaInfo)
def douban_info(doubanid: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据豆瓣ID查询豆瓣媒体信息
"""
doubaninfo = DoubanChain(db).douban_info(doubanid=doubanid)
doubaninfo = DoubanChain().douban_info(doubanid=doubanid)
if doubaninfo:
return MediaInfo(douban_info=doubaninfo).to_dict()
else:

View File

@@ -68,12 +68,12 @@ def exists(media_in: schemas.MediaInfo,
if media_in.tmdb_id:
mediainfo.from_dict(media_in.dict())
elif media_in.douban_id:
context = DoubanChain(db).recognize_by_doubanid(doubanid=media_in.douban_id)
context = DoubanChain().recognize_by_doubanid(doubanid=media_in.douban_id)
if context:
mediainfo = context.media_info
meta = context.meta_info
else:
context = MediaChain(db).recognize_by_title(title=f"{media_in.title} {media_in.year}")
context = MediaChain().recognize_by_title(title=f"{media_in.title} {media_in.year}")
if context:
mediainfo = context.media_info
meta = context.meta_info

View File

@@ -74,11 +74,11 @@ def bing_wallpaper() -> Any:
@router.get("/tmdb", summary="TMDB电影海报", response_model=schemas.Response)
def tmdb_wallpaper(db: Session = Depends(get_db)) -> Any:
def tmdb_wallpaper() -> Any:
"""
获取TMDB电影海报
"""
wallpager = TmdbChain(db).get_random_wallpager()
wallpager = TmdbChain().get_random_wallpager()
if wallpager:
return schemas.Response(
success=True,

View File

@@ -20,13 +20,12 @@ router = APIRouter()
@router.get("/recognize", summary="识别媒体信息(种子)", response_model=schemas.Context)
def recognize(title: str,
subtitle: str = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据标题、副标题识别媒体信息
"""
# 识别媒体信息
context = MediaChain(db).recognize_by_title(title=title, subtitle=subtitle)
context = MediaChain().recognize_by_title(title=title, subtitle=subtitle)
if context:
return context.to_dict()
return schemas.Context()
@@ -34,13 +33,12 @@ def recognize(title: str,
@router.get("/recognize_file", summary="识别媒体信息(文件)", response_model=schemas.Context)
def recognize(path: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据文件路径识别媒体信息
"""
# 识别媒体信息
context = MediaChain(db).recognize_by_path(path)
context = MediaChain().recognize_by_path(path)
if context:
return context.to_dict()
return schemas.Context()
@@ -50,12 +48,11 @@ def recognize(path: str,
def search_by_title(title: str,
page: int = 1,
count: int = 8,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
模糊搜索媒体信息列表
"""
_, medias = MediaChain(db).search(title=title)
_, medias = MediaChain().search(title=title)
if medias:
return [media.to_dict() for media in medias[(page - 1) * count: page * count]]
return []
@@ -85,21 +82,20 @@ def exists(title: str = None,
@router.get("/{mediaid}", summary="查询媒体详情", response_model=schemas.MediaInfo)
def tmdb_info(mediaid: str, type_name: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据媒体ID查询themoviedb或豆瓣媒体信息type_name: 电影/电视剧
"""
mtype = MediaType(type_name)
if mediaid.startswith("tmdb:"):
result = TmdbChain(db).tmdb_info(int(mediaid[5:]), mtype)
result = TmdbChain().tmdb_info(int(mediaid[5:]), mtype)
return MediaInfo(tmdb_info=result).to_dict()
elif mediaid.startswith("douban:"):
# 查询豆瓣信息
doubaninfo = DoubanChain(db).douban_info(doubanid=mediaid[7:])
doubaninfo = DoubanChain().douban_info(doubanid=mediaid[7:])
if not doubaninfo:
return schemas.MediaInfo()
result = DoubanChain(db).recognize_by_doubaninfo(doubaninfo)
result = DoubanChain().recognize_by_doubaninfo(doubaninfo)
if result:
# TMDB
return result.media_info.to_dict()

View File

@@ -40,7 +40,7 @@ def search_by_tmdbid(mediaid: str,
elif mediaid.startswith("douban:"):
doubanid = mediaid.replace("douban:", "")
# 识别豆瓣信息
context = DoubanChain(db).recognize_by_doubanid(doubanid)
context = DoubanChain().recognize_by_doubanid(doubanid)
if not context or not context.media_info or not context.media_info.tmdb_id:
return []
torrents = SearchChain(db).search_by_tmdbid(tmdbid=context.media_info.tmdb_id,

View File

@@ -223,7 +223,7 @@ def execute_command(jobid: str,
if not jobid:
return schemas.Response(success=False, message="命令不能为空!")
if jobid == "subscribe_search":
Scheduler().start(jobid, state = 'R')
Scheduler().start(jobid, state='R')
else:
Scheduler().start(jobid)
return schemas.Response(success=True)
return schemas.Response(success=True)

View File

@@ -1,25 +1,22 @@
from typing import List, Any
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app import schemas
from app.chain.tmdb import TmdbChain
from app.core.context import MediaInfo
from app.core.security import verify_token
from app.db import get_db
from app.schemas.types import MediaType
router = APIRouter()
@router.get("/seasons/{tmdbid}", summary="TMDB所有季", response_model=List[schemas.TmdbSeason])
def tmdb_seasons(tmdbid: int, db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
def tmdb_seasons(tmdbid: int, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询themoviedb所有季信息
"""
seasons_info = TmdbChain(db).tmdb_seasons(tmdbid=tmdbid)
seasons_info = TmdbChain().tmdb_seasons(tmdbid=tmdbid)
if not seasons_info:
return []
else:
@@ -29,16 +26,15 @@ def tmdb_seasons(tmdbid: int, db: Session = Depends(get_db),
@router.get("/similar/{tmdbid}/{type_name}", summary="类似电影/电视剧", response_model=List[schemas.MediaInfo])
def tmdb_similar(tmdbid: int,
type_name: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询类似电影/电视剧type_name: 电影/电视剧
"""
mediatype = MediaType(type_name)
if mediatype == MediaType.MOVIE:
tmdbinfos = TmdbChain(db).movie_similar(tmdbid=tmdbid)
tmdbinfos = TmdbChain().movie_similar(tmdbid=tmdbid)
elif mediatype == MediaType.TV:
tmdbinfos = TmdbChain(db).tv_similar(tmdbid=tmdbid)
tmdbinfos = TmdbChain().tv_similar(tmdbid=tmdbid)
else:
return []
if not tmdbinfos:
@@ -50,16 +46,15 @@ def tmdb_similar(tmdbid: int,
@router.get("/recommend/{tmdbid}/{type_name}", summary="推荐电影/电视剧", response_model=List[schemas.MediaInfo])
def tmdb_recommend(tmdbid: int,
type_name: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询推荐电影/电视剧type_name: 电影/电视剧
"""
mediatype = MediaType(type_name)
if mediatype == MediaType.MOVIE:
tmdbinfos = TmdbChain(db).movie_recommend(tmdbid=tmdbid)
tmdbinfos = TmdbChain().movie_recommend(tmdbid=tmdbid)
elif mediatype == MediaType.TV:
tmdbinfos = TmdbChain(db).tv_recommend(tmdbid=tmdbid)
tmdbinfos = TmdbChain().tv_recommend(tmdbid=tmdbid)
else:
return []
if not tmdbinfos:
@@ -72,16 +67,15 @@ def tmdb_recommend(tmdbid: int,
def tmdb_credits(tmdbid: int,
type_name: str,
page: int = 1,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询演员阵容type_name: 电影/电视剧
"""
mediatype = MediaType(type_name)
if mediatype == MediaType.MOVIE:
tmdbinfos = TmdbChain(db).movie_credits(tmdbid=tmdbid, page=page)
tmdbinfos = TmdbChain().movie_credits(tmdbid=tmdbid, page=page)
elif mediatype == MediaType.TV:
tmdbinfos = TmdbChain(db).tv_credits(tmdbid=tmdbid, page=page)
tmdbinfos = TmdbChain().tv_credits(tmdbid=tmdbid, page=page)
else:
return []
if not tmdbinfos:
@@ -92,12 +86,11 @@ def tmdb_credits(tmdbid: int,
@router.get("/person/{person_id}", summary="人物详情", response_model=schemas.TmdbPerson)
def tmdb_person(person_id: int,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据人物ID查询人物详情
"""
tmdbinfo = TmdbChain(db).person_detail(person_id=person_id)
tmdbinfo = TmdbChain().person_detail(person_id=person_id)
if not tmdbinfo:
return schemas.TmdbPerson()
else:
@@ -107,12 +100,11 @@ def tmdb_person(person_id: int,
@router.get("/person/credits/{person_id}", summary="人物参演作品", response_model=List[schemas.MediaInfo])
def tmdb_person_credits(person_id: int,
page: int = 1,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据人物ID查询人物参演作品
"""
tmdbinfo = TmdbChain(db).person_credits(person_id=person_id, page=page)
tmdbinfo = TmdbChain().person_credits(person_id=person_id, page=page)
if not tmdbinfo:
return []
else:
@@ -124,16 +116,15 @@ def tmdb_movies(sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "",
page: int = 1,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB电影信息
"""
movies = TmdbChain(db).tmdb_discover(mtype=MediaType.MOVIE,
sort_by=sort_by,
with_genres=with_genres,
with_original_language=with_original_language,
page=page)
movies = TmdbChain().tmdb_discover(mtype=MediaType.MOVIE,
sort_by=sort_by,
with_genres=with_genres,
with_original_language=with_original_language,
page=page)
if not movies:
return []
return [MediaInfo(tmdb_info=movie).to_dict() for movie in movies]
@@ -144,16 +135,15 @@ def tmdb_tvs(sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "",
page: int = 1,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB剧集信息
"""
tvs = TmdbChain(db).tmdb_discover(mtype=MediaType.TV,
sort_by=sort_by,
with_genres=with_genres,
with_original_language=with_original_language,
page=page)
tvs = TmdbChain().tmdb_discover(mtype=MediaType.TV,
sort_by=sort_by,
with_genres=with_genres,
with_original_language=with_original_language,
page=page)
if not tvs:
return []
return [MediaInfo(tmdb_info=tv).to_dict() for tv in tvs]
@@ -161,12 +151,11 @@ def tmdb_tvs(sort_by: str = "popularity.desc",
@router.get("/trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
def tmdb_trending(page: int = 1,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB剧集信息
"""
infos = TmdbChain(db).tmdb_trending(page=page)
infos = TmdbChain().tmdb_trending(page=page)
if not infos:
return []
return [MediaInfo(tmdb_info=info).to_dict() for info in infos]
@@ -174,12 +163,11 @@ def tmdb_trending(page: int = 1,
@router.get("/{tmdbid}/{season}", summary="TMDB季所有集", response_model=List[schemas.TmdbEpisode])
def tmdb_season_episodes(tmdbid: int, season: int,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询某季的所有信信息
"""
episodes_info = TmdbChain(db).tmdb_episodes(tmdbid=tmdbid, season=season)
episodes_info = TmdbChain().tmdb_episodes(tmdbid=tmdbid, season=season)
if not episodes_info:
return []
else:

View File

@@ -301,11 +301,11 @@ def arr_movie_lookup(apikey: str, term: str, db: Session = Depends(get_db)) -> A
)
tmdbid = term.replace("tmdb:", "")
# 查询媒体信息
mediainfo = MediaChain(db).recognize_media(mtype=MediaType.MOVIE, tmdbid=int(tmdbid))
mediainfo = MediaChain().recognize_media(mtype=MediaType.MOVIE, tmdbid=int(tmdbid))
if not mediainfo:
return [RadarrMovie()]
# 查询是否已存在
exists = MediaChain(db).media_exists(mediainfo=mediainfo)
exists = MediaChain().media_exists(mediainfo=mediainfo)
if not exists:
# 文件不存在
hasfile = False
@@ -581,7 +581,7 @@ def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db)) ->
# 获取TVDBID
if not term.startswith("tvdb:"):
mediainfo = MediaChain(db).recognize_media(meta=MetaInfo(term),
mediainfo = MediaChain().recognize_media(meta=MetaInfo(term),
mtype=MediaType.TV)
if not mediainfo:
return [SonarrSeries()]
@@ -593,7 +593,7 @@ def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db)) ->
tvdbid = int(term.replace("tvdb:", ""))
# 查询TVDB信息
tvdbinfo = MediaChain(db).tvdb_info(tvdbid=tvdbid)
tvdbinfo = MediaChain().tvdb_info(tvdbid=tvdbid)
if not tvdbinfo:
return [SonarrSeries()]
@@ -605,11 +605,11 @@ def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db)) ->
# 根据TVDB查询媒体信息
if not mediainfo:
mediainfo = MediaChain(db).recognize_media(meta=MetaInfo(tvdbinfo.get('seriesName')),
mediainfo = MediaChain().recognize_media(meta=MetaInfo(tvdbinfo.get('seriesName')),
mtype=MediaType.TV)
# 查询是否存在
exists = MediaChain(db).media_exists(mediainfo)
exists = MediaChain().media_exists(mediainfo)
if exists:
hasfile = True
else:

View File

@@ -115,16 +115,18 @@ class ChainBase(metaclass=ABCMeta):
"""
return self.run_module("recognize_media", meta=meta, mtype=mtype, tmdbid=tmdbid)
def match_doubaninfo(self, name: str, mtype: str = None,
year: str = None, season: int = None) -> Optional[dict]:
def match_doubaninfo(self, name: str, imdbid: str = None,
mtype: str = None, year: str = None, season: int = None) -> Optional[dict]:
"""
搜索和匹配豆瓣信息
:param name: 标题
:param imdbid: imdbid
:param mtype: 类型
:param year: 年份
:param season: 季
"""
return self.run_module("match_doubaninfo", name=name, mtype=mtype, year=year, season=season)
return self.run_module("match_doubaninfo", name=name, imdbid=imdbid,
mtype=mtype, year=year, season=season)
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
"""

View File

@@ -6,11 +6,12 @@ from app.core.context import MediaInfo
from app.core.metainfo import MetaInfo
from app.log import logger
from app.schemas import MediaType
from app.utils.singleton import Singleton
class DoubanChain(ChainBase):
class DoubanChain(ChainBase, metaclass=Singleton):
"""
豆瓣处理链
豆瓣处理链,单例运行
"""
def recognize_by_doubanid(self, doubanid: str) -> Optional[Context]:
@@ -49,13 +50,12 @@ class DoubanChain(ChainBase):
# 使用原标题识别媒体信息
mediainfo = self.recognize_media(meta=meta, mtype=meta.type)
if not mediainfo:
logger.warn(f'{meta.name} 未识别到TMDB媒体信息')
if season_meta and season_meta.name != meta.name:
# 使用主标题识别媒体信息
mediainfo = self.recognize_media(meta=season_meta, mtype=season_meta.type)
if not mediainfo:
logger.warn(f'{season_meta.name} 未识别到TMDB媒体信息')
return Context(meta_info=season_meta, media_info=MediaInfo(douban_info=doubaninfo))
logger.warn(f'{meta.name} 未识别到TMDB媒体信息')
return Context(meta_info=meta, media_info=MediaInfo(douban_info=doubaninfo))
logger.info(f'识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year} {meta.season}')
mediainfo.set_douban_info(doubaninfo)
return Context(meta_info=meta, media_info=mediainfo)

View File

@@ -1,18 +1,31 @@
import copy
import time
from pathlib import Path
from threading import Lock
from typing import Optional, List, Tuple
from app.chain import ChainBase
from app.core.context import Context, MediaInfo
from app.core.event import eventmanager, Event
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo, MetaInfoPath
from app.log import logger
from app.schemas.types import EventType, MediaType
from app.utils.singleton import Singleton
from app.utils.string import StringUtils
class MediaChain(ChainBase):
recognize_lock = Lock()
class MediaChain(ChainBase, metaclass=Singleton):
"""
媒体信息处理链
媒体信息处理链,单例运行
"""
# 临时识别标题
recognize_title: Optional[str] = None
# 临时识别结果 {title, name, year, season, episode}
recognize_temp: Optional[dict] = None
def recognize_by_title(self, title: str, subtitle: str = None) -> Optional[Context]:
"""
@@ -24,14 +37,104 @@ class MediaChain(ChainBase):
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=metainfo)
if not mediainfo:
logger.warn(f'{title} 未识别到媒体信息')
return Context(meta_info=metainfo)
# 偿试使用辅助识别,如果有注册响应事件的话
if eventmanager.check(EventType.NameRecognize):
logger.info(f'请求辅助识别,标题:{title} ...')
mediainfo = self.recognize_help(title=title, org_meta=metainfo)
if not mediainfo:
logger.warn(f'{title} 未识别到媒体信息')
return Context(meta_info=metainfo)
# 识别成功
logger.info(f'{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
# 更新媒体图片
self.obtain_images(mediainfo=mediainfo)
# 返回上下文
return Context(meta_info=metainfo, media_info=mediainfo)
def recognize_help(self, title: str, org_meta: MetaBase) -> Optional[MediaInfo]:
"""
请求辅助识别,返回媒体信息
:param title: 标题
:param org_meta: 原始元数据
"""
with recognize_lock:
self.recognize_temp = None
self.recognize_title = title
# 发送请求事件
eventmanager.send_event(
EventType.NameRecognize,
{
'title': title,
}
)
# 每0.5秒循环一次等待结果直到10秒后超时
for i in range(10):
if self.recognize_temp is not None:
break
time.sleep(0.5)
# 加锁
with recognize_lock:
mediainfo = None
if not self.recognize_temp or self.recognize_title != title:
# 没有识别结果或者识别标题已改变
return None
# 有识别结果
meta_dict = copy.deepcopy(self.recognize_temp)
logger.info(f'获取到辅助识别结果:{meta_dict}')
if meta_dict.get("name") == org_meta.name and meta_dict.get("year") == org_meta.year:
logger.info(f'辅助识别结果与原始识别结果一致')
else:
logger.info(f'辅助识别结果与原始识别结果不一致,重新匹配媒体信息 ...')
org_meta.name = meta_dict.get("name")
org_meta.year = meta_dict.get("year")
org_meta.begin_season = meta_dict.get("season")
org_meta.begin_episode = meta_dict.get("episode")
if org_meta.begin_season or org_meta.begin_episode:
org_meta.type = MediaType.TV
# 重新识别
mediainfo = self.recognize_media(meta=org_meta)
return mediainfo
@eventmanager.register(EventType.NameRecognizeResult)
def recognize_result(self, event: Event):
"""
监控识别结果事件,获取辅助识别结果,结果格式:{title, name, year, season, episode}
"""
if not event:
return
event_data = event.event_data or {}
# 加锁
with recognize_lock:
# 不是原标题的结果不要
if event_data.get("title") != self.recognize_title:
return
# 标志收到返回
self.recognize_temp = {}
# 处理数据格式
file_title, file_year, season_number, episode_number = None, None, None, None
if event_data.get("name"):
file_title = str(event_data["name"]).split("/")[0].strip().replace(".", " ")
if event_data.get("year"):
file_year = str(event_data["year"]).split("/")[0].strip()
if event_data.get("season") and str(event_data["season"]).isdigit():
season_number = int(event_data["season"])
if event_data.get("episode") and str(event_data["episode"]).isdigit():
episode_number = int(event_data["episode"])
if not file_title:
return
if file_title == 'Unknown':
return
if not str(file_year).isdigit():
file_year = None
# 结果赋值
self.recognize_temp = {
"name": file_title,
"year": file_year,
"season": season_number,
"episode": episode_number
}
def recognize_by_path(self, path: str) -> Optional[Context]:
"""
根据文件路径识别媒体信息
@@ -43,8 +146,13 @@ class MediaChain(ChainBase):
# 识别媒体信息
mediainfo = self.recognize_media(meta=file_meta)
if not mediainfo:
logger.warn(f'{path} 未识别到媒体信息')
return Context(meta_info=file_meta)
# 偿试使用辅助识别,如果有注册响应事件的话
if eventmanager.check(EventType.NameRecognize):
logger.info(f'请求辅助识别,标题:{file_path.name} ...')
mediainfo = self.recognize_help(title=path, org_meta=file_meta)
if not mediainfo:
logger.warn(f'{path} 未识别到媒体信息')
return Context(meta_info=file_meta)
logger.info(f'{path} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
# 更新媒体图片
self.obtain_images(mediainfo=mediainfo)

View File

@@ -32,7 +32,7 @@ class MessageChain(ChainBase):
self.downloadchain = DownloadChain(self._db)
self.subscribechain = SubscribeChain(self._db)
self.searchchain = SearchChain(self._db)
self.medtachain = MediaChain(self._db)
self.medtachain = MediaChain()
self.torrent = TorrentHelper()
self.eventmanager = EventManager()
self.torrenthelper = TorrentHelper()

View File

@@ -342,6 +342,12 @@ class SearchChain(ChainBase):
include = filter_rule.get("include")
# 排除
exclude = filter_rule.get("exclude")
# 质量
quality = filter_rule.get("quality")
# 分辨率
resolution = filter_rule.get("resolution")
# 特效
effect = filter_rule.get("effect")
def __filter_torrent(t: TorrentInfo) -> bool:
"""
@@ -359,6 +365,24 @@ class SearchChain(ChainBase):
f"{t.title} {t.description}", re.I):
logger.info(f"{t.title} 匹配排除规则 {exclude}")
return False
# 质量
if quality:
if not re.search(r"%s" % quality, t.title, re.I):
logger.info(f"{t.title} 不匹配质量规则 {quality}")
return False
# 分辨率
if resolution:
if not re.search(r"%s" % resolution, t.title, re.I):
logger.info(f"{t.title} 不匹配分辨率规则 {resolution}")
return False
# 特效
if effect:
if not re.search(r"%s" % effect, t.title, re.I):
logger.info(f"{t.title} 不匹配特效规则 {effect}")
return False
return True
# 使用默认过滤规则再次过滤

View File

@@ -6,6 +6,7 @@ from typing import Dict, List, Optional, Union, Tuple
from sqlalchemy.orm import Session
from app.chain import ChainBase
from app.chain.douban import DoubanChain
from app.chain.download import DownloadChain
from app.chain.search import SearchChain
from app.chain.torrents import TorrentsChain
@@ -50,18 +51,28 @@ class SubscribeChain(ChainBase):
识别媒体信息并添加订阅
"""
logger.info(f'开始添加订阅,标题:{title} ...')
# 识别元数据
metainfo = MetaInfo(title)
if year:
metainfo.year = year
if mtype:
metainfo.type = mtype
if season:
metainfo.type = MediaType.TV
metainfo.begin_season = season
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid)
if not mediainfo:
metainfo = None
mediainfo = None
if not tmdbid and doubanid:
# 将豆瓣信息转换为TMDB信息
context = DoubanChain().recognize_by_doubanid(doubanid)
if context:
metainfo = context.meta_info
mediainfo = context.media_info
else:
# 识别元数据
metainfo = MetaInfo(title)
if year:
metainfo.year = year
if mtype:
metainfo.type = mtype
if season:
metainfo.type = MediaType.TV
metainfo.begin_season = season
# 识别媒体信息
mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid)
# 识别失败
if not mediainfo or not metainfo or not mediainfo.tmdb_id:
logger.warn(f'未识别到媒体信息,标题:{title}tmdbid{tmdbid}')
return None, "未识别到媒体信息"
# 更新媒体图片
@@ -74,8 +85,8 @@ class SubscribeChain(ChainBase):
if not kwargs.get('total_episode'):
if not mediainfo.seasons:
# 补充媒体信息
mediainfo: MediaInfo = self.recognize_media(mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id)
mediainfo = self.recognize_media(mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id)
if not mediainfo:
logger.error(f"媒体信息识别失败!")
return None, "媒体信息识别失败"
@@ -85,7 +96,7 @@ class SubscribeChain(ChainBase):
total_episode = len(mediainfo.seasons.get(season) or [])
if not total_episode:
logger.error(f'未获取到总集数,标题:{title}tmdbid{tmdbid}')
return None, "未获取到总集数"
return None, f"未获取到{season} 季的总集数"
kwargs.update({
'total_episode': total_episode
})
@@ -176,66 +187,75 @@ class SubscribeChain(ChainBase):
totals = {
subscribe.season: subscribe.total_episode
}
# 查询缺失的媒体信息
# 查询媒体库缺失的媒体信息
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
meta=meta,
mediainfo=mediainfo,
totals=totals
)
if exist_flag:
logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅')
self.subscribeoper.delete(subscribe.id)
# 发送通知
self.post_message(Notification(mtype=NotificationType.Subscribe,
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
image=mediainfo.get_message_image()))
continue
# 电视剧订阅
if meta.type == MediaType.TV:
# 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits(
no_exists=no_exists,
tmdb_id=mediainfo.tmdb_id,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
)
# 打印缺失集信息
if no_exists and no_exists.get(subscribe.tmdbid):
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
if no_exists_info:
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
else:
# 洗版状态
exist_flag = False
if meta.type == MediaType.TV:
no_exists = {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1)
subscribe.tmdbid: {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1)
}
}
else:
no_exists = {}
# 已存在
if exist_flag:
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
continue
# 电视剧订阅处理缺失集
if meta.type == MediaType.TV:
# 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits(
no_exists=no_exists,
tmdb_id=mediainfo.tmdb_id,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
)
# 打印缺失集信息
if no_exists and no_exists.get(subscribe.tmdbid):
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
if no_exists_info:
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
# 站点范围
if subscribe.sites:
sites = json.loads(subscribe.sites)
else:
sites = None
# 优先级过滤规则
if subscribe.best_version:
priority_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
else:
priority_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
# 默认过滤规则
if subscribe.include or subscribe.exclude:
filter_rule = {
"include": subscribe.include,
"exclude": subscribe.exclude
"exclude": subscribe.exclude,
"quality": subscribe.quality,
"resolution": subscribe.resolution,
"effect": subscribe.effect,
}
else:
filter_rule = self.systemconfig.get(SystemConfigKey.DefaultFilterRules)
# 搜索,同时电视剧会过滤掉不需要的剧集
contexts = self.searchchain.process(mediainfo=mediainfo,
keyword=subscribe.keyword,
@@ -247,8 +267,10 @@ class SubscribeChain(ChainBase):
logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源')
if meta.type == MediaType.TV:
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
meta=meta, mediainfo=mediainfo)
continue
# 过滤
matched_contexts = []
for context in contexts:
@@ -278,8 +300,10 @@ class SubscribeChain(ChainBase):
logger.warn(f'订阅 {subscribe.name} 没有符合过滤条件的资源')
# 非洗版未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
if meta.type == MediaType.TV and not subscribe.best_version:
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
meta=meta, mediainfo=mediainfo)
continue
# 自动下载
downloads, lefts = self.downloadchain.batch_download(contexts=matched_contexts,
no_exists=no_exists)
@@ -299,8 +323,9 @@ class SubscribeChain(ChainBase):
if meta.type == MediaType.TV and not subscribe.best_version:
# 更新订阅剩余集数和时间
update_date = True if downloads else False
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, meta=meta,
mediainfo=mediainfo, update_date=update_date)
# 手动触发时发送系统消息
if manual:
if sid:
@@ -309,19 +334,19 @@ class SubscribeChain(ChainBase):
self.message.put('所有订阅搜索完成!')
def finish_subscribe_or_not(self, subscribe: Subscribe, meta: MetaInfo,
mediainfo: MediaInfo, downloads: List[Context]):
mediainfo: MediaInfo, downloads: List[Context] = None):
"""
判断是否应完成订阅
"""
if not subscribe.best_version:
# 全部下载完成
logger.info(f'{mediainfo.title_year} 下载完成,完成订阅')
logger.info(f'{mediainfo.title_year} 完成订阅')
self.subscribeoper.delete(subscribe.id)
# 发送通知
self.post_message(Notification(mtype=NotificationType.Subscribe,
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
image=mediainfo.get_message_image()))
else:
elif downloads:
# 当前下载资源的优先级
priority = max([item.torrent_info.pri_order for item in downloads])
if priority == 100:
@@ -411,46 +436,50 @@ class SubscribeChain(ChainBase):
mediainfo=mediainfo,
totals=totals
)
if exist_flag:
logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅')
self.subscribeoper.delete(subscribe.id)
# 发送通知
self.post_message(Notification(mtype=NotificationType.Subscribe,
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
image=mediainfo.get_message_image()))
continue
# 电视剧订阅
if meta.type == MediaType.TV:
# 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits(
no_exists=no_exists,
tmdb_id=mediainfo.tmdb_id,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
)
# 打印缺失集信息
if no_exists and no_exists.get(subscribe.tmdbid):
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
if no_exists_info:
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
else:
# 洗版
exist_flag = False
if meta.type == MediaType.TV:
no_exists = {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1)
subscribe.tmdbid: {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1)
}
}
else:
no_exists = {}
# 已存在
if exist_flag:
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
continue
# 电视剧订阅
if meta.type == MediaType.TV:
# 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits(
no_exists=no_exists,
tmdb_id=mediainfo.tmdb_id,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
)
# 打印缺失集信息
if no_exists and no_exists.get(subscribe.tmdbid):
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
if no_exists_info:
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
# 默认过滤规则
default_filter = self.systemconfig.get(SystemConfigKey.DefaultFilterRules) or {}
include = subscribe.include or default_filter.get("include")
exclude = subscribe.exclude or default_filter.get("exclude")
# 遍历缓存种子
_match_context = []
for domain, contexts in torrents.items():
@@ -537,6 +566,7 @@ class SubscribeChain(ChainBase):
# 匹配成功
logger.info(f'{mediainfo.title_year} 匹配成功:{torrent_info.title}')
_match_context.append(context)
# 开始下载
logger.info(f'{mediainfo.title_year} 匹配完成,共匹配到{len(_match_context)}个资源')
if _match_context:
@@ -554,12 +584,13 @@ class SubscribeChain(ChainBase):
if meta.type == MediaType.TV and not subscribe.best_version:
update_date = True if downloads else False
# 未完成下载,计算剩余集数
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, meta=meta,
mediainfo=mediainfo, update_date=update_date)
else:
if meta.type == MediaType.TV:
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
meta=meta, mediainfo=mediainfo)
def check(self):
"""
@@ -651,31 +682,36 @@ class SubscribeChain(ChainBase):
def __update_lack_episodes(self, lefts: Dict[int, Dict[int, NotExistMediaInfo]],
subscribe: Subscribe,
meta: MetaBase,
mediainfo: MediaInfo,
update_date: bool = False):
"""
更新订阅剩余集数
"""
left_seasons = lefts.get(mediainfo.tmdb_id) or {}
for season_info in left_seasons.values():
season = season_info.season
if season == subscribe.season:
left_episodes = season_info.episodes
if not left_episodes:
lack_episode = season_info.total_episode
else:
lack_episode = len(left_episodes)
logger.info(f'{mediainfo.title_year}{season} 更新缺失集数为{lack_episode} ...')
if update_date:
# 同时更新最后时间
self.subscribeoper.update(subscribe.id, {
"lack_episode": lack_episode,
"last_update": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
else:
self.subscribeoper.update(subscribe.id, {
"lack_episode": lack_episode
})
left_seasons = lefts.get(mediainfo.tmdb_id)
if left_seasons:
for season_info in left_seasons.values():
season = season_info.season
if season == subscribe.season:
left_episodes = season_info.episodes
if not left_episodes:
lack_episode = season_info.total_episode
else:
lack_episode = len(left_episodes)
logger.info(f'{mediainfo.title_year}{season} 更新缺失集数为{lack_episode} ...')
if update_date:
# 同时更新最后时间
self.subscribeoper.update(subscribe.id, {
"lack_episode": lack_episode,
"last_update": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
else:
self.subscribeoper.update(subscribe.id, {
"lack_episode": lack_episode
})
else:
# 判断是否应完成订阅
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
def remote_list(self, channel: MessageChannel, userid: Union[str, int] = None):
"""

View File

@@ -12,7 +12,7 @@ from app.utils.singleton import Singleton
class TmdbChain(ChainBase, metaclass=Singleton):
"""
TheMovieDB处理链
TheMovieDB处理链,单例运行
"""
def tmdb_discover(self, mtype: MediaType, sort_by: str, with_genres: str,

View File

@@ -41,8 +41,8 @@ class TransferChain(ChainBase):
self.downloadhis = DownloadHistoryOper(self._db)
self.transferhis = TransferHistoryOper(self._db)
self.progress = ProgressHelper()
self.mediachain = MediaChain(self._db)
self.tmdbchain = TmdbChain(self._db)
self.mediachain = MediaChain()
self.tmdbchain = TmdbChain()
self.systemconfig = SystemConfigOper()
def process(self) -> bool:

View File

@@ -1,3 +1,4 @@
import importlib
import traceback
from threading import Thread, Event
from typing import Any, Union, Dict
@@ -175,10 +176,24 @@ class Command(metaclass=Singleton):
for handler in handlers:
try:
names = handler.__qualname__.split(".")
if names[0] == "Command":
self.command_event(event)
[class_name, method_name] = names
if class_name in self.pluginmanager.get_plugin_ids():
# 插件事件
self.pluginmanager.run_plugin_method(class_name, method_name, event)
else:
self.pluginmanager.run_plugin_method(names[0], names[1], event)
# 检查全局变量中是否存在
if class_name not in globals():
# 导入模块除了插件和Command本身只有chain能响应事件
module = importlib.import_module(
f"app.chain.{class_name[:-5].lower()}"
)
class_obj = getattr(module, class_name)()
else:
# 通过类名创建类实例
class_obj = globals()[class_name]()
# 检查类是否存在并调用方法
if hasattr(class_obj, method_name):
getattr(class_obj, method_name)(event)
except Exception as e:
logger.error(f"事件处理出错:{str(e)} - {traceback.format_exc()}")

View File

@@ -10,16 +10,13 @@ class EventManager(metaclass=Singleton):
事件管理器
"""
# 事件队列
_eventQueue: Queue = None
# 事件响应函数字典
_handlers: dict = {}
def __init__(self):
# 事件队列
self._eventQueue = Queue()
# 事件响应函数字典
self._handlers = {}
# 已禁用的事件响应
self._disabled_handlers = []
def get_event(self):
"""
@@ -27,11 +24,21 @@ class EventManager(metaclass=Singleton):
"""
try:
event = self._eventQueue.get(block=True, timeout=1)
handlerList = self._handlers.get(event.event_type)
return event, handlerList or []
handlerList = self._handlers.get(event.event_type) or []
if handlerList:
# 去除掉被禁用的事件响应
handlerList = [handler for handler in handlerList
if handler.__qualname__.split(".")[0] not in self._disabled_handlers]
return event, handlerList
except Empty:
return None, []
def check(self, etype: EventType):
"""
检查事件是否存在响应
"""
return etype.value in self._handlers
def add_event_listener(self, etype: EventType, handler: type):
"""
注册事件处理
@@ -45,18 +52,21 @@ class EventManager(metaclass=Singleton):
handlerList.append(handler)
logger.debug(f"Event Registed{etype.value} - {handler}")
def remove_event_listener(self, etype: EventType, handler: type):
def disable_events_hander(self, class_name: str):
"""
移除监听器的处理函数
标记对应类事件处理为不可用
"""
try:
handlerList = self._handlers[etype.value]
if handler in handlerList[:]:
handlerList.remove(handler)
if not handlerList:
del self._handlers[etype.value]
except KeyError:
pass
if class_name not in self._disabled_handlers:
self._disabled_handlers.append(class_name)
logger.debug(f"Event Disabled{class_name}")
def enable_events_hander(self, class_name: str):
"""
标记对应类事件处理为可用
"""
if class_name in self._disabled_handlers:
self._disabled_handlers.remove(class_name)
logger.debug(f"Event Enabled{class_name}")
def send_event(self, etype: EventType, data: dict = None):
"""

View File

@@ -87,6 +87,17 @@ class MetaBase(object):
return self.cn_name
return ""
@name.setter
def name(self, name: str):
"""
设置名称
"""
if StringUtils.is_all_chinese(name):
self.cn_name = name
else:
self.en_name = name
self.cn_name = None
def init_subtitle(self, title_text: str):
"""
副标题识别

View File

@@ -61,8 +61,7 @@ class WordsMatcher(metaclass=Singleton):
if state:
appley_words.append(word)
else:
logger.debug(f"自定义识别词替换失败:{message}")
except Exception as err:
print(str(err))

View File

@@ -1,6 +1,7 @@
import traceback
from typing import List, Any, Dict, Tuple
from app.core.event import eventmanager
from app.db.systemconfig_oper import SystemConfigOper
from app.helper.module import ModuleHelper
from app.helper.sites import SitesHelper
@@ -58,6 +59,8 @@ class PluginManager(metaclass=Singleton):
self._plugins[plugin_id] = plugin
# 未安装的不加载
if plugin_id not in installed_plugins:
# 设置事件状态为不可用
eventmanager.disable_events_hander(plugin_id)
continue
# 生成实例
plugin_obj = plugin()
@@ -66,6 +69,8 @@ class PluginManager(metaclass=Singleton):
# 存储运行实例
self._running_plugins[plugin_id] = plugin_obj
logger.info(f"Plugin Loaded{plugin_id}")
# 设置事件注册状态可用
eventmanager.enable_events_hander(plugin_id)
except Exception as err:
logger.error(f"加载插件 {plugin_id} 出错:{err} - {traceback.format_exc()}")
@@ -177,6 +182,12 @@ class PluginManager(metaclass=Singleton):
return None
return getattr(self._running_plugins[pid], method)(*args, **kwargs)
def get_plugin_ids(self) -> List[str]:
"""
获取所有插件ID
"""
return list(self._plugins.keys())
def get_plugin_apps(self) -> List[dict]:
"""
获取所有插件信息

View File

@@ -37,6 +37,12 @@ class Subscribe(Base):
include = Column(String)
# 排除
exclude = Column(String)
# 质量
quality = Column(String)
# 分辨率
resolution = Column(String)
# 特效
effect = Column(String)
# 总集数
total_episode = Column(Integer)
# 开始集数

View File

@@ -1,4 +1,3 @@
from datetime import datetime
from pathlib import Path
from typing import List, Optional, Tuple, Union
@@ -406,17 +405,25 @@ class DoubanModule(_ModuleBase):
return ret_medias
@retry(Exception, 5, 3, 3, logger=logger)
def match_doubaninfo(self, name: str, mtype: str = None,
year: str = None, season: int = None) -> dict:
def match_doubaninfo(self, name: str, imdbid: str = None,
mtype: str = None, year: str = None, season: int = None) -> dict:
"""
搜索和匹配豆瓣信息
:param name: 名称
:param imdbid: IMDB ID
:param mtype: 类型 电影/电视剧
:param year: 年份
:param season: 季号
"""
result = self.doubanapi.search(f"{name} {year or ''}".strip(),
ts=datetime.strftime(datetime.now(), '%Y%m%d%H%M%S'))
if imdbid:
# 优先使用IMDBID查询
logger.info(f"开始使用IMDBID {imdbid} 查询豆瓣信息 ...")
result = self.doubanapi.imdbid(imdbid)
if result:
return result
# 搜索
logger.info(f"开始使用名称 {name} 查询豆瓣信息 ...")
result = self.doubanapi.search(f"{name} {year or ''}".strip())
if not result:
logger.warn(f"未找到 {name} 的豆瓣信息")
return {}
@@ -473,6 +480,7 @@ class DoubanModule(_ModuleBase):
return
# 根据名称查询豆瓣数据
doubaninfo = self.match_doubaninfo(name=mediainfo.title,
imdbid=mediainfo.imdb_id,
mtype=mediainfo.type.value,
year=mediainfo.year,
season=meta.begin_season)
@@ -495,6 +503,7 @@ class DoubanModule(_ModuleBase):
continue
# 根据名称查询豆瓣数据
doubaninfo = self.match_doubaninfo(name=mediainfo.title,
imdbid=mediainfo.imdb_id,
mtype=mediainfo.type.value,
year=mediainfo.year,
season=meta.begin_season)

View File

@@ -18,28 +18,29 @@ class DoubanApi(metaclass=Singleton):
_urls = {
# 搜索类
# sort=U:近期热门 T:标记最多 S:评分最高 R:最新上映
# q=search_word&start=0&count=20&sort=U
# q=search_word&start: int = 0&count: int = 20&sort=U
# 聚合搜索
"search": "/search/weixin",
"search_agg": "/search",
"imdbid": "/movie/imdb/%s",
# 电影探索
# sort=U:综合排序 T:近期热度 S:高分优先 R:首播时间
# tags='日本,动画,2022'&start=0&count=20&sort=U
# tags='日本,动画,2022'&start: int = 0&count: int = 20&sort=U
"movie_recommend": "/movie/recommend",
# 电视剧探索
"tv_recommend": "/tv/recommend",
# 搜索
"movie_tag": "/movie/tag",
"tv_tag": "/tv/tag",
# q=search_word&start=0&count=20
# q=search_word&start: int = 0&count: int = 20
"movie_search": "/search/movie",
"tv_search": "/search/movie",
"book_search": "/search/book",
"group_search": "/search/group",
# 各类主题合集
# start=0&count=20
# start: int = 0&count: int = 20
# 正在上映
"movie_showing": "/subject_collection/movie_showing/items",
# 热门电影
@@ -145,7 +146,9 @@ class DoubanApi(metaclass=Singleton):
"api-client/1 com.douban.frodo/7.3.0(207) Android/22 product/MI 9 vendor/Xiaomi model/MI 9 brand/Android rom/miui6 network/wifi platform/mobile nd/1"]
_api_secret_key = "bf7dddc7c9cfe6f7"
_api_key = "0dad551ec0f84ed02907ff5c42e8ec70"
_api_key2 = "0ab215a8b1977939201640fa14c66bab"
_base_url = "https://frodo.douban.com/api/v2"
_api_url = "https://api.douban.com/v2"
_session = None
def __init__(self):
@@ -153,6 +156,9 @@ class DoubanApi(metaclass=Singleton):
@classmethod
def __sign(cls, url: str, ts: int, method='GET') -> str:
"""
签名
"""
url_path = parse.urlparse(url).path
raw_sign = '&'.join([method.upper(), parse.quote(url_path, safe=''), str(ts)])
return base64.b64encode(
@@ -164,7 +170,10 @@ class DoubanApi(metaclass=Singleton):
).decode()
@lru_cache(maxsize=settings.CACHE_CONF.get('douban'))
def __invoke(self, url, **kwargs):
def __invoke(self, url: str, **kwargs) -> dict:
"""
GET请求
"""
req_url = self._base_url + url
params = {'apiKey': self._api_key}
@@ -189,119 +198,224 @@ class DoubanApi(metaclass=Singleton):
return resp.json()
return resp.json() if resp else {}
def search(self, keyword, start=0, count=20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
@lru_cache(maxsize=settings.CACHE_CONF.get('douban'))
def __post(self, url: str, **kwargs) -> dict:
"""
POST请求
esponse = requests.post(
url="https://api.douban.com/v2/movie/imdb/tt29139455",
headers={
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"Cookie": "bid=J9zb1zA5sJc",
},
data={
"apikey": "0ab215a8b1977939201640fa14c66bab",
},
)
"""
req_url = self._api_url + url
params = {'apikey': self._api_key2}
if kwargs:
params.update(kwargs)
if '_ts' in params:
params.pop('_ts')
resp = RequestUtils(
ua=settings.USER_AGENT,
session=self._session,
).post_res(url=req_url, data=params)
if resp.status_code == 400 and "rate_limit" in resp.text:
return resp.json()
return resp.json() if resp else {}
def search(self, keyword: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')) -> dict:
"""
关键字搜索
"""
return self.__invoke(self._urls["search"], q=keyword,
start=start, count=count, _ts=ts)
def movie_search(self, keyword, start=0, count=20,
def imdbid(self, imdbid: str,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
IMDBID搜索
"""
return self.__post(self._urls["imdbid"] % imdbid, _ts=ts)
def movie_search(self, keyword: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影搜索
"""
return self.__invoke(self._urls["movie_search"], q=keyword,
start=start, count=count, _ts=ts)
def tv_search(self, keyword, start=0, count=20,
def tv_search(self, keyword: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视搜索
"""
return self.__invoke(self._urls["tv_search"], q=keyword,
start=start, count=count, _ts=ts)
def book_search(self, keyword, start=0, count=20,
def book_search(self, keyword: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
书籍搜索
"""
return self.__invoke(self._urls["book_search"], q=keyword,
start=start, count=count, _ts=ts)
def group_search(self, keyword, start=0, count=20,
def group_search(self, keyword: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
小组搜索
"""
return self.__invoke(self._urls["group_search"], q=keyword,
start=start, count=count, _ts=ts)
def movie_showing(self, start=0, count=20,
def movie_showing(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
正在热映
"""
return self.__invoke(self._urls["movie_showing"],
start=start, count=count, _ts=ts)
def movie_soon(self, start=0, count=20,
def movie_soon(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
即将上映
"""
return self.__invoke(self._urls["movie_soon"],
start=start, count=count, _ts=ts)
def movie_hot_gaia(self, start=0, count=20,
def movie_hot_gaia(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
热门电影
"""
return self.__invoke(self._urls["movie_hot_gaia"],
start=start, count=count, _ts=ts)
def tv_hot(self, start=0, count=20,
def tv_hot(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
热门剧集
"""
return self.__invoke(self._urls["tv_hot"],
start=start, count=count, _ts=ts)
def tv_animation(self, start=0, count=20,
def tv_animation(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
动画
"""
return self.__invoke(self._urls["tv_animation"],
start=start, count=count, _ts=ts)
def tv_variety_show(self, start=0, count=20,
def tv_variety_show(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
综艺
"""
return self.__invoke(self._urls["tv_variety_show"],
start=start, count=count, _ts=ts)
def tv_rank_list(self, start=0, count=20,
def tv_rank_list(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视剧排行榜
"""
return self.__invoke(self._urls["tv_rank_list"],
start=start, count=count, _ts=ts)
def show_hot(self, start=0, count=20,
def show_hot(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
综艺热门
"""
return self.__invoke(self._urls["show_hot"],
start=start, count=count, _ts=ts)
def movie_detail(self, subject_id):
def movie_detail(self, subject_id: str):
"""
电影详情
"""
return self.__invoke(self._urls["movie_detail"] + subject_id)
def movie_celebrities(self, subject_id):
def movie_celebrities(self, subject_id: str):
"""
电影演职员
"""
return self.__invoke(self._urls["movie_celebrities"] % subject_id)
def tv_detail(self, subject_id):
def tv_detail(self, subject_id: str):
"""
电视剧详情
"""
return self.__invoke(self._urls["tv_detail"] + subject_id)
def tv_celebrities(self, subject_id):
def tv_celebrities(self, subject_id: str):
"""
电视剧演职员
"""
return self.__invoke(self._urls["tv_celebrities"] % subject_id)
def book_detail(self, subject_id):
def book_detail(self, subject_id: str):
"""
书籍详情
"""
return self.__invoke(self._urls["book_detail"] + subject_id)
def movie_top250(self, start=0, count=20,
def movie_top250(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影TOP250
"""
return self.__invoke(self._urls["movie_top250"],
start=start, count=count, _ts=ts)
def movie_recommend(self, tags='', sort='R', start=0, count=20,
def movie_recommend(self, tags='', sort='R', start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影探索
"""
return self.__invoke(self._urls["movie_recommend"], tags=tags, sort=sort,
start=start, count=count, _ts=ts)
def tv_recommend(self, tags='', sort='R', start=0, count=20,
def tv_recommend(self, tags='', sort='R', start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视剧探索
"""
return self.__invoke(self._urls["tv_recommend"], tags=tags, sort=sort,
start=start, count=count, _ts=ts)
def tv_chinese_best_weekly(self, start=0, count=20,
def tv_chinese_best_weekly(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
华语口碑周榜
"""
return self.__invoke(self._urls["tv_chinese_best_weekly"],
start=start, count=count, _ts=ts)
def tv_global_best_weekly(self, start=0, count=20,
def tv_global_best_weekly(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
全球口碑周榜
"""
return self.__invoke(self._urls["tv_global_best_weekly"],
start=start, count=count, _ts=ts)
def doulist_detail(self, subject_id):
def doulist_detail(self, subject_id: str):
"""
豆列详情
:param subject_id: 豆列id
"""
return self.__invoke(self._urls["doulist"] + subject_id)
def doulist_items(self, subject_id, start=0, count=20,
def doulist_items(self, subject_id: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
豆列列表

View File

@@ -382,13 +382,14 @@ class FileTransferModule(_ModuleBase):
path=in_path,
message=f"{in_path} 路径不存在")
if not target_dir.exists():
return TransferInfo(success=False,
path=in_path,
message=f"{target_dir} 目标路径不存在")
# 媒体库目的目录
target_dir = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target_dir)
if transfer_type not in ['rclone_copy', 'rclone_move']:
# 检查目标路径
if not target_dir.exists():
return TransferInfo(success=False,
path=in_path,
message=f"{target_dir} 目标路径不存在")
# 媒体库目的目录
target_dir = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target_dir)
# 重命名格式
rename_format = settings.TV_RENAME_FORMAT \

View File

@@ -70,11 +70,16 @@ class FilterModule(_ModuleBase):
"include": [r'[Hx].?264|AVC'],
"exclude": []
},
# 杜比
# 杜比视界
"DOLBY": {
"include": [r"Dolby[\s.]+Vision|DOVI|[\s.]+DV[\s.]+|杜比视界"],
"exclude": []
},
# 杜比全景声
"ATMOS": {
"include": [r"Dolby[\s.+]+Atmos|Atmos|杜比全景[声聲]"],
"exclude": []
},
# HDR
"HDR": {
"include": [r"[\s.]+HDR[\s.]+|HDR10|HDR10\+"],
@@ -98,7 +103,12 @@ class FilterModule(_ModuleBase):
"CNVOI": {
"include": [r'[国國][语語]配音|[国國]配|[国國][语語]'],
"exclude": []
}
},
# 60FPS
"60FPS": {
"include": [r'60fps'],
"exclude": []
},
}
def init_module(self) -> None:

View File

@@ -283,6 +283,8 @@ class TheMovieDbModule(_ModuleBase):
:param mediainfo: 识别的媒体信息
:return: 更新后的媒体信息
"""
if not mediainfo.tmdb_id:
return mediainfo
if mediainfo.logo_path \
and mediainfo.poster_path \
and mediainfo.backdrop_path:

View File

@@ -172,6 +172,9 @@ class TMDb(object):
else:
req = self.request(method, url, data, json)
if req is None:
raise TMDbException("Failed to establish a new connection: no response from the server.")
headers = req.headers
if "X-RateLimit-Remaining" in headers:

View File

@@ -1,7 +1,8 @@
from typing import Any, List, Dict, Tuple
from app.core.config import settings
from app.core.event import eventmanager
from app.core.event import eventmanager, Event
from app.log import logger
from app.plugins import _PluginBase
from app.plugins.chatgpt.openai import OpenAi
from app.schemas.types import EventType
@@ -33,6 +34,7 @@ class ChatGPT(_PluginBase):
openai = None
_enabled = False
_proxy = False
_recognize = False
_openai_url = None
_openai_key = None
@@ -40,6 +42,7 @@ class ChatGPT(_PluginBase):
if config:
self._enabled = config.get("enabled")
self._proxy = config.get("proxy")
self._recognize = config.get("recognize")
self._openai_url = config.get("openai_url")
self._openai_key = config.get("openai_key")
self.openai = OpenAi(api_key=self._openai_key, api_url=self._openai_url,
@@ -70,7 +73,7 @@ class ChatGPT(_PluginBase):
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
'md': 4
},
'content': [
{
@@ -86,7 +89,7 @@ class ChatGPT(_PluginBase):
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
'md': 4
},
'content': [
{
@@ -97,6 +100,22 @@ class ChatGPT(_PluginBase):
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'recognize',
'label': '辅助识别',
}
}
]
}
]
},
@@ -143,6 +162,7 @@ class ChatGPT(_PluginBase):
], {
"enabled": False,
"proxy": False,
"recognize": False,
"openai_url": "https://api.openai.com",
"openai_key": ""
}
@@ -151,10 +171,12 @@ class ChatGPT(_PluginBase):
pass
@eventmanager.register(EventType.UserMessage)
def talk(self, event):
def talk(self, event: Event):
"""
监听用户消息获取ChatGPT回复
"""
if not self._enabled:
return
if not self.openai:
return
text = event.event_data.get("text")
@@ -166,6 +188,42 @@ class ChatGPT(_PluginBase):
if response:
self.post_message(channel=channel, title=response, userid=userid)
@eventmanager.register(EventType.NameRecognize)
def recognize(self, event: Event):
"""
监听识别事件使用ChatGPT辅助识别名称
"""
if not event.event_data:
return
title = event.event_data.get("title")
if not title:
return
# 收到事件后需要立码返回,避免主程序等待
if not self._enabled \
or not self.openai \
or not self._recognize:
eventmanager.send_event(
EventType.NameRecognizeResult,
{
'title': title
}
)
return
# 调用ChatGPT
response = self.openai.get_media_name(filename=title)
logger.info(f"ChatGPT辅助识别结果{response}")
if response:
eventmanager.send_event(
EventType.NameRecognizeResult,
{
'title': title,
'name': response.get("title"),
'year': response.get("year"),
'season': response.get("season"),
'episode': response.get("episode")
}
)
def stop_service(self):
"""
退出插件

View File

@@ -251,6 +251,6 @@ class ChineseSubFinder(_PluginBase):
else:
logger.info("ChineseSubFinder任务添加成功%s" % job_id)
else:
logger.error("%s 目录缺失nfo元数据" % file_path)
logger.warn(f"ChineseSubFinder调用出错{res.status_code} - {res.reason}")
except Exception as e:
logger.error("连接ChineseSubFinder出错" + str(e))

View File

@@ -97,7 +97,7 @@ class DirMonitor(_PluginBase):
self.transferhis = TransferHistoryOper(self.db)
self.downloadhis = DownloadHistoryOper(self.db)
self.transferchian = TransferChain(self.db)
self.tmdbchain = TmdbChain(self.db)
self.tmdbchain = TmdbChain()
# 清空配置
self._dirconf = {}
self._transferconf = {}

View File

@@ -457,8 +457,8 @@ class DoubanSync(_PluginBase):
try:
dtype = result.get("title", "")[:2]
title = result.get("title", "")[2:]
if dtype not in ["想看"]:
logger.info(f'标题:{title},非想看数据,跳过')
if dtype not in ["想看", "在看"]:
logger.info(f'标题:{title},非想看/在看数据,跳过')
continue
if not result.get("link"):
logger.warn(f'标题:{title},未获取到链接,跳过')

View File

@@ -156,7 +156,7 @@ class DownloadingMsg(_PluginBase):
channel_value = downloadhis.channel
else:
try:
context = MediaChain(self.db).recognize_by_title(title=torrent.title)
context = MediaChain().recognize_by_title(title=torrent.title)
if not context or not context.media_info:
continue
media_info = context.media_info

View File

@@ -67,7 +67,7 @@ class PersonMeta(_PluginBase):
_remove_nozh = False
def init_plugin(self, config: dict = None):
self.tmdbchain = TmdbChain(self.db)
self.tmdbchain = TmdbChain()
self.mschain = MediaServerChain(self.db)
if config:
self._enabled = config.get("enabled")
@@ -587,6 +587,7 @@ class PersonMeta(_PluginBase):
time.sleep(sleep_time)
# 匹配豆瓣信息
doubaninfo = self.chain.match_doubaninfo(name=mediainfo.title,
imdbid=mediainfo.imdb_id,
mtype=mediainfo.type.value,
year=mediainfo.year,
season=season)

View File

@@ -31,6 +31,12 @@ class Subscribe(BaseModel):
include: Optional[str] = None
# 排除
exclude: Optional[str] = None
# 质量
quality: Optional[str] = None
# 分辨率
resolution: Optional[str] = None
# 特效
effect: Optional[str] = None
# 总集数
total_episode: Optional[int] = 0
# 开始集数

View File

@@ -40,6 +40,10 @@ class EventType(Enum):
UserMessage = "user.message"
# 通知消息
NoticeMessage = "notice.message"
# 名称识别请求
NameRecognize = "name.recognize"
# 名称识别结果
NameRecognizeResult = "name.recognize.result"
# 系统配置Key字典

View File

@@ -8,7 +8,6 @@ Create Date: 2023-09-28 13:37:16.479360
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a521fbc28b18'
down_revision = 'b2f011d3a8b7'
@@ -26,5 +25,6 @@ def upgrade() -> None:
pass
# ### end Alembic commands ###
def downgrade() -> None:
pass
pass

View File

@@ -0,0 +1,32 @@
"""1.0.10
Revision ID: d633ca6cd572
Revises: a521fbc28b18
Create Date: 2023-10-12 08:54:49.728638
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd633ca6cd572'
down_revision = 'a521fbc28b18'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
try:
with op.batch_alter_table("subscribe") as batch_op:
batch_op.add_column(sa.Column('quality', sa.String, nullable=True))
batch_op.add_column(sa.Column('resolution', sa.String, nullable=True))
batch_op.add_column(sa.Column('effect', sa.String, nullable=True))
except Exception as e:
pass
# ### end Alembic commands ###
def downgrade() -> None:
pass

View File

@@ -1 +1 @@
APP_VERSION = 'v1.3.1'
APP_VERSION = 'v1.3.2'