♻️ 为B站动态API建立model,转发动态放入repost字段 (#507)

* 🐛 替换为pyd兼容性写法

* ♻️ 优化 Bilibili 推送解析

* 🐛 为B站动态API建立Model,动态转发部分放入repost字段

* 🔥 删掉无关紧要的ttl字段

* 🐛 修理转发链接解析

* 🧑‍💻 增加可读性

* 🐛 使用pyd兼容函数

* 🐛 处理 pyd1 model rebuild 时没有递归的情况

* ♻️ 将链接生成进行统一
This commit is contained in:
Azide 2024-03-29 15:08:06 +08:00 committed by GitHub
parent 6cceb07a33
commit 9bc5be5426
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 295 additions and 431 deletions

View File

@ -1,16 +1,16 @@
import re
import json
from abc import ABC
from typing import Any
from copy import deepcopy
from enum import Enum, unique
from typing_extensions import Self
from datetime import datetime, timedelta
from typing import Any, TypeVar, TypeAlias, NamedTuple
from httpx import AsyncClient
from nonebot.log import logger
from pydantic import Field, BaseModel
from nonebot.compat import type_validate_python
from nonebot.compat import PYDANTIC_V2, ConfigDict, type_validate_json, type_validate_python
from nonebot_bison.compat import model_rebuild
@ -19,6 +19,90 @@ from ..types import Tag, Target, RawPost, ApiError, Category
from ..utils import SchedulerConfig, http_client, text_similarity
from .platform import NewMessage, StatusChange, CategoryNotSupport, CategoryNotRecognize
TBaseModel = TypeVar("TBaseModel", bound=type[BaseModel])
# 不能当成装饰器用
# 当装饰器用时global namespace 中还没有被装饰的类,会报错
def model_rebuild_recurse(cls: TBaseModel) -> TBaseModel:
"""Recursively rebuild all BaseModel subclasses in the class."""
if not PYDANTIC_V2:
from inspect import isclass, getmembers
for _, sub_cls in getmembers(cls, lambda x: isclass(x) and issubclass(x, BaseModel)):
model_rebuild_recurse(sub_cls)
model_rebuild(cls)
return cls
class Base(BaseModel):
if PYDANTIC_V2:
model_config = ConfigDict(from_attributes=True)
else:
class Config:
orm_mode = True
class APIBase(Base):
"""Bilibili API返回的基础数据"""
code: int
message: str
class UserAPI(APIBase):
class Card(Base):
name: str
class Data(Base):
card: "UserAPI.Card"
data: Data | None = None
class PostAPI(APIBase):
class Info(Base):
uname: str
class UserProfile(Base):
info: "PostAPI.Info"
class Origin(Base):
uid: int
dynamic_id: int
dynamic_id_str: str
timestamp: int
type: int
rid: int
bvid: str | None = None
class Desc(Base):
dynamic_id: int
dynamic_id_str: str
timestamp: int
type: int
user_profile: "PostAPI.UserProfile"
rid: int
bvid: str | None = None
origin: "PostAPI.Origin | None" = None
class Card(Base):
desc: "PostAPI.Desc"
card: str
class Data(Base):
cards: "list[PostAPI.Card] | None"
data: Data | None = None
DynRawPost: TypeAlias = PostAPI.Card
model_rebuild_recurse(UserAPI)
model_rebuild_recurse(PostAPI)
class BilibiliClient:
_client: AsyncClient
@ -95,10 +179,10 @@ class Bilibili(NewMessage):
async def get_target_name(cls, client: AsyncClient, target: Target) -> str | None:
res = await client.get("https://api.bilibili.com/x/web-interface/card", params={"mid": target})
res.raise_for_status()
res_data = res.json()
if res_data["code"]:
res_data = type_validate_json(UserAPI, res.content)
if res_data.code != 0:
return None
return res_data["data"]["card"]["name"]
return res_data.data.card.name if res_data.data else None
@classmethod
async def parse_target(cls, target_text: str) -> Target:
@ -109,7 +193,7 @@ class Bilibili(NewMessage):
else:
raise cls.ParseTargetException()
async def get_sub_list(self, target: Target) -> list[RawPost]:
async def get_sub_list(self, target: Target) -> list[DynRawPost]:
params = {"host_uid": target, "offset": 0, "need_top": 0}
res = await self.client.get(
"https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history",
@ -117,42 +201,45 @@ class Bilibili(NewMessage):
timeout=4.0,
)
res.raise_for_status()
res_dict = res.json()
if res_dict["code"] == 0:
return res_dict["data"].get("cards", [])
else:
raise ApiError(res.request.url)
res_obj = type_validate_json(PostAPI, res.content)
def get_id(self, post: RawPost) -> Any:
return post["desc"]["dynamic_id"]
if res_obj.code == 0:
if (data := res_obj.data) and (card := data.cards):
return card
return []
raise ApiError(res.request.url)
def get_date(self, post: RawPost) -> int:
return post["desc"]["timestamp"]
def get_id(self, post: DynRawPost) -> int:
return post.desc.dynamic_id
def get_date(self, post: DynRawPost) -> int:
return post.desc.timestamp
def _do_get_category(self, post_type: int) -> Category:
if post_type == 2:
return Category(1)
elif post_type == 64:
return Category(2)
elif post_type == 8:
return Category(3)
elif post_type == 4:
return Category(4)
elif post_type == 1:
# 转发
return Category(5)
raise CategoryNotRecognize(post_type)
match post_type:
case 2:
return Category(1)
case 64:
return Category(2)
case 8:
return Category(3)
case 4:
return Category(4)
case 1:
# 转发
return Category(5)
case unknown_type:
raise CategoryNotRecognize(unknown_type)
def get_category(self, post: RawPost) -> Category:
post_type = post["desc"]["type"]
def get_category(self, post: DynRawPost) -> Category:
post_type = post.desc.type
return self._do_get_category(post_type)
def get_tags(self, raw_post: RawPost) -> list[Tag]:
# FIXME: 更深的原因可能是返回格式的变动,需要进一步确认
if topic_info := raw_post["display"].get("topic_info"):
return [tp["topic_name"] for tp in topic_info["topic_details"]]
return []
def get_tags(self, raw_post: DynRawPost) -> list[Tag]:
card_content = json.loads(raw_post.card)
text: str = card_content["item"]["content"]
result: list[str] = re.findall(r"#(.*?)#", text)
return result
def _text_process(self, dynamic: str, desc: str, title: str) -> str:
similarity = 1.0 if len(dynamic) == 0 or len(desc) == 0 else text_similarity(dynamic, desc)
@ -164,61 +251,85 @@ class Bilibili(NewMessage):
text = dynamic + "\n=================\n" + title + "\n\n" + desc
return text
def _get_info(self, post_type: Category, card) -> tuple[str, list]:
if post_type == 1:
# 一般动态
text = card["item"]["description"]
pic = [img["img_src"] for img in card["item"]["pictures"]]
elif post_type == 2:
# 专栏文章
text = "{} {}".format(card["title"], card["summary"])
pic = card["image_urls"]
elif post_type == 3:
# 视频
dynamic = card.get("dynamic", "")
title = card["title"]
desc = card.get("desc", "")
text = self._text_process(dynamic, desc, title)
pic = [card["pic"]]
elif post_type == 4:
# 纯文字
text = card["item"]["content"]
pic = []
else:
raise CategoryNotSupport(post_type)
return text, pic
def _raw_post_parse(self, raw_post: DynRawPost, in_repost: bool = False):
class ParsedPost(NamedTuple):
text: str
pics: list[str]
url: str | None
repost_owner: str | None = None
repost: "ParsedPost | None" = None
async def parse(self, raw_post: RawPost) -> Post:
card_content = json.loads(raw_post["card"])
post_type = self.get_category(raw_post)
target_name = raw_post["desc"]["user_profile"]["info"]["uname"]
if post_type >= 1 and post_type < 5:
url = ""
if post_type == 1:
card_content: dict[str, Any] = json.loads(raw_post.card)
repost_owner: str | None = ou["info"]["uname"] if (ou := card_content.get("origin_user")) else None
def extract_url_id(url_template: str, name: str) -> str | None:
if in_repost:
if origin := raw_post.desc.origin:
return url_template.format(getattr(origin, name))
return None
return url_template.format(getattr(raw_post.desc, name))
match self._do_get_category(raw_post.desc.type):
case 1:
# 一般动态
url = "https://t.bilibili.com/{}".format(raw_post["desc"]["dynamic_id_str"])
elif post_type == 2:
url = extract_url_id("https://t.bilibili.com/{}", "dynamic_id_str")
text: str = card_content["item"]["description"]
pic: list[str] = [img["img_src"] for img in card_content["item"]["pictures"]]
return ParsedPost(text, pic, url, repost_owner)
case 2:
# 专栏文章
url = "https://www.bilibili.com/read/cv{}".format(raw_post["desc"]["rid"])
elif post_type == 3:
url = extract_url_id("https://www.bilibili.com/read/cv{}", "rid")
text = "{} {}".format(card_content["title"], card_content["summary"])
pic = card_content["image_urls"]
return ParsedPost(text, pic, url, repost_owner)
case 3:
# 视频
url = "https://www.bilibili.com/video/{}".format(raw_post["desc"]["bvid"])
elif post_type == 4:
url = extract_url_id("https://www.bilibili.com/video/{}", "bvid")
dynamic = card_content.get("dynamic", "")
title = card_content["title"]
desc = card_content.get("desc", "")
text = self._text_process(dynamic, desc, title)
pic = [card_content["pic"]]
return ParsedPost(text, pic, url, repost_owner)
case 4:
# 纯文字
url = "https://t.bilibili.com/{}".format(raw_post["desc"]["dynamic_id_str"])
text, pic = self._get_info(post_type, card_content)
elif post_type == 5:
# 转发
url = "https://t.bilibili.com/{}".format(raw_post["desc"]["dynamic_id_str"])
text = card_content["item"]["content"]
orig_type = card_content["item"]["orig_type"]
orig = json.loads(card_content["origin"])
orig_text, pic = self._get_info(self._do_get_category(orig_type), orig)
text += "\n--------------\n"
text += orig_text
else:
raise CategoryNotSupport(post_type)
return Post(self, text, url=url, images=pic, nickname=target_name)
url = extract_url_id("https://t.bilibili.com/{}", "dynamic_id_str")
text = card_content["item"]["content"]
pic = []
return ParsedPost(text, pic, url, repost_owner)
case 5:
# 转发
url = extract_url_id("https://t.bilibili.com/{}", "dynamic_id_str")
text = card_content["item"]["content"]
orig_type: int = card_content["item"]["orig_type"]
orig_card: str = card_content["origin"]
orig_post = DynRawPost(desc=raw_post.desc, card=orig_card)
orig_post.desc.type = orig_type
orig_parsed_post = self._raw_post_parse(orig_post, in_repost=True)
return ParsedPost(text, [], url, repost_owner, orig_parsed_post)
case unsupported_type:
raise CategoryNotSupport(unsupported_type)
async def parse(self, raw_post: DynRawPost) -> Post:
parsed_raw_post = self._raw_post_parse(raw_post)
post = Post(
self,
parsed_raw_post.text,
url=parsed_raw_post.url,
images=list(parsed_raw_post.pics),
nickname=raw_post.desc.user_profile.info.uname,
)
if rp := parsed_raw_post.repost:
post.repost = Post(
self,
rp.text,
url=rp.url,
images=list(rp.pics),
nickname=rp.repost_owner,
)
return post
class Bilibililive(StatusChange):

View File

@ -130,7 +130,7 @@ class Weibo(NewMessage):
def _get_text(self, raw_text: str) -> str:
text = raw_text.replace("<br/>", "\n").replace("<br />", "\n")
selector = etree.HTML(text)
selector = etree.HTML(text, parser=None)
if selector is None:
return text
url_elems = selector.xpath("//a[@href]/span[@class='surl-text']")

View File

@ -1,6 +1,6 @@
{
"code": 0,
"msg": "",
"ttl": 0,
"message": "",
"data": {
"has_more": 1,

View File

@ -1,246 +0,0 @@
{
"data": {
"cards": [
{
"desc": {
"type": 2
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": ""
}
]
}
}
},
{
"desc": {
"type": 1
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": ""
}
]
}
}
},
{
"desc": {
"type": 8
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": "明日方舟"
},
{
"topic_name": "风笛"
},
{
"topic_name": "琴柳"
},
{
"topic_name": "风暴瞭望"
},
{
"topic_name": "轮换池"
},
{
"topic_name": "打卡挑战"
}
]
}
}
},
{
"desc": {
"type": 2
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": ""
}
]
}
}
},
{
"desc": {
"type": 1
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": "明日方舟"
},
{
"topic_name": "饼学大厦"
},
{
"topic_name": "可露希尔的秘密档案"
}
]
}
}
},
{
"desc": {
"type": 1
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": "罗德岛相簿"
},
{
"topic_name": "可露希尔的秘密档案"
},
{
"topic_name": "罗德岛闲逛部"
}
]
}
}
},
{
"desc": {
"type": 8
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": "明日方舟"
},
{
"topic_name": "轮换学"
},
{
"topic_name": "常驻标准寻访"
},
{
"topic_name": "轮换池"
},
{
"topic_name": "打卡挑战"
},
{
"topic_name": "舟游"
}
]
}
}
},
{
"desc": {
"type": 2
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": "明日方舟"
},
{
"topic_name": "饼学大厦"
}
]
}
}
},
{
"desc": {
"type": 4
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": ""
}
]
}
}
},
{
"desc": {
"type": 1
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": "明日方舟"
},
{
"topic_name": "饼学大厦"
},
{
"topic_name": "罗德岛相簿"
},
{
"topic_name": "可露希尔的秘密档案"
},
{
"topic_name": "罗德岛闲逛部"
}
]
}
}
},
{
"desc": {
"type": 1
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": "明日方舟"
},
{
"topic_name": "饼学大厦"
}
]
}
}
},
{
"desc": {
"type": 1
},
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": "明日方舟"
},
{
"topic_name": "饼学大厦"
},
{
"topic_name": "罗德岛相簿"
},
{
"topic_name": "可露希尔的秘密档案"
},
{
"topic_name": "罗德岛闲逛部"
}
]
}
}
}
]
}
}

View File

@ -1,6 +1,6 @@
{
"code": 0,
"msg": "",
"ttl": 0,
"message": "",
"data": {
"has_more": 1,

View File

@ -1,6 +1,6 @@
{
"code": 0,
"msg": "",
"ttl": 0,
"message": "",
"data": {
"has_more": 1,

View File

@ -5,13 +5,16 @@ import respx
import pytest
from nonebug.app import App
from httpx import Response, AsyncClient
from nonebot.compat import model_dump, type_validate_python
from .utils import get_json
@pytest.fixture(scope="module")
def bing_dy_list():
return get_json("bilibili_bing_list.json")["data"]["cards"]
@pytest.fixture()
def bing_dy_list(app: App):
from nonebot_bison.platform.bilibili import PostAPI
return type_validate_python(PostAPI, get_json("bilibili_bing_list.json")).data.cards # type: ignore
if typing.TYPE_CHECKING:
@ -19,69 +22,73 @@ if typing.TYPE_CHECKING:
@pytest.fixture()
def bilibili(app: App):
def bilibili(app: App) -> "Bilibili":
from nonebot_bison.utils import ProcessContext
from nonebot_bison.platform import platform_manager
return platform_manager["bilibili"](ProcessContext(), AsyncClient())
return platform_manager["bilibili"](ProcessContext(), AsyncClient()) # type: ignore
@pytest.fixture()
def without_dynamic(app: App):
return {
"code": 0,
"msg": "",
"message": "",
"data": {
"has_more": 0,
"next_offset": 0,
"_gt_": 0,
},
}
from nonebot_bison.platform.bilibili import PostAPI
# 先验证实际的空动态返回能否通过校验,再重新导出
return model_dump(
type_validate_python(
PostAPI,
{
"code": 0,
"ttl": 1,
"message": "",
"data": {
"cards": None,
"has_more": 0,
"next_offset": 0,
"_gt_": 0,
},
},
)
)
@pytest.mark.asyncio
async def test_get_tag_without_topic_info(bilibili, bing_dy_list):
simple_raw_post = {
"display": {
"topic_info": {
"topic_details": [
{
"topic_name": "可露希尔的秘密档案",
},
{
"topic_name": "罗德岛相簿",
},
],
},
},
}
async def test_get_tag(bilibili: "Bilibili", bing_dy_list):
from nonebot_bison.platform.bilibili import DynRawPost
simple_raw_post_without_topic_info = {
"display": {
"damedane": "dameyo",
},
}
raw_post_has_tag = type_validate_python(DynRawPost, bing_dy_list[0])
raw_post_has_tag.card = '{"user":{"uid":111111,"uname":"1111","face":"https://i2.hdslb.com/bfs/face/0b.jpg"},"item":{"rp_id":11111,"uid":31111,"content":"#测试1#\\n测试\\n#测试2#\\n#测\\n测\\n测#","ctrl":"","reply":0}}'
res1 = bilibili.get_tags(simple_raw_post)
assert res1 == ["可露希尔的秘密档案", "罗德岛相簿"]
raw_post_has_no_tag = type_validate_python(DynRawPost, bing_dy_list[1])
raw_post_has_no_tag.card = '{"user":{"uid":111111,"uname":"1111","face":"https://i2.hdslb.com/bfs/face/0b.jpg"},"item":{"rp_id":11111,"uid":31111,"content":"测试1\\n测试\\n测试2\\n#测\\n测\\n测#","ctrl":"","reply":0}}'
res2 = bilibili.get_tags(simple_raw_post_without_topic_info)
res1 = bilibili.get_tags(raw_post_has_tag)
assert res1 == ["测试1", "测试2"]
res2 = bilibili.get_tags(raw_post_has_no_tag)
assert res2 == []
@pytest.mark.asyncio
async def test_video_forward(bilibili, bing_dy_list):
from nonebot_bison.post import Post
post: Post = await bilibili.parse(bing_dy_list[1])
assert (
post.content
== "答案揭晓:宿舍!来看看投票结果\nhttps://t.bilibili.com/568093580488553786\n--------------\n#可露希尔的秘密档案#"
" \n11来宿舍休息一下吧 \n档案来源lambda:\\罗德岛内务\\秘密档案 \n发布时间9/12 1:00 P.M."
" \n档案类型:可见 \n档案描述:今天请了病假在宿舍休息。很舒适。"
" \n提供者:赫默\n=================\n《可露希尔的秘密档案》11话来宿舍休息一下吧"
assert post.content == """答案揭晓:宿舍!来看看投票结果\nhttps://t.bilibili.com/568093580488553786"""
assert post.repost is not None
# 注意原文前几行末尾是有空格的
assert post.repost.content == (
"#可露希尔的秘密档案# \n"
"11来宿舍休息一下吧 \n"
"档案来源lambda:\\罗德岛内务\\秘密档案 \n"
"发布时间9/12 1:00 P.M. \n"
"档案类型:可见 \n"
"档案描述:今天请了病假在宿舍休息。很舒适。 \n"
"提供者:赫默\n"
"=================\n"
"《可露希尔的秘密档案》11话来宿舍休息一下吧"
)
assert post.url == "https://t.bilibili.com/569448354910819194"
assert post.repost.url == "https://www.bilibili.com/video/BV1E3411q7nU"
assert post.get_priority_themes()[0] == "basic"
@ -99,40 +106,61 @@ async def test_video_forward_without_dynamic(bilibili, bing_dy_list):
"包含慕夏对新PV的个人解读风笛厨力疯狂放出CP言论输出9.16轮换池预测视频分析和理智规划杂谈内容。"
"\n注意:内含大量个人性质对风笛的厨力观点与多CP混乱发言不适者请及时点击退出或跳到下一片段。"
)
assert post.repost is None
assert post.url == "https://www.bilibili.com/video/BV1K44y1h7Xg"
assert post.get_priority_themes()[0] == "basic"
@pytest.mark.asyncio
async def test_article_forward(bilibili, bing_dy_list):
async def test_article_forward(bilibili: "Bilibili", bing_dy_list):
post = await bilibili.parse(bing_dy_list[4])
assert (
post.content == "#明日方舟##饼学大厦#\n9.11专栏更新完毕,"
"这还塌了实属没跟新运营对上\n后边除了周日发饼和PV没提及的中文语音"
"稳了\n别忘了来参加#可露希尔的秘密档案#的主题投票\nhttps://t.bilibili.com/568093580488553786?tab=2"
+ "\n--------------\n"
+ "【明日方舟】饼学大厦#12~14风暴瞭望&玛莉娅·临光&红松林&感谢庆典9.11更新"
" 更新记录09.11更新覆盖09.10更新以及排期更新猜测周一周五开活动09.10更新:以周五开活动为底,"
"PV/公告调整位置整体结构更新09.08更新:饼学大厦#12更新"
"新增一件六星商店服饰周日发饼09.06更新:饼学大厦整栋整栋翻新,"
"改为9.16开主线四日无饼09.05凌晨更新10.13后的排期(两日无饼,鹰角背刺,"
"心狠手辣)前言感谢楪筱祈ぺ的动态-哔哩哔哩 (bilibili.com) 对饼学的贡献后续排期9.17【风暴瞭望】、"
"10.01【玛莉娅·临光】复刻、10.1"
assert post.content == (
"#明日方舟##饼学大厦#\n"
"9.11专栏更新完毕,这还塌了实属没跟新运营对上\n"
"后边除了周日发饼和PV没提及的中文语音稳了\n"
"别忘了来参加#可露希尔的秘密档案#的主题投票\n"
"https://t.bilibili.com/568093580488553786?tab=2"
)
assert post.repost is not None
assert post.repost.content == (
"【明日方舟】饼学大厦#12~14风暴瞭望&玛莉娅·临光&红松林&感谢庆典)"
"9.11更新 更新记录09.11更新覆盖09.10更新;以及排期更新,猜测周一周五开活动"
"09.10更新以周五开活动为底PV/公告调整位置,整体结构更新"
"09.08更新:饼学大厦#12更新新增一件六星商店服饰周日发饼"
"09.06更新饼学大厦整栋整栋翻新改为9.16开主线(四日无饼!)"
"09.05凌晨更新10.13后的排期(两日无饼,鹰角背刺,心狠手辣)"
"前言感谢楪筱祈ぺ的动态-哔哩哔哩 (bilibili.com) 对饼学的贡献!"
"后续排期9.17【风暴瞭望】、10.01【玛莉娅·临光】复刻、10.1"
)
assert post.url == "https://t.bilibili.com/569189870889648693"
assert post.repost.url == "https://www.bilibili.com/read/cv12993752"
@pytest.mark.asyncio
async def test_dynamic_forward(bilibili, bing_dy_list):
post = await bilibili.parse(bing_dy_list[5])
assert (
post.content == "饼组主线饼学预测——9.11版\n①今日结果\n9.11 殿堂上的游禽-星极(x"
"新运营实锤了)\n②后续预测\n9.12 #罗德岛相簿#+#可露希尔的秘密档案#11话\n9.13"
" 六星先锋(执旗手)干员-琴柳\n9.14 宣传策略-空弦+家具\n9.15 轮换池(+中文语音前瞻)\n9.16"
" 停机\n9.17 #罗德岛闲逛部#+新六星EP+EP09·风暴瞭望开启\n9.19 #罗德岛相簿#"
+ "\n--------------\n"
+ "#明日方舟#\n【新增服饰】\n//殿堂上的游禽 - 星极\n塞壬唱片偶像企划《闪耀阶梯》特供服饰/殿堂上的游禽。"
"星极自费参加了这项企划,尝试着用大众能接受的方式演绎天空之上的故事。\n\n_____________\n谦逊留给观众,"
"骄傲发自歌喉,此夜,唯我璀璨。 "
assert post.content == (
"饼组主线饼学预测——9.11版\n"
"①今日结果\n"
"9.11 殿堂上的游禽-星极(x新运营实锤了)\n"
"②后续预测\n"
"9.12 #罗德岛相簿#+#可露希尔的秘密档案#11话\n"
"9.13 六星先锋(执旗手)干员-琴柳\n9.14 宣传策略-空弦+家具\n"
"9.15 轮换池(+中文语音前瞻)\n"
"9.16 停机\n"
"9.17 #罗德岛闲逛部#+新六星EP+EP09·风暴瞭望开启\n"
"9.19 #罗德岛相簿#"
)
assert post.repost.content == (
"#明日方舟#\n"
"【新增服饰】\n"
"//殿堂上的游禽 - 星极\n"
"塞壬唱片偶像企划《闪耀阶梯》特供服饰/殿堂上的游禽。星极自费参加了这项企划,尝试着用大众能接受的方式演绎天空之上的故事。\n\n"
"_____________\n"
"谦逊留给观众,骄傲发自歌喉,此夜,唯我璀璨。 "
)
assert post.url == "https://t.bilibili.com/569107343093484983"
assert post.repost.url == "https://t.bilibili.com/569105539209306328"
@pytest.mark.asyncio
@ -196,32 +224,3 @@ async def test_parse_target(bilibili: "Bilibili"):
assert res2 == "161775300"
with pytest.raises(Platform.ParseTargetException):
await bilibili.parse_target("https://www.bilibili.com/video/BV1qP4y1g738?spm_id_from=333.999.0.0")
@pytest.fixture(scope="module")
def post_list():
return get_json("bilibili_fake_dy_list.json")["data"]["cards"]
# 测试新tag机制的平台推送情况
@pytest.mark.asyncio
async def test_filter_user_custom(bilibili, post_list):
only_banned_tags = ["~可露希尔的秘密档案"]
res0 = await bilibili.filter_user_custom(post_list, [], only_banned_tags)
assert len(res0) == 8
only_subscribed_tags = ["可露希尔的秘密档案"]
res1 = await bilibili.filter_user_custom(post_list, [], only_subscribed_tags)
assert len(res1) == 4
multi_subs_tags_1 = ["可露希尔的秘密档案", "罗德岛相簿"]
res2 = await bilibili.filter_user_custom(post_list, [], multi_subs_tags_1)
assert len(res2) == 4
multi_subs_tags_2 = ["罗德岛相簿", "风暴瞭望"]
res3 = await bilibili.filter_user_custom(post_list, [], multi_subs_tags_2)
assert len(res3) == 4
multi_subs_tags_3 = ["明日方舟", "~饼学大厦"]
res4 = await bilibili.filter_user_custom(post_list, [], multi_subs_tags_3)
assert len(res4) == 2