add Bangumi

This commit is contained in:
jxxghp 2024-03-18 19:02:34 +08:00
parent f7c1d28c0f
commit b6486035c4
24 changed files with 611 additions and 36 deletions

View File

@ -1,7 +1,8 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.api.endpoints import login, user, site, message, webhook, subscribe, \ from app.api.endpoints import login, user, site, message, webhook, subscribe, \
media, douban, search, plugin, tmdb, history, system, download, dashboard, filebrowser, transfer, mediaserver media, douban, search, plugin, tmdb, history, system, download, dashboard, \
filebrowser, transfer, mediaserver, bangumi
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(login.router, prefix="/login", tags=["login"]) api_router.include_router(login.router, prefix="/login", tags=["login"])
@ -22,3 +23,5 @@ api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboar
api_router.include_router(filebrowser.router, prefix="/filebrowser", tags=["filebrowser"]) api_router.include_router(filebrowser.router, prefix="/filebrowser", tags=["filebrowser"])
api_router.include_router(transfer.router, prefix="/transfer", tags=["transfer"]) api_router.include_router(transfer.router, prefix="/transfer", tags=["transfer"])
api_router.include_router(mediaserver.router, prefix="/mediaserver", tags=["mediaserver"]) api_router.include_router(mediaserver.router, prefix="/mediaserver", tags=["mediaserver"])
api_router.include_router(bangumi.router, prefix="/bangumi", tags=["bangumi"])

View File

@ -0,0 +1,64 @@
from typing import List, Any
from fastapi import APIRouter, Depends
from app import schemas
from app.chain.bangumi import BangumiChain
from app.core.context import MediaInfo
from app.core.security import verify_token
router = APIRouter()
@router.get("/calendar", summary="Bangumi每日放送", response_model=List[schemas.MediaInfo])
def calendar(page: int = 1,
count: int = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览Bangumi每日放送
"""
infos = BangumiChain().calendar(page=page, count=count)
if not infos:
return []
medias = [MediaInfo(bangumi_info=info) for info in infos]
return [media.to_dict() for media in medias]
@router.get("/credits/{bangumiid}", summary="查询Bangumi演职员表", response_model=List[schemas.BangumiPerson])
def bangumi_credits(bangumiid: int,
page: int = 1,
count: int = 20,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询Bangumi演职员表
"""
persons = BangumiChain().bangumi_credits(bangumiid, page=page, count=count)
if not persons:
return []
return [schemas.BangumiPerson(**person) for person in persons]
@router.get("/recommend/{bangumiid}", summary="查询Bangumi推荐", response_model=List[schemas.MediaInfo])
def bangumi_recommend(bangumiid: int,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询Bangumi推荐
"""
infos = BangumiChain().bangumi_recommend(bangumiid)
if not infos:
return []
medias = [MediaInfo(bangumi_info=info) for info in infos]
return [media.to_dict() for media in medias]
@router.get("/{bangumiid}", summary="查询Bangumi详情", response_model=schemas.MediaInfo)
def bangumi_info(bangumiid: int,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询Bangumi详情
"""
info = BangumiChain().bangumi_info(bangumiid)
if info:
return MediaInfo(bangumi_info=info).to_dict()
else:
return schemas.MediaInfo()

View File

@ -106,14 +106,17 @@ def media_info(mediaid: str, type_name: str,
根据媒体ID查询themoviedb或豆瓣媒体信息type_name: 电影/电视剧 根据媒体ID查询themoviedb或豆瓣媒体信息type_name: 电影/电视剧
""" """
mtype = MediaType(type_name) mtype = MediaType(type_name)
tmdbid, doubanid = None, None tmdbid, doubanid, bangumiid = None, None, None
if mediaid.startswith("tmdb:"): if mediaid.startswith("tmdb:"):
tmdbid = int(mediaid[5:]) tmdbid = int(mediaid[5:])
elif mediaid.startswith("douban:"): elif mediaid.startswith("douban:"):
doubanid = mediaid[7:] doubanid = mediaid[7:]
if not tmdbid and not doubanid: elif mediaid.startswith("bangumi:"):
bangumiid = int(mediaid[8:])
if not tmdbid and not doubanid and not bangumiid:
return schemas.MediaInfo() return schemas.MediaInfo()
mediainfo = MediaChain().recognize_media(tmdbid=tmdbid, doubanid=doubanid, mtype=mtype) # 识别
mediainfo = MediaChain().recognize_media(tmdbid=tmdbid, doubanid=doubanid, bangumiid=bangumiid, mtype=mtype)
if mediainfo: if mediainfo:
MediaChain().obtain_images(mediainfo) MediaChain().obtain_images(mediainfo)
return mediainfo.to_dict() return mediainfo.to_dict()

View File

@ -52,6 +52,20 @@ def search_by_id(mediaid: str,
mtype=mtype, area=area) mtype=mtype, area=area)
else: else:
torrents = SearchChain().search_by_id(doubanid=doubanid, mtype=mtype, area=area) torrents = SearchChain().search_by_id(doubanid=doubanid, mtype=mtype, area=area)
elif mediaid.startswith("bangumi:"):
bangumiid = int(mediaid.replace("bangumi:", ""))
if settings.RECOGNIZE_SOURCE == "themoviedb":
# 通过BangumiID识别TMDBID
tmdbinfo = MediaChain().get_tmdbinfo_by_bangumiid(bangumiid=bangumiid)
if tmdbinfo:
torrents = SearchChain().search_by_id(tmdbid=tmdbinfo.get("id"),
mtype=mtype, area=area)
else:
# 通过BangumiID识别豆瓣ID
doubaninfo = MediaChain().get_doubaninfo_by_bangumiid(bangumiid=bangumiid)
if doubaninfo:
torrents = SearchChain().search_by_id(doubanid=doubaninfo.get("id"),
mtype=mtype, area=area)
else: else:
return [] return []
return [torrent.to_dict() for torrent in torrents] return [torrent.to_dict() for torrent in torrents]

View File

@ -65,7 +65,7 @@ def create_subscribe(
else: else:
mtype = None mtype = None
# 豆瓣标理 # 豆瓣标理
if subscribe_in.doubanid: if subscribe_in.doubanid or subscribe_in.bangumiid:
meta = MetaInfo(subscribe_in.name) meta = MetaInfo(subscribe_in.name)
subscribe_in.name = meta.name subscribe_in.name = meta.name
subscribe_in.season = meta.begin_season subscribe_in.season = meta.begin_season
@ -80,6 +80,7 @@ def create_subscribe(
tmdbid=subscribe_in.tmdbid, tmdbid=subscribe_in.tmdbid,
season=subscribe_in.season, season=subscribe_in.season,
doubanid=subscribe_in.doubanid, doubanid=subscribe_in.doubanid,
bangumiid=subscribe_in.bangumiid,
username=current_user.name, username=current_user.name,
best_version=subscribe_in.best_version, best_version=subscribe_in.best_version,
save_path=subscribe_in.save_path, save_path=subscribe_in.save_path,
@ -131,9 +132,10 @@ def subscribe_mediaid(
db: Session = Depends(get_db), db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any: _: schemas.TokenPayload = Depends(verify_token)) -> Any:
""" """
根据TMDBID或豆瓣ID查询订阅 tmdb:/douban: 根据 TMDBID/豆瓣ID/BangumiId 查询订阅 tmdb:/douban:
""" """
result = None result = None
title_check = False
if mediaid.startswith("tmdb:"): if mediaid.startswith("tmdb:"):
tmdbid = mediaid[5:] tmdbid = mediaid[5:]
if not tmdbid or not str(tmdbid).isdigit(): if not tmdbid or not str(tmdbid).isdigit():
@ -144,14 +146,21 @@ def subscribe_mediaid(
if not doubanid: if not doubanid:
return Subscribe() return Subscribe()
result = Subscribe.get_by_doubanid(db, doubanid) result = Subscribe.get_by_doubanid(db, doubanid)
# 豆瓣已订阅如果 id 搜索无结果使用标题搜索
# 会造成同名结果也会被返回
if not result and title: if not result and title:
meta = MetaInfo(title) title_check = True
if season: elif mediaid.startswith("bangumi:"):
meta.begin_season = season bangumiid = mediaid[8:]
result = Subscribe.get_by_title(db, title=meta.name, season=meta.begin_season) if not bangumiid or not str(bangumiid).isdigit():
return Subscribe()
result = Subscribe.get_by_bangumiid(db, int(bangumiid))
if not result and title:
title_check = True
# 使用名称检查订阅
if title_check and title:
meta = MetaInfo(title)
if season:
meta.begin_season = season
result = Subscribe.get_by_title(db, title=meta.name, season=meta.begin_season)
if result and result.sites: if result and result.sites:
result.sites = json.loads(result.sites) result.sites = json.loads(result.sites)

View File

@ -119,6 +119,7 @@ class ChainBase(metaclass=ABCMeta):
mtype: MediaType = None, mtype: MediaType = None,
tmdbid: int = None, tmdbid: int = None,
doubanid: str = None, doubanid: str = None,
bangumiid: int = None,
cache: bool = True) -> Optional[MediaInfo]: cache: bool = True) -> Optional[MediaInfo]:
""" """
识别媒体信息 识别媒体信息
@ -126,6 +127,7 @@ class ChainBase(metaclass=ABCMeta):
:param mtype: 识别的媒体类型与tmdbid配套 :param mtype: 识别的媒体类型与tmdbid配套
:param tmdbid: tmdbid :param tmdbid: tmdbid
:param doubanid: 豆瓣ID :param doubanid: 豆瓣ID
:param bangumiid: BangumiID
:param cache: 是否使用缓存 :param cache: 是否使用缓存
:return: 识别的媒体信息包括剧集信息 :return: 识别的媒体信息包括剧集信息
""" """
@ -136,11 +138,12 @@ class ChainBase(metaclass=ABCMeta):
tmdbid = meta.tmdbid tmdbid = meta.tmdbid
if not doubanid and hasattr(meta, "doubanid"): if not doubanid and hasattr(meta, "doubanid"):
doubanid = meta.doubanid doubanid = meta.doubanid
# 有tmdbid时不使用doubanid # 有tmdbid时不使用其它ID
if tmdbid: if tmdbid:
doubanid = None doubanid = None
bangumiid = None
return self.run_module("recognize_media", meta=meta, mtype=mtype, return self.run_module("recognize_media", meta=meta, mtype=mtype,
tmdbid=tmdbid, doubanid=doubanid, cache=cache) tmdbid=tmdbid, doubanid=doubanid, bangumiid=bangumiid, cache=cache)
def match_doubaninfo(self, name: str, imdbid: str = None, def match_doubaninfo(self, name: str, imdbid: str = None,
mtype: MediaType = None, year: str = None, season: int = None) -> Optional[dict]: mtype: MediaType = None, year: str = None, season: int = None) -> Optional[dict]:
@ -217,6 +220,14 @@ class ChainBase(metaclass=ABCMeta):
""" """
return self.run_module("tmdb_info", tmdbid=tmdbid, mtype=mtype) return self.run_module("tmdb_info", tmdbid=tmdbid, mtype=mtype)
def bangumi_info(self, bangumiid: int) -> Optional[dict]:
"""
获取Bangumi信息
:param bangumiid: int
:return: Bangumi信息
"""
return self.run_module("bangumi_info", bangumiid=bangumiid)
def message_parser(self, body: Any, form: Any, def message_parser(self, body: Any, form: Any,
args: Any) -> Optional[CommingMessage]: args: Any) -> Optional[CommingMessage]:
""" """

42
app/chain/bangumi.py Normal file
View File

@ -0,0 +1,42 @@
from typing import Optional, List
from app.chain import ChainBase
from app.utils.singleton import Singleton
class BangumiChain(ChainBase, metaclass=Singleton):
"""
Bangumi处理链单例运行
"""
def calendar(self, page: int = 1, count: int = 30) -> Optional[List[dict]]:
"""
获取Bangumi每日放送
:param page: 页码
:param count: 每页数量
"""
return self.run_module("bangumi_calendar", page=page, count=count)
def bangumi_info(self, bangumiid: int) -> Optional[dict]:
"""
获取Bangumi信息
:param bangumiid: BangumiID
:return: Bangumi信息
"""
return self.run_module("bangumi_info", bangumiid=bangumiid)
def bangumi_credits(self, bangumiid: int, page: int = 1, count: int = 20) -> List[dict]:
"""
根据BangumiID查询电影演职员表
:param bangumiid: BangumiID
:param page: 页码
:param count: 数量
"""
return self.run_module("bangumi_credits", bangumiid=bangumiid, page=page, count=count)
def bangumi_recommend(self, bangumiid: int) -> List[dict]:
"""
根据BangumiID查询推荐电影
:param bangumiid: BangumiID
"""
return self.run_module("bangumi_recommend", bangumiid=bangumiid)

View File

@ -229,6 +229,28 @@ class MediaChain(ChainBase, metaclass=Singleton):
) )
return tmdbinfo return tmdbinfo
def get_tmdbinfo_by_bangumiid(self, bangumiid: int) -> Optional[dict]:
"""
根据BangumiID获取TMDB信息
"""
bangumiinfo = self.bangumi_info(bangumiid=bangumiid)
if bangumiinfo:
# 名称
name = bangumiinfo.get("name") or bangumiinfo.get("name_cn")
# 年份
release_date = bangumiinfo.get("date") or bangumiinfo.get("air_date")
if release_date:
year = release_date[:4]
else:
year = None
# 使用名称识别TMDB媒体信息
return self.match_tmdbinfo(
name=name,
year=year,
mtype=MediaType.TV
)
return None
def get_doubaninfo_by_tmdbid(self, tmdbid: int, def get_doubaninfo_by_tmdbid(self, tmdbid: int,
mtype: MediaType = None, season: int = None) -> Optional[dict]: mtype: MediaType = None, season: int = None) -> Optional[dict]:
""" """
@ -261,3 +283,25 @@ class MediaChain(ChainBase, metaclass=Singleton):
imdbid=imdbid imdbid=imdbid
) )
return None return None
def get_doubaninfo_by_bangumiid(self, bangumiid: int) -> Optional[dict]:
"""
根据BangumiID获取豆瓣信息
"""
bangumiinfo = self.bangumi_info(bangumiid=bangumiid)
if bangumiinfo:
# 名称
name = bangumiinfo.get("name") or bangumiinfo.get("name_cn")
# 年份
release_date = bangumiinfo.get("date") or bangumiinfo.get("air_date")
if release_date:
year = release_date[:4]
else:
year = None
# 使用名称识别豆瓣媒体信息
return self.match_doubaninfo(
name=name,
year=year,
mtype=MediaType.TV
)
return None

View File

@ -45,6 +45,7 @@ class SubscribeChain(ChainBase):
mtype: MediaType = None, mtype: MediaType = None,
tmdbid: int = None, tmdbid: int = None,
doubanid: str = None, doubanid: str = None,
bangumiid: int = None,
season: int = None, season: int = None,
channel: MessageChannel = None, channel: MessageChannel = None,
userid: str = None, userid: str = None,
@ -100,6 +101,7 @@ class SubscribeChain(ChainBase):
mediainfo = self.recognize_media(mtype=mediainfo.type, mediainfo = self.recognize_media(mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id, tmdbid=mediainfo.tmdb_id,
doubanid=mediainfo.douban_id, doubanid=mediainfo.douban_id,
bangumiid=mediainfo.bangumi_id,
cache=False) cache=False)
if not mediainfo: if not mediainfo:
logger.error(f"媒体信息识别失败!") logger.error(f"媒体信息识别失败!")
@ -124,6 +126,8 @@ class SubscribeChain(ChainBase):
# 合并信息 # 合并信息
if doubanid: if doubanid:
mediainfo.douban_id = doubanid mediainfo.douban_id = doubanid
if bangumiid:
mediainfo.bangumi_id = bangumiid
# 添加订阅 # 添加订阅
sid, err_msg = self.subscribeoper.add(mediainfo, season=season, username=username, **kwargs) sid, err_msg = self.subscribeoper.add(mediainfo, season=season, username=username, **kwargs)
if not sid: if not sid:

View File

@ -153,6 +153,8 @@ class MediaInfo:
tvdb_id: int = None tvdb_id: int = None
# 豆瓣ID # 豆瓣ID
douban_id: str = None douban_id: str = None
# Bangumi ID
bangumi_id: int = None
# 媒体原语种 # 媒体原语种
original_language: str = None original_language: str = None
# 媒体原发行标题 # 媒体原发行标题
@ -185,6 +187,8 @@ class MediaInfo:
tmdb_info: dict = field(default_factory=dict) tmdb_info: dict = field(default_factory=dict)
# 豆瓣 INFO # 豆瓣 INFO
douban_info: dict = field(default_factory=dict) douban_info: dict = field(default_factory=dict)
# Bangumi INFO
bangumi_info: dict = field(default_factory=dict)
# 导演 # 导演
directors: List[dict] = field(default_factory=list) directors: List[dict] = field(default_factory=list)
# 演员 # 演员
@ -240,6 +244,8 @@ class MediaInfo:
self.set_tmdb_info(self.tmdb_info) self.set_tmdb_info(self.tmdb_info)
if self.douban_info: if self.douban_info:
self.set_douban_info(self.douban_info) self.set_douban_info(self.douban_info)
if self.bangumi_info:
self.set_bangumi_info(self.bangumi_info)
def __setattr__(self, name: str, value: Any): def __setattr__(self, name: str, value: Any):
self.__dict__[name] = value self.__dict__[name] = value
@ -540,6 +546,69 @@ class MediaInfo:
if not hasattr(self, key): if not hasattr(self, key):
setattr(self, key, value) setattr(self, key, value)
def set_bangumi_info(self, info: dict):
"""
初始化Bangumi信息
"""
if not info:
return
# 本体
self.bangumi_info = info
# 豆瓣ID
self.bangumi_id = info.get("id")
# 类型
if not self.type:
self.type = MediaType.TV
# 标题
if not self.title:
self.title = info.get("name_cn") or info.get("name")
# 原语种标题
if not self.original_title:
self.original_title = info.get("name")
# 识别标题中的季
meta = MetaInfo(self.title)
# 季
if not self.season:
self.season = meta.begin_season
# 评分
if not self.vote_average:
rating = info.get("rating")
if rating:
vote_average = float(rating.get("score"))
else:
vote_average = 0
self.vote_average = vote_average
# 发行日期
if not self.release_date:
self.release_date = info.get("date") or info.get("air_date")
# 年份
if not self.year:
self.year = self.release_date[:4] if self.release_date else None
# 海报
if not self.poster_path:
self.poster_path = info.get("images", {}).get("large")
# 简介
if not self.overview:
self.overview = info.get("summary")
# 别名
if not self.names:
infobox = info.get("infobox")
if infobox:
akas = [item.get("value") for item in infobox if item.get("key") == "别名"]
if akas:
self.names = [aka.get("v") for aka in akas[0]]
# 剧集
if self.type == MediaType.TV and not self.seasons:
meta = MetaInfo(self.title)
season = meta.begin_season or 1
episodes_count = info.get("total_episodes")
if episodes_count:
self.seasons[season] = list(range(1, episodes_count + 1))
# 演员
if not self.actors:
self.actors = info.get("actors") or []
@property @property
def title_year(self): def title_year(self):
if self.title: if self.title:
@ -558,6 +627,8 @@ class MediaInfo:
return "https://www.themoviedb.org/tv/%s" % self.tmdb_id return "https://www.themoviedb.org/tv/%s" % self.tmdb_id
elif self.douban_id: elif self.douban_id:
return "https://movie.douban.com/subject/%s" % self.douban_id return "https://movie.douban.com/subject/%s" % self.douban_id
elif self.bangumi_id:
return "http://bgm.tv/subject/%s" % self.bangumi_id
return "" return ""
@property @property
@ -621,6 +692,7 @@ class MediaInfo:
dicts["title_year"] = self.title_year dicts["title_year"] = self.title_year
dicts["tmdb_info"] = None dicts["tmdb_info"] = None
dicts["douban_info"] = None dicts["douban_info"] = None
dicts["bangumi_info"] = None
return dicts return dicts
def clear(self): def clear(self):
@ -629,6 +701,7 @@ class MediaInfo:
""" """
self.tmdb_info = {} self.tmdb_info = {}
self.douban_info = {} self.douban_info = {}
self.bangumi_info = {}
self.seasons = {} self.seasons = {}
self.genres = [] self.genres = []
self.season_info = [] self.season_info = []

View File

@ -69,8 +69,8 @@ class MetaBase(object):
_subtitle_flag = False _subtitle_flag = False
_subtitle_season_re = r"(?<![全共]\s*)[第\s]+([0-9一二三四五六七八九十S\-]+)\s*季(?!\s*[全共])" _subtitle_season_re = r"(?<![全共]\s*)[第\s]+([0-9一二三四五六七八九十S\-]+)\s*季(?!\s*[全共])"
_subtitle_season_all_re = r"[全共]\s*([0-9一二三四五六七八九十]+)\s*季|([0-9一二三四五六七八九十]+)\s*季\s*全" _subtitle_season_all_re = r"[全共]\s*([0-9一二三四五六七八九十]+)\s*季|([0-9一二三四五六七八九十]+)\s*季\s*全"
_subtitle_episode_re = r"(?<![全共]\s*)[第\s]+([0-9一二三四五六七八九十百零EP\-]+)\s*[集话話期](?!\s*[全共])" _subtitle_episode_re = r"(?<![全共]\s*)[第\s]+([0-9一二三四五六七八九十百零EP\-]+)\s*[集话話期](?!\s*[全共])"
_subtitle_episode_all_re = r"([0-9一二三四五六七八九十百零]+)\s*集\s*全|[全共]\s*([0-9一二三四五六七八九十百零]+)\s*[集话話期]" _subtitle_episode_all_re = r"([0-9一二三四五六七八九十百零]+)\s*集\s*全|[全共]\s*([0-9一二三四五六七八九十百零]+)\s*[集话話期]"
def __init__(self, title: str, subtitle: str = None, isfile: bool = False): def __init__(self, title: str, subtitle: str = None, isfile: bool = False):
if not title: if not title:
@ -110,7 +110,7 @@ class MetaBase(object):
if not title_text: if not title_text:
return return
title_text = f" {title_text} " title_text = f" {title_text} "
if re.search(r'[全第季集话話期]', title_text, re.IGNORECASE): if re.search(r'[全第季集话話期]', title_text, re.IGNORECASE):
# 第x季 # 第x季
season_str = re.search(r'%s' % self._subtitle_season_re, title_text, re.IGNORECASE) season_str = re.search(r'%s' % self._subtitle_season_re, title_text, re.IGNORECASE)
if season_str: if season_str:

View File

@ -1,6 +1,6 @@
import json import json
import time import time
from typing import Optional from typing import Optional, Union
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -26,7 +26,7 @@ class MessageOper(DbOper):
link: str = None, link: str = None,
userid: str = None, userid: str = None,
action: int = 1, action: int = 1,
note: dict = None, note: Union[list, dict] = None,
**kwargs): **kwargs):
""" """
新增媒体服务器数据 新增媒体服务器数据

View File

@ -1,6 +1,6 @@
import time import time
from sqlalchemy import Column, Integer, String, Sequence from sqlalchemy import Column, Integer, String, Sequence, Float
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.db import db_query, db_update, Base from app.db import db_query, db_update, Base
@ -23,14 +23,15 @@ class Subscribe(Base):
imdbid = Column(String) imdbid = Column(String)
tvdbid = Column(Integer) tvdbid = Column(Integer)
doubanid = Column(String, index=True) doubanid = Column(String, index=True)
bangumiid = Column(Integer, index=True)
# 季号 # 季号
season = Column(Integer) season = Column(Integer)
# 海报 # 海报
poster = Column(String) poster = Column(String)
# 背景图 # 背景图
backdrop = Column(String) backdrop = Column(String)
# 评分 # 评分float
vote = Column(Integer) vote = Column(Float)
# 简介 # 简介
description = Column(String) description = Column(String)
# 过滤规则 # 过滤规则
@ -115,6 +116,11 @@ class Subscribe(Base):
def get_by_doubanid(db: Session, doubanid: str): def get_by_doubanid(db: Session, doubanid: str):
return db.query(Subscribe).filter(Subscribe.doubanid == doubanid).first() return db.query(Subscribe).filter(Subscribe.doubanid == doubanid).first()
@staticmethod
@db_query
def get_by_bangumiid(db: Session, bangumiid: int):
return db.query(Subscribe).filter(Subscribe.bangumiid == bangumiid).first()
@db_update @db_update
def delete_by_tmdbid(self, db: Session, tmdbid: int, season: int): def delete_by_tmdbid(self, db: Session, tmdbid: int, season: int):
subscrbies = self.get_by_tmdbid(db, tmdbid, season) subscrbies = self.get_by_tmdbid(db, tmdbid, season)

View File

@ -27,6 +27,7 @@ class SubscribeOper(DbOper):
imdbid=mediainfo.imdb_id, imdbid=mediainfo.imdb_id,
tvdbid=mediainfo.tvdb_id, tvdbid=mediainfo.tvdb_id,
doubanid=mediainfo.douban_id, doubanid=mediainfo.douban_id,
bangumiid=mediainfo.bangumi_id,
poster=mediainfo.get_poster_image(), poster=mediainfo.get_poster_image(),
backdrop=mediainfo.get_backdrop_image(), backdrop=mediainfo.get_backdrop_image(),
vote=mediainfo.vote_average, vote=mediainfo.vote_average,

View File

@ -1,7 +1,7 @@
import json import json
import queue import queue
import time import time
from typing import Optional, Any from typing import Optional, Any, Union
from app.utils.singleton import Singleton from app.utils.singleton import Singleton
@ -14,7 +14,7 @@ class MessageHelper(metaclass=Singleton):
self.sys_queue = queue.Queue() self.sys_queue = queue.Queue()
self.user_queue = queue.Queue() self.user_queue = queue.Queue()
def put(self, message: Any, role: str = "sys", note: dict = None): def put(self, message: Any, role: str = "sys", note: Union[list, dict] = None):
""" """
存消息 存消息
:param message: 消息 :param message: 消息

View File

@ -0,0 +1,93 @@
from typing import List, Optional, Tuple, Union
from app.core.context import MediaInfo
from app.log import logger
from app.modules import _ModuleBase
from app.modules.bangumi.bangumi import BangumiApi
from app.utils.http import RequestUtils
class BangumiModule(_ModuleBase):
bangumiapi: BangumiApi = None
def init_module(self) -> None:
self.bangumiapi = BangumiApi()
def stop(self):
pass
def test(self) -> Tuple[bool, str]:
"""
测试模块连接性
"""
ret = RequestUtils().get_res("https://api.bgm.tv/")
if ret and ret.status_code == 200:
return True, ""
elif ret:
return False, f"无法连接Bangumi错误码{ret.status_code}"
return False, "Bangumi网络连接失败"
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass
def recognize_media(self, bangumiid: int = None,
**kwargs) -> Optional[MediaInfo]:
"""
识别媒体信息
:param bangumiid: 识别的Bangumi ID
:return: 识别的媒体信息包括剧集信息
"""
if not bangumiid:
return None
# 直接查询详情
info = self.bangumi_info(bangumiid=bangumiid)
if info:
# 赋值TMDB信息并返回
mediainfo = MediaInfo(bangumi_info=info)
logger.info(f"{bangumiid} Bangumi识别结果{mediainfo.type.value} "
f"{mediainfo.title_year}")
return mediainfo
else:
logger.info(f"{bangumiid} 未匹配到Bangumi媒体信息")
return None
def bangumi_info(self, bangumiid: int) -> Optional[dict]:
"""
获取Bangumi信息
:param bangumiid: BangumiID
:return: Bangumi信息
"""
if not bangumiid:
return None
logger.info(f"开始获取Bangumi信息{bangumiid} ...")
return self.bangumiapi.detail(bangumiid)
def bangumi_calendar(self, page: int = 1, count: int = 30) -> Optional[List[dict]]:
"""
获取Bangumi每日放送
:param page: 页码
:param count: 每页数量
"""
return self.bangumiapi.calendar(page, count)
def bangumi_credits(self, bangumiid: int, page: int = 1, count: int = 20) -> List[dict]:
"""
根据TMDBID查询电影演职员表
:param bangumiid: BangumiID
:param page: 页码
:param count: 数量
"""
persons = self.bangumiapi.persons(bangumiid) or []
if persons:
return persons[(page - 1) * count: page * count]
else:
return []
def bangumi_recommend(self, bangumiid: int) -> List[dict]:
"""
根据BangumiID查询推荐电影
:param bangumiid: BangumiID
"""
return self.bangumiapi.subjects(bangumiid) or []

View File

@ -0,0 +1,154 @@
from datetime import datetime
from functools import lru_cache
import requests
from app.utils.http import RequestUtils
class BangumiApi(object):
"""
https://bangumi.github.io/api/
"""
_urls = {
"calendar": "calendar",
"detail": "v0/subjects/%s",
"persons": "v0/subjects/%s/persons",
"subjects": "v0/subjects/%s/subjects"
}
_base_url = "https://api.bgm.tv/"
_req = RequestUtils(session=requests.Session())
def __init__(self):
pass
@classmethod
@lru_cache(maxsize=128)
def __invoke(cls, url, **kwargs):
req_url = cls._base_url + url
params = {}
if kwargs:
params.update(kwargs)
resp = cls._req.get_res(url=req_url, params=params)
try:
return resp.json() if resp else None
except Exception as e:
print(e)
return None
def calendar(self, page: int = 1, count: int = 30):
"""
获取每日放送返回items
"""
"""
[
{
"weekday": {
"en": "Mon",
"cn": "星期一",
"ja": "月耀日",
"id": 1
},
"items": [
{
"id": 350235,
"url": "http://bgm.tv/subject/350235",
"type": 2,
"name": "月が導く異世界道中 第二幕",
"name_cn": "月光下的异世界之旅 第二幕",
"summary": "",
"air_date": "2024-01-08",
"air_weekday": 1,
"rating": {
"total": 257,
"count": {
"1": 1,
"2": 1,
"3": 4,
"4": 15,
"5": 51,
"6": 111,
"7": 49,
"8": 13,
"9": 5,
"10": 7
},
"score": 6.1
},
"rank": 6125,
"images": {
"large": "http://lain.bgm.tv/pic/cover/l/3c/a5/350235_A0USf.jpg",
"common": "http://lain.bgm.tv/pic/cover/c/3c/a5/350235_A0USf.jpg",
"medium": "http://lain.bgm.tv/pic/cover/m/3c/a5/350235_A0USf.jpg",
"small": "http://lain.bgm.tv/pic/cover/s/3c/a5/350235_A0USf.jpg",
"grid": "http://lain.bgm.tv/pic/cover/g/3c/a5/350235_A0USf.jpg"
},
"collection": {
"doing": 920
}
},
{
"id": 358561,
"url": "http://bgm.tv/subject/358561",
"type": 2,
"name": "大宇宙时代",
"name_cn": "大宇宙时代",
"summary": "",
"air_date": "2024-01-22",
"air_weekday": 1,
"rating": {
"total": 2,
"count": {
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 1,
"6": 1,
"7": 0,
"8": 0,
"9": 0,
"10": 0
},
"score": 5.5
},
"images": {
"large": "http://lain.bgm.tv/pic/cover/l/71/66/358561_UzsLu.jpg",
"common": "http://lain.bgm.tv/pic/cover/c/71/66/358561_UzsLu.jpg",
"medium": "http://lain.bgm.tv/pic/cover/m/71/66/358561_UzsLu.jpg",
"small": "http://lain.bgm.tv/pic/cover/s/71/66/358561_UzsLu.jpg",
"grid": "http://lain.bgm.tv/pic/cover/g/71/66/358561_UzsLu.jpg"
},
"collection": {
"doing": 9
}
}
]
}
]
"""
ret_list = []
result = self.__invoke(self._urls["calendar"], _ts=datetime.strftime(datetime.now(), '%Y%m%d'))
if result:
for item in result:
ret_list.extend(item.get("items") or [])
return ret_list[(page - 1) * count: page * count]
def detail(self, bid: int):
"""
获取番剧详情
"""
return self.__invoke(self._urls["detail"] % bid, _ts=datetime.strftime(datetime.now(), '%Y%m%d'))
def persons(self, bid: int):
"""
获取番剧人物
"""
return self.__invoke(self._urls["persons"] % bid, _ts=datetime.strftime(datetime.now(), '%Y%m%d'))
def subjects(self, bid: int):
"""
获取关联条目信息
"""
return self.__invoke(self._urls["subjects"] % bid, _ts=datetime.strftime(datetime.now(), '%Y%m%d'))

View File

@ -14,3 +14,5 @@ from .message import *
from .tmdb import * from .tmdb import *
from .transfer import * from .transfer import *
from .file import * from .file import *
from .bangumi import *
from .douban import *

12
app/schemas/bangumi.py Normal file
View File

@ -0,0 +1,12 @@
from typing import Optional
from pydantic import BaseModel
class BangumiPerson(BaseModel):
id: Optional[int] = None
name: Optional[str] = None
type: Optional[int] = 1
career: Optional[list] = []
images: Optional[dict] = {}
relation: Optional[str] = None

View File

@ -83,6 +83,8 @@ class MediaInfo(BaseModel):
tvdb_id: Optional[str] = None tvdb_id: Optional[str] = None
# 豆瓣ID # 豆瓣ID
douban_id: Optional[str] = None douban_id: Optional[str] = None
# Bangumi ID
bangumi_id: Optional[int] = None
# 媒体原语种 # 媒体原语种
original_language: Optional[str] = None original_language: Optional[str] = None
# 媒体原发行标题 # 媒体原发行标题

14
app/schemas/douban.py Normal file
View File

@ -0,0 +1,14 @@
from typing import Optional
from pydantic import BaseModel
class DoubanPerson(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
roles: Optional[list] = []
title: Optional[str] = None
url: Optional[str] = None
character: Optional[str] = None
avatar: Optional[dict] = None
latin_name: Optional[str] = None

View File

@ -15,6 +15,7 @@ class Subscribe(BaseModel):
keyword: Optional[str] = None keyword: Optional[str] = None
tmdbid: Optional[int] = None tmdbid: Optional[int] = None
doubanid: Optional[str] = None doubanid: Optional[str] = None
bangumiid: Optional[int] = None
# 季号 # 季号
season: Optional[int] = None season: Optional[int] = None
# 海报 # 海报

View File

@ -49,14 +49,3 @@ class TmdbPerson(BaseModel):
popularity: Optional[float] = None popularity: Optional[float] = None
images: Optional[dict] = {} images: Optional[dict] = {}
biography: Optional[str] = None biography: Optional[str] = None
class DoubanPerson(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
roles: Optional[list] = []
title: Optional[str] = None
url: Optional[str] = None
character: Optional[str] = None
avatar: Optional[dict] = None
latin_name: Optional[str] = None

View File

@ -0,0 +1,34 @@
"""1.0.16
Revision ID: d146dea51516
Revises: 5813aaa7cb3a
Create Date: 2024-03-18 18:13:38.099531
"""
import contextlib
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd146dea51516'
down_revision = '5813aaa7cb3a'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with contextlib.suppress(Exception):
with op.batch_alter_table("subscribe") as batch_op:
batch_op.add_column(sa.Column('bangumiid', sa.Integer, nullable=True))
try:
op.create_index('ix_subscribe_bangumiid', 'subscribe', ['bangumiid'], unique=False)
except Exception as err:
pass
# ### end Alembic commands ###
def downgrade() -> None:
pass