""" bilibili_api.bangumi 番剧相关 概念: + media_id: 番剧本身的 ID,有时候也是每季度的 ID,如 https://www.bilibili.com/bangumi/media/md28231846/ + season_id: 每季度的 ID + episode_id: 每集的 ID,如 https://www.bilibili.com/bangumi/play/ep374717 """ import datetime from enum import Enum from typing import Any, List, Tuple, Union, Optional from bilibili_api.utils.danmaku import Danmaku from . import settings from .video import Video from .utils.utils import get_api from .utils.credential import Credential from .exceptions.ApiException import ApiException from .utils.network import Api, get_session, HEADERS from .utils.initial_state import ( get_initial_state, get_initial_state_sync, InitialDataType, ) API = get_api("bangumi") episode_data_cache = {} class BangumiCommentOrder(Enum): """ 短评 / 长评 排序方式 + DEFAULT: 默认 + CTIME: 发布时间倒序 """ DEFAULT = 0 CTIME = 1 class BangumiType(Enum): """ 番剧类型 + BANGUMI: 番剧 + FT: 影视 + GUOCHUANG: 国创 """ BANGUMI = 1 FT = 3 GUOCHUANG = 4 async def get_timeline(type_: BangumiType, before: int = 7, after: int = 0) -> dict: """ 获取番剧时间线 Args: type_(BangumiType): 番剧类型 before(int) : 几天前开始(0~7), defaults to 7 after(int) : 几天后结束(0~7), defaults to 0 """ api = API["info"]["timeline"] params = {"types": type_.value, "before": before, "after": after} return await Api(**api).update_params(**params).result class IndexFilter: """ 番剧索引相关固定参数以及值 """ class Type(Enum): """ 索引类型 + ANIME: 番剧 + MOVIE: 电影 + DOCUMENTARY: 纪录片 + GUOCHUANG: 国创 + TV: 电视剧 + VARIETY: 综艺 """ ANIME = 1 MOVIE = 2 DOCUMENTARY = 3 GUOCHUANG = 4 TV = 5 VARIETY = 7 class Version(Enum): """ 番剧版本 + ALL: 全部 + MAIN: 正片 + FILM: 电影 + OTHER: 其他 """ ALL = -1 MAIN = 1 FILM = 2 OTHER = 3 class Spoken_Language(Enum): """ 配音 + ALL: 全部 + ORIGINAL: 原声 + CHINESE: 中配 """ ALL = -1 ORIGINAL = 1 CHINESE = 2 class Finish_Status(Enum): """ 完结状态 + ALL: 全部 + FINISHED: 完结 + UNFINISHED: 连载 """ ALL = -1 FINISHED = 1 UNFINISHED = 0 class Copyright(Enum): """ 版权方 + ALL: 全部 + EXCLUSIVE: 独家 + OTHER: 其他 """ ALL = -1 EXCLUSIVE = 3 OTHER = "1,2,4" class Season(Enum): """ 季度 + ALL: 全部 + SPRING: 春季 + SUMMER: 夏季 + AUTUMN: 秋季 + WINTER: 冬季 """ ALL = -1 WINTER = 1 SPRING = 4 SUMMER = 7 AUTUMN = 10 @staticmethod def make_time_filter( start: Optional[Union[datetime.datetime, str, int]] = None, end: Optional[Union[datetime.datetime, str, int]] = None, include_start: bool = True, include_end: bool = False, ) -> str: """ 生成番剧索引所需的时间条件 番剧、国创直接传入年份,为 int 或者 str 类型,如 `make_time_filter(start=2019, end=2020)` 影视、纪录片、电视剧传入 datetime.datetime,如 `make_time_filter(start=datetime.datetime(2019, 1, 1), end=datetime.datetime(2020, 1, 1))` start 或 end 为 None 时则表示不设置开始或结尾 Args: start (datetime, str, int): 开始时间. 如果是 None 则不设置开头. end (datetime, str, int): 结束时间. 如果是 None 则不设置结尾. include_start (bool): 是否包含开始时间. 默认为 True. include_end (bool): 是否包含结束时间. 默认为 False. Returns: str: 年代条件 """ start_str = "" end_str = "" if start != None: if isinstance(start, datetime.datetime): start_str = start.strftime("%Y-%m-%d %H:%M:%S") else: start_str = start if end != None: if isinstance(end, datetime.datetime): end_str = end.strftime("%Y-%m-%d %H:%M:%S") else: end_str = end # 是否包含边界 if include_start: start_str = f"[{start_str}" else: start_str = f"({start_str}" if include_end: end_str = f"{end_str}]" else: end_str = f"{end_str})" return f"{start_str},{end_str}" class Producer(Enum): """ 制作方 + ALL: 全部 + CCTV: CCTV + BBC: BBC + DISCOVERY: 探索频道 + NATIONAL_GEOGRAPHIC: 国家地理 + NHK: NHK + HISTORY: 历史频道 + SATELLITE: 卫视 + SELF: 自制 + ITV: ITV + SKY: SKY + ZDF: ZDF + PARTNER: 合作机构 + SONY: 索尼 + GLOBAL_NEWS: 环球 + PARAMOUNT: 派拉蒙 + WARNER: 华纳 + DISNEY: 迪士尼 + DOMESTIC_OTHER: 国内其他 + FOREIGN_OTHER: 国外其他 """ ALL = -1 CCTV = 4 BBC = 1 DISCOVERY = 7 NATIONAL_GEOGRAPHIC = 14 NHK = 2 HISTORY = 6 SATELLITE = 8 SELF = 9 ITV = 5 SKY = 3 ZDF = 10 PARTNER = 11 DOMESTIC_OTHER = 12 FOREIGN_OTHER = 13 SONY = 15 GLOBAL_NEWS = 16 PARAMOUNT = 17 WARNER = 18 DISNEY = 19 class Payment(Enum): """ 观看条件 + ALL: 全部 + FREE: 免费 + PAID: 付费 + VIP: 大会员 """ ALL = -1 FREE = 1 PAID = "2,6" VIP = "4,6" class Area(Enum): """ 地区 + ALL: 全部 + CHINA: 中国 + CHINA_MAINLAND: 中国大陆 + CHINA_HONGKONG_AND_TAIWAN: 中国港台 + JAPAN: 日本 + USA: 美国 + UK: 英国 + SOUTH_KOREA: 韩国 + FRANCE: 法国 + THAILAND: 泰国 + GERMANY: 德国 + ITALY: 意大利 + SPAIN: 西班牙 + ANIME_OTHER: 番剧其他 + MOVIE_OTHER: 影视其他 + DOCUMENTARY_OTHER: 纪录片其他 注意:各索引的 其他 表示的地区都不同 """ ALL = "-1" CHINA = "1,6,7" CHINA_MAINLAND = "1" CHINA_HONGKONG_AND_TAIWAN = "6,7" JAPAN = "2" USA = "3" UK = "4" SOUTH_KOREA = "8" FRANCE = "9" THAILAND = "10" GERMANY = "15" ITALY = "35" SPAIN = "13" ANIME_OTHER = "1,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70" TV_OTHER = "5,8,9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70" MOVIE_OTHER = "5,11,12,14,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70" class Style: """ 风格,根据索引不同,可选的风格也不同 """ class Anime(Enum): """ 番剧风格 + ALL: 全部 + ORIGINAL: 原创 + COMIC: 漫画改 + NOVEL: 小说改 + GAME: 游戏改 + TOKUSATSU: 特摄 + BUDAIXI: 布袋戏 + WARM: 热血 + TIMEBACK: 穿越 + IMAGING: 奇幻 + WAR: 战斗 + FUNNY: 搞笑 + DAILY: 日常 + SCIENCE_FICTION: 科幻 + MOE: 萌系 + HEAL: 治愈 + SCHOOL: 校园 + CHILDREN: 儿童 + NOODLES: 泡面 + LOVE: 恋爱 + GIRLISH: 少女 + MAGIC: 魔法 + ADVENTURE: 冒险 + HISTORY: 历史 + ALTERNATE: 架空 + MACHINE_BATTLE: 机战 + GODS_DEM: 神魔 + VOICE: 声控 + SPORT: 运动 + INSPIRATION: 励志 + MUSIC: 音乐 + ILLATION: 推理 + SOCIEITES: 社团 + OUTWIT: 智斗 + TEAR: 催泪 + FOOD: 美食 + IDOL: 偶像 + OTOME: 乙女 + WORK: 职场 """ ALL = -1 ORIGINAL = 10010 COMIC = 10011 NOVEL = 10012 GAME = 10013 TOKUSATSU = 10102 BUDAIXI = 10015 WARM = 10016 TIMEBACK = 10017 IMAGING = 10018 WAR = 10020 FUNNY = 10021 DAILY = 10022 SCIENCE_FICTION = 10023 MOE = 10024 HEAL = 10025 SCHOOL = 10026 CHILDREN = 10027 NOODLES = 10028 LOVE = 10029 GIRLISH = 10030 MAGIC = 10031 ADVENTURE = 10032 HISTORY = 10033 ALTERNATE = 10034 MACHINE_BATTLE = 10035 GODS_DEMONS = 10036 VOICE = 10037 SPORTS = 10038 INSPIRATIONAL = 10039 MUSIC = 10040 ILLATION = 10041 SOCIETIES = 10042 OUTWIT = 10043 TEAR = 10044 FOODS = 10045 IDOL = 10046 OTOME = 10047 WORK = 10048 class Movie(Enum): """ 电影风格 + ALL: 全部 + SKETCH: 短片 + PLOT: 剧情 + COMEDY: 喜剧 + ROMANTIC: 爱情 + ACTION: 动作 + SCAIRIER: 恐怖 + SCIENCE_FICTION: 科幻 + CRIME: 犯罪 + TIRILLER: 惊悚 + SUSPENSE: 悬疑 + IMAGING: 奇幻 + WAR: 战争 + ANIME: 动画 + BIOAGRAPHY: 传记 + FAMILY: 家庭 + SING_DANCE: 歌舞 + HISTORY: 历史 + DISCOVER: 探险 + DOCUMENTARY: 纪录片 + DISATER: 灾难 + COMIC: 漫画改 + NOVEL: 小说改 """ ALL = -1 SKETCH = 10104 PLOT = 10050 COMEDY = 10051 ROMANTIC = 10052 ACTION = 10053 SCAIRIER = 10054 SCIENCE_FICTION = 10023 CRIME = 10055 TIRILLER = 10056 SUSPENSE = 10057 IMAGING = 10018 WAR = 10058 ANIME = 10059 BIOAGRAPHY = 10060 FAMILY = 10061 SING_DANCE = 10062 HISTORY = 10033 DISCOVER = 10032 DOCUMENTARY = 10063 DISATER = 10064 COMIC = 10011 NOVEL = 10012 class GuoChuang(Enum): """ 国创风格 + ALL: 全部 + ORIGINAL: 原创 + COMIC: 漫画改 + NOVEL: 小说改 + GAME: 游戏改 + DYNAMIC: 动态漫 + BUDAIXI: 布袋戏 + WARM: 热血 + IMAGING: 奇幻 + FANTASY: 玄幻 + WAR: 战斗 + FUNNY: 搞笑 + WUXIA: 武侠 + DAILY: 日常 + SCIENCE_FICTION: 科幻 + MOE: 萌系 + HEAL: 治愈 + SUSPENSE: 悬疑 + SCHOOL: 校园 + CHILDREN: 少儿 + NOODLES: 泡面 + LOVE: 恋爱 + GIRLISH: 少女 + MAGIC: 魔法 + HISTORY: 历史 + MACHINE_BATTLE: 机战 + GODS_DEMONS: 神魔 + VOICE: 声控 + SPORT: 运动 + INSPIRATION: 励志 + MUSIC: 音乐 + ILLATION: 推理 + SOCIEITES: 社团 + OUTWIT: 智斗 + TEAR: 催泪 + FOOD: 美食 + IDOL: 偶像 + OTOME: 乙女 + WORK: 职场 + ANCIENT: 古风 """ ALL = -1 ORIGINAL = 10010 COMIC = 10011 NOVEL = 10012 GAME = 10013 DYNAMIC = 10014 BUDAIXI = 10015 WARM = 10016 IMAGING = 10018 FANTASY = 10019 WAR = 10020 FUNNY = 10021 WUXIA = 10078 DAILY = 10022 SCIENCE_FICTION = 10023 MOE = 10024 HEAL = 10025 SUSPENSE = 10057 SCHOOL = 10026 CHILDREN = 10027 NOODLES = 10028 LOVE = 10029 GIRLISH = 10030 MAGIC = 10031 HISTORY = 10033 MACHINE_BATTLE = 10035 GODS_DEMONS = 10036 VOICE = 10037 SPORTS = 10038 INSPIRATIONAL = 10039 MUSIC = 10040 ILLATION = 10041 SOCIETIES = 10042 OUTWIT = 10043 TEAR = 10044 FOODS = 10045 IDOL = 10046 OTOME = 10047 WORK = 10048 ANCIENT = 10049 class TV(Enum): """ 电视剧风格 + ALL: 全部 + FUNNY: 搞笑 + IMAGING: 奇幻 + WAR: 战争 + WUXIA: 武侠 + YOUTH: 青春 + SKETCH: 短剧 + CITY: 都市 + ANCIENT: 古装 + SPY: 谍战 + CLASSIC: 经典 + EMOTION: 情感 + SUSPENSE: 悬疑 + INSPIRATION: 励志 + MYTH: 神话 + TIMEBACK: 穿越 + YEAR: 年代 + COUNTRYSIDE: 乡村 + INVESTIGATION: 刑侦 + PLOT: 剧情 + FAMILY: 家庭 + HISTORY: 历史 + ARMY: 军旅 """ ALL = -1 FUNNY = 10021 IMAGING = 10018 WAR = 10058 WUXIA = 10078 YOUTH = 10079 SKETCH = 10103 CITY = 10080 COSTUME = 10081 SPY = 10082 CLASSIC = 10083 EMOTION = 10084 SUSPENSE = 10057 INSPIRATIONAL = 10039 MYTH = 10085 TIMEBACK = 10017 YEAR = 10086 COUNTRYSIDE = 10087 INVESTIGATION = 10088 PLOT = 10050 FAMILY = 10061 HISTORY = 10033 ARMY = 10089 class Documentary(Enum): """ 纪录片风格 + ALL: 全部 + HISTORY: 历史 + FOODS: 美食 + HUMANITIES: 人文 + TECHNOLOGY: 科技 + DISCOVER: 探险 + UNIVERSE: 宇宙 + PETS: 萌宠 + SOCIAL: 社会 + ANIMALS: 动物 + NATURE: 自然 + MEDICAL: 医疗 + WAR: 战争 + DISATER: 灾难 + INVESTIGATIONS: 罪案 + MYSTERIOUS: 神秘 + TRAVEL: 旅行 + SPORTS: 运动 + MOVIES: 电影 """ ALL = -1 HISTORY = 10033 FOODS = 10045 HUMANITIES = 10065 TECHNOLOGY = 10066 DISCOVER = 10067 UNIVERSE = 10068 PETS = 10069 SOCIAL = 10070 ANIMALS = 10071 NATURE = 10072 MEDICAL = 10073 WAR = 10074 DISATER = 10064 INVESTIGATIONS = 10075 MYSTERIOUS = 10076 TRAVEL = 10077 SPORTS = 10038 MOVIES = -10 class Variety(Enum): """ 综艺风格 + ALL: 全部 + MUSIC: 音乐 + TALK: 访谈 + TALK_SHOW: 脱口秀 + REALITY_SHOW: 真人秀 + TALENT_SHOW: 选秀 + FOOD: 美食 + TRAVEL: 旅行 + SOIREE: 晚会 + CONCERT: 演唱会 + EMOTION: 情感 + COMEDY: 喜剧 + PARENT_CHILD: 亲子 + CULTURE: 文化 + OFFICE: 职场 + PET: 萌宠 + CULTIVATE: 养成 """ ALL = -1 MUSIC = 10040 TALK = 10091 TALK_SHOW = 10081 REALITY_SHOW = 10092 TALENT_SHOW = 10094 FOOD = 10045 TRAVEL = 10095 SOIREE = 10098 CONCERT = 10096 EMOTION = 10084 COMEDY = 10051 PARENT_CHILD = 10097 CULTURE = 10100 OFFICE = 10048 PET = 10069 CULTIVATE = 10099 class Sort(Enum): """ 排序方式 + DESC: 降序 + ASC: 升序 """ DESC = "0" ASC = "1" class Order(Enum): """ 排序字段 + UPDATE: 更新时间 + DANMAKU: 弹幕数量 + PLAY: 播放数量 + FOLLOWER: 追番人数 + SOCRE: 最高评分 + ANIME_RELEASE: 番剧开播日期 + MOVIE_RELEASE: 电影上映日期 """ UPDATE = "0" DANMAKU = "1" PLAY = "2" FOLLOWER = "3" SCORE = "4" ANIME_RELEASE = "5" MOVIE_RELEASE = "6" class IndexFilterMeta: """ IndexFilter 元数据 用于传入 get_index_info 方法 """ class Anime: def __init__( self, version: IndexFilter.Version = IndexFilter.Version.ALL, spoken_language: IndexFilter.Spoken_Language = IndexFilter.Spoken_Language.ALL, area: IndexFilter.Area = IndexFilter.Area.ALL, finish_status: IndexFilter.Finish_Status = IndexFilter.Finish_Status.ALL, copyright: IndexFilter.Copyright = IndexFilter.Copyright.ALL, payment: IndexFilter.Payment = IndexFilter.Payment.ALL, season: IndexFilter.Season = IndexFilter.Season.ALL, year: str = -1, style: IndexFilter.Style.Anime = IndexFilter.Style.Anime.ALL, ) -> None: """ Anime Meta Args: version (Index_Filter.Version): 类型,如正片、电影等 spoken_language (Index_Filter.Spoken_Language): 配音 area (Index_Filter.Area): 地区 finish_status (Index_Filter.Finish_Status): 是否完结 copyright (Index_Filter.Copryright): 版权 payment (Index_Filter.Payment): 付费门槛 season (Index_Filter.Season): 季度 year (str): 年份,调用 Index_Filter.make_time_filter() 传入年份 (int, str) 获取 style (Index_Filter.Style.Anime): 风格 """ self.season_type = IndexFilter.Type.ANIME self.season_version = version self.spoken_language_type = spoken_language self.area = area self.is_finish = finish_status self.copyright = copyright self.season_status = payment self.season_month = season self.year = year self.style_id = style class Movie: def __init__( self, area: IndexFilter.Area = IndexFilter.Area.ALL, release_date: str = -1, style: IndexFilter.Style.Movie = IndexFilter.Style.Movie.ALL, payment: IndexFilter.Payment = IndexFilter.Payment.ALL, ) -> None: """ Movie Meta Args: area (Index_Filter.Area): 地区 payment (Index_Filter.Payment): 付费门槛 season (Index_Filter.Season): 季度 release_date (str): 上映时间,调用 Index_Filter.make_time_filter() 传入年份 (datetime.datetime) 获取 style (Index_Filter.Style.Movie): 风格 """ self.season_type = IndexFilter.Type.MOVIE self.area = area self.release_date = release_date self.style_id = style self.season_status = payment class Documentary: def __init__( self, release_date: str = -1, style: IndexFilter.Style.Documentary = IndexFilter.Style.Documentary.ALL, payment: IndexFilter.Payment = IndexFilter.Payment.ALL, producer: IndexFilter.Producer = IndexFilter.Producer.ALL, ) -> None: """ Documentary Meta Args: area (Index_Filter.Area): 地区 release_date (str): 上映时间,调用 Index_Filter.make_time_filter() 传入年份 (datetime.datetime) 获取 style (Index_Filter.Style.Documentary): 风格 producer (Index_Filter.Producer): 制作方 """ self.season_type = IndexFilter.Type.DOCUMENTARY self.release_date = release_date self.style_id = style self.season_status = payment self.producer_id = producer class TV: def __init__( self, area: IndexFilter.Area = IndexFilter.Area.ALL, release_date: str = -1, style: IndexFilter.Style.TV = IndexFilter.Style.TV.ALL, payment: IndexFilter.Payment = IndexFilter.Payment.ALL, ) -> None: """ TV Meta Args: area (Index_Filter.Area): 地区 payment (Index_Filter.Payment): 付费门槛 release_date (str): 上映时间,调用 Index_Filter.make_time_filter() 传入年份 (datetime.datetime) 获取 style (Index_Filter.Style.TV): 风格 """ self.season_type = IndexFilter.Type.TV self.area = area self.release_date = release_date self.style_id = style self.season_status = payment class GuoChuang: def __init__( self, version: IndexFilter.Version = IndexFilter.Version.ALL, finish_status: IndexFilter.Finish_Status = IndexFilter.Finish_Status.ALL, copyright: IndexFilter.Copyright = IndexFilter.Copyright.ALL, payment: IndexFilter.Payment = IndexFilter.Payment.ALL, year: str = -1, style: IndexFilter.Style.GuoChuang = IndexFilter.Style.GuoChuang.ALL, ) -> None: """ Guochuang Meta Args: version (Index_Filter.VERSION): 类型,如正片、电影等 finish_status (Index_Filter.Finish_Status): 是否完结 copyright (Index_Filter.Copyright): 版权 payment (Index_Filter.Payment): 付费门槛 year (str): 年份,调用 Index_Filter.make_time_filter() 传入年份 (int, str) 获取 style (Index_Filter.Style.GuoChuang): 风格 """ self.season_type = IndexFilter.Type.GUOCHUANG self.season_version = version self.is_finish = finish_status self.copyright = copyright self.season_status = payment self.year = year self.style_id = style class Variety: def __init__( self, style: IndexFilter.Style.Variety = IndexFilter.Style.Variety.ALL, payment: IndexFilter.Payment = IndexFilter.Payment.ALL, ) -> None: """ Variety Meta Args: payment (Index_Filter.Payment): 付费门槛 style (Index_Filter.Style.Variety): 风格 """ self.season_type = IndexFilter.Type.VARIETY self.season_status = payment self.style_id = style async def get_index_info( filters: IndexFilterMeta = IndexFilterMeta.Anime(), order: IndexFilter.Order = IndexFilter.Order.SCORE, sort: IndexFilter.Sort = IndexFilter.Sort.DESC, pn: int = 1, ps: int = 20, ) -> dict: """ 查询番剧索引,索引的详细参数信息见 `IndexFilterMeta` 请先通过 `IndexFilterMeta` 构造 filters Args: filters (Index_Filter_Meta, optional): 筛选条件元数据. Defaults to Anime. order (BANGUMI_INDEX.ORDER, optional): 排序字段. Defaults to SCORE. sort (BANGUMI_INDEX.SORT, optional): 排序方式. Defaults to DESC. pn (int, optional): 页数. Defaults to 1. ps (int, optional): 每页数量. Defaults to 20. Returns: dict: 调用 API 返回的结果 """ api = API["info"]["index"] params = {} for key, value in filters.__dict__.items(): if value is not None: if isinstance(value, Enum): params[key] = value.value else: params[key] = value if order in params: if ( order == IndexFilter.Order.SCORE.value and sort == IndexFilter.Sort.ASC.value ): raise ValueError( "order 为 Index_Filter.ORDER.SCORE 时,sort 不能为 Index_Filter.SORT.ASC" ) # 必要参数 season_type、type # 常规参数 params["order"] = order.value params["sort"] = sort.value params["page"] = pn params["pagesize"] = ps # params["st"] 未知参数,暂时不传 # params["type"] 未知参数,为 1 params["type"] = 1 return await Api(**api).update_params(**params).result class Bangumi: """ 番剧类 Attributes: credential (Credential): 凭据类 """ def __init__( self, media_id: int = -1, ssid: int = -1, epid: int = -1, oversea: bool = False, credential: Union[Credential, None] = None, ) -> None: """ Args: media_id (int, optional) : 番剧本身的 ID. Defaults to -1. ssid (int, optional) : 每季度的 ID. Defaults to -1. epid (int, optional) : 每集的 ID. Defaults to -1. oversea (bool, optional) : 是否要采用兼容的港澳台Api,用于仅限港澳台地区番剧的信息请求. Defaults to False. credential (Credential | None, optional): 凭据类. Defaults to None. """ if media_id == -1 and ssid == -1 and epid == -1: raise ValueError("需要 Media_id 或 Season_id 或 epid 中的一个 !") self.credential = credential if credential else Credential() # 处理极端情况 params = {} self.__ssid = ssid if self.__ssid == -1 and epid == -1: api = API["info"]["meta"] params = {"media_id": media_id} meta = Api(**api, credential=credential).update_params(**params).result_sync self.__ssid = meta["media"]["season_id"] params["media_id"] = media_id # 处理正常情况 if self.__ssid != -1: params["season_id"] = self.__ssid if epid != -1: params["ep_id"] = epid self.oversea = oversea if oversea: api = API["info"]["collective_info_oversea"] else: api = API["info"]["collective_info"] resp = Api(**api, credential=credential).update_params(**params).result_sync self.__raw = resp self.__epid = epid # 确认有结果后,取出数据 self.__ssid = resp["season_id"] self.__media_id = resp["media_id"] if "up_info" in resp: self.__up_info = resp["up_info"] else: self.__up_info = {} # 获取剧集相关 self.ep_list = resp.get("episodes") self.ep_item = [{}] # 出海 Api 和国内的字段有些不同 if self.ep_list: if self.oversea: self.ep_item = [ item for item in self.ep_list if item["ep_id"] == self.__epid ] else: self.ep_item = [ item for item in self.ep_list if item["id"] == self.__epid ] def get_media_id(self) -> int: return self.__media_id def get_season_id(self) -> int: return self.__ssid def get_up_info(self) -> dict: """ 番剧上传者信息 出差或者原版 Returns: Api 相关字段 """ return self.__up_info def get_raw(self) -> Tuple[dict, bool]: """ 原始初始化数据 Returns: Api 相关字段 """ return self.__raw, self.oversea def set_media_id(self, media_id: int) -> None: self.__init__(media_id=media_id, credential=self.credential) def set_ssid(self, ssid: int) -> None: self.__init__(ssid=ssid, credential=self.credential) async def get_meta(self) -> dict: """ 获取番剧元数据信息(评分,封面 URL,标题等) Returns: dict: 调用 API 返回的结果 """ credential = self.credential if self.credential is not None else Credential() api = API["info"]["meta"] params = {"media_id": self.__media_id} return await Api(**api, credential=credential).update_params(**params).result async def get_short_comment_list( self, order: BangumiCommentOrder = BangumiCommentOrder.DEFAULT, next: Union[str, None] = None, ) -> dict: """ 获取短评列表 Args: order (BangumiCommentOrder, optional): 排序方式。Defaults to BangumiCommentOrder.DEFAULT next (str | None, optional) : 调用返回结果中的 next 键值,用于获取下一页数据。Defaults to None Returns: dict: 调用 API 返回的结果 """ credential = self.credential if self.credential is not None else Credential() api = API["info"]["short_comment"] params = {"media_id": self.__media_id, "ps": 20, "sort": order.value} if next is not None: params["cursor"] = next return await Api(**api, credential=credential).update_params(**params).result async def get_long_comment_list( self, order: BangumiCommentOrder = BangumiCommentOrder.DEFAULT, next: Union[str, None] = None, ) -> dict: """ 获取长评列表 Args: order (BangumiCommentOrder, optional): 排序方式。Defaults to BangumiCommentOrder.DEFAULT next (str | None, optional) : 调用返回结果中的 next 键值,用于获取下一页数据。Defaults to None Returns: dict: 调用 API 返回的结果 """ credential = self.credential if self.credential is not None else Credential() api = API["info"]["long_comment"] params = {"media_id": self.__media_id, "ps": 20, "sort": order.value} if next is not None: params["cursor"] = next return await Api(**api, credential=credential).update_params(**params).result async def get_episode_list(self) -> dict: """ 获取季度分集列表,自动转换出海Api的字段,适配部分,但是键还是有不同 Returns: dict: 调用 API 返回的结果 """ if self.oversea: # 转换 ep_id->id ,index_title->longtitle ,index->title fix_ep_list = [] for item in self.ep_list: item["id"] = item.get("ep_id") item["longtitle"] = item.get("index_title") item["title"] = item.get("index") fix_ep_list.append(item) return {"main_section": {"episodes": fix_ep_list}} else: credential = ( self.credential if self.credential is not None else Credential() ) api = API["info"]["episodes_list"] params = {"season_id": self.__ssid} return ( await Api(**api, credential=credential).update_params(**params).result ) async def get_episodes(self) -> List["Episode"]: """ 获取番剧所有的剧集,自动生成类。 """ global episode_data_cache episode_list = await self.get_episode_list() if len(episode_list["main_section"]["episodes"]) == 0: return [] first_epid = episode_list["main_section"]["episodes"][0]["id"] credential = self.credential if self.credential else Credential() content_type = None while content_type != InitialDataType.NEXT_DATA: bangumi_meta, content_type = await get_initial_state( url=f"https://www.bilibili.com/bangumi/play/ep{first_epid}", credential=credential, ) bangumi_meta["media_id"] = self.get_media_id() episodes = [] for ep in episode_list["main_section"]["episodes"]: episode_data_cache[ep["id"]] = { "bangumi_meta": bangumi_meta, "bangumi_class": self, } episodes.append(Episode(epid=ep["id"], credential=self.credential)) return episodes async def get_stat(self) -> dict: """ 获取番剧播放量,追番等信息 Returns: dict: 调用 API 返回的结果 """ credential = self.credential if self.credential is not None else Credential() api = API["info"]["season_status"] params = {"season_id": self.__ssid} return await Api(**api, credential=credential).update_params(**params).result async def get_overview(self) -> dict: """ 获取番剧全面概括信息,包括发布时间、剧集情况、stat 等情况 Returns: dict: 调用 API 返回的结果 """ credential = self.credential if self.credential is not None else Credential() if self.oversea: api = API["info"]["collective_info_oversea"] else: api = API["info"]["collective_info"] params = {"season_id": self.__ssid} return await Api(**api, credential=credential).update_params(**params).result async def set_follow( bangumi: Bangumi, status: bool = True, credential: Union[Credential, None] = None ) -> dict: """ 追番状态设置 Args: bangumi (Bangumi) : 番剧类 status (bool, optional) : 追番状态. Defaults to True. credential (Credential | None, optional): 凭据. Defaults to None. Returns: dict: 调用 API 返回的结果 """ credential = credential if credential is not None else Credential() credential.raise_for_no_sessdata() api = API["operate"]["follow_add"] if status else API["operate"]["follow_del"] data = {"season_id": bangumi.get_season_id()} return await Api(**api, credential=credential).update_data(**data).result async def update_follow_status( bangumi: Bangumi, status: int, credential: Union[Credential, None] = None ) -> dict: """ 更新追番状态 Args: bangumi (Bangumi) : 番剧类 credential (Credential | None, optional): 凭据. Defaults to None. status (int) : 追番状态 1 想看 2 在看 3 已看 Returns: dict: 调用 API 返回的结果 """ credential = credential if credential is not None else Credential() credential.raise_for_no_sessdata() api = API["operate"]["follow_status"] data = {"season_id": bangumi.get_season_id(), "status": status} return await Api(**api, credential=credential).update_data(**data).result class Episode(Video): """ 番剧剧集类 Attributes: credential (Credential): 凭据类 video_class (Video) : 视频类 bangumi (Bangumi) : 所属番剧 """ def __init__(self, epid: int, credential: Union[Credential, None] = None): """ Args: epid (int) : 番剧 epid credential (Credential, optional): 凭据. Defaults to None. """ global episode_data_cache self.credential: Credential = credential if credential else Credential() self.__epid: int = epid if not epid in episode_data_cache.keys(): res, content_type = get_initial_state_sync( url=f"https://www.bilibili.com/bangumi/play/ep{self.__epid}", credential=self.credential, ) # 随机 __NEXT_DATA__ 见 https://github.com/Nemo2011/bilibili-api/issues/433 if content_type == InitialDataType.NEXT_DATA: content = res["props"]["pageProps"]["dehydratedState"]["queries"][0][ "state" ]["data"]["seasonInfo"]["mediaInfo"] self.bangumi = ( Bangumi(ssid=content["season_id"]) if not epid in episode_data_cache.keys() else episode_data_cache[epid]["bangumi_class"] ) for ep_info in content["episodes"]: if int( ep_info["id"] if "id" in ep_info else ep_info["ep_id"] ) == int(epid): bvid = ep_info["bvid"] self.__ep_info: dict = ep_info break else: # InitialDataType.INITIAL_STATE self.__ep_info: dict = res["epInfo"] self.bangumi = ( Bangumi(ssid=res["mediaInfo"]["season_id"]) if not epid in episode_data_cache.keys() else episode_data_cache[epid]["bangumi_class"] ) bvid = res["epInfo"]["bvid"] else: content = episode_data_cache[epid]["bangumi_meta"] bvid = None for einfo in content["props"]["pageProps"]["dehydratedState"]["queries"][0][ "state" ]["data"]["seasonInfo"]["mediaInfo"]["episodes"]: if einfo["ep_id"] == epid: bvid = einfo["bvid"] self.bangumi = episode_data_cache[epid]["bangumi_class"] self.__ep_info: dict = episode_data_cache[epid] self.video_class = Video(bvid=bvid, credential=self.credential) super().__init__(bvid=bvid, credential=self.credential) self.set_aid = self.set_aid_e self.set_bvid = self.set_bvid_e def get_epid(self) -> int: """ 获取 epid """ return self.__epid def set_aid_e(self, aid: int) -> None: print("Set aid is not allowed in Episode") def set_bvid_e(self, bvid: str) -> None: print("Set bvid is not allowed in Episode") async def get_cid(self) -> int: """ 获取稿件 cid Returns: int: cid """ return (await self.get_episode_info())["epInfo"]["cid"] def get_bangumi(self) -> "Bangumi": """ 获取对应的番剧 Returns: Bangumi: 番剧类 """ return self.bangumi # type: ignore def set_epid(self, epid: int) -> None: self.__init__(epid, self.credential) async def get_episode_info(self) -> dict: """ 获取番剧单集信息 Returns: HTML 中的数据 """ if self.__ep_info is None: res, content_type = get_initial_state( url=f"https://www.bilibili.com/bangumi/play/ep{self.__epid}", credential=self.credential, ) if content_type == InitialDataType.NEXT_DATA: content = res["props"]["pageProps"]["dehydratedState"]["queries"][0][ "state" ]["data"]["mediaInfo"] for ep_info in content["episodes"]: if int( ep_info["id"] if "id" in ep_info else ep_info["ep_id"] ) == int(self.get_epid()): return ep_info else: return res["epInfo"] else: return self.__ep_info async def get_bangumi_from_episode(self) -> "Bangumi": """ 获取剧集对应的番剧 Returns: Bangumi: 输入的集对应的番剧类 """ info = await self.get_episode_info() ssid = info["mediaInfo"]["season_id"] return Bangumi(ssid=ssid) async def get_download_url(self) -> dict: """ 获取番剧剧集下载信息。 Returns: dict: 调用 API 返回的结果。 """ api = API["info"]["playurl"] if True: params = { "avid": self.get_aid(), "ep_id": self.get_epid(), "qn": "127", "otype": "json", "fnval": 4048, "fourk": 1, } return ( await Api(**api, credential=self.credential).update_params(**params).result ) async def get_danmaku_xml(self) -> str: """ 获取所有弹幕的 xml 源文件(非装填) Returns: str: 文件源 """ cid = await self.get_cid() url = f"https://comment.bilibili.com/{cid}.xml" sess = get_session() config: dict[str, Any] = {"url": url} # 代理 if settings.proxy: config["proxies"] = {"all://", settings.proxy} resp = await sess.get(**config) return resp.content.decode("utf-8") async def get_danmaku_view(self) -> dict: """ 获取弹幕设置、特殊弹幕、弹幕数量、弹幕分段等信息。 Returns: dict: 二进制流解析结果 """ return await self.video_class.get_danmaku_view(0) async def get_danmakus( self, date: Union[datetime.date, None] = None ) -> List["Danmaku"]: """ 获取弹幕 Args: date (datetime.date | None, optional): 指定某一天查询弹幕. Defaults to None. (不指定某一天) Returns: dict[Danmaku]: 弹幕列表 """ return await self.video_class.get_danmakus(0, date) async def get_history_danmaku_index( self, date: Union[datetime.date, None] = None ) -> Union[None, List[str]]: """ 获取特定月份存在历史弹幕的日期。 Args: date (datetime.date | None, optional): 精确到年月. Defaults to None。 Returns: None | List[str]: 调用 API 返回的结果。不存在时为 None。 """ return await self.video_class.get_history_danmaku_index(0, date)