feat: Transplant and adapt the code required for development based on bilibili-api

This commit is contained in:
LWR
2022-10-29 01:57:01 +08:00
parent 539e4385dc
commit b5b2be1740
21 changed files with 2773 additions and 0 deletions

6
starbot/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
import asyncio
import platform
# 如果系统为 Windows则修改默认策略以解决代理报错问题
if 'windows' in platform.system().lower():
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

362
starbot/api/live.json Normal file
View File

@@ -0,0 +1,362 @@
{
"info": {
"room_play_info": {
"url": "https://api.live.bilibili.com/xlive/web-room/v1/index/getRoomPlayInfo",
"method": "GET",
"verify": false,
"params": {
"room_id": "int: 房间号"
},
"comment": "获取房间信息(真实房间号,封禁情况等)"
},
"chat_conf": {
"url": "https://api.live.bilibili.com/room/v1/Danmu/getConf",
"method": "GET",
"verify": false,
"params": {
"room_id": "int: 真实房间号"
},
"comment": "获取聊天弹幕服务器配置信息(websocket)"
},
"room_info": {
"url": "https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom",
"method": "GET",
"verify": false,
"params": {
"room_id": "int: 真实房间号"
},
"comment": "获取直播间信息(标题,简介等)"
},
"user_info_in_room": {
"url": "https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByUser",
"method": "GET",
"verify": true,
"params": {
"room_id": "int: 真实房间号"
},
"comment": "获取自己在直播间的信息(粉丝勋章等级,直播用户等级等)"
},
"area_info": {
"url": "http://api.live.bilibili.com/room/v1/Area/getList",
"method": "GET",
"verify": false,
"params": null,
"comment": "获取直播间分区信息"
},
"user_info": {
"url": "https://api.live.bilibili.com/xlive/web-ucenter/user/get_user_info",
"method": "GET",
"verify": true,
"params": null,
"comment": "获取直播用户等级等信息"
},
"user_guards": {
"url": "https://api.live.bilibili.com/xlive/web-ucenter/user/guards",
"method": "GET",
"verify": true,
"params": {
"page": "页码",
"page_size": "每页数量, 过多可能报错 默认10"
},
"comment": "获取用户开通的大航海列表"
},
"bag_list": {
"url": "https://api.live.bilibili.com/xlive/web-room/v1/gift/bag_list",
"method": "GET",
"verify": "true",
"params": null,
"comment": "获取自己的礼物包裹"
},
"dahanghai": {
"url": "https://api.live.bilibili.com/xlive/app-room/v1/guardTab/topList",
"method": "GET",
"verify": false,
"params": {
"roomid": "int: 真实房间号",
"page": "int: 页码",
"ruid": "int: 全称 room_uid从 room_play_info 里头的 uid 可以找到",
"page_size": 29
},
"comment": "获取大航海列表"
},
"gaonengbang": {
"url": "https://api.live.bilibili.com/xlive/general-interface/v1/rank/getOnlineGoldRank",
"method": "GET",
"verify": false,
"params": {
"roomId": "int: 真实房间号",
"page": "int: 页码",
"ruid": "int: 全称 room_uid从 room_play_info 里头的 uid 可以找到",
"pageSize": 50
},
"comment": "获取高能榜"
},
"live_info": {
"url": "https://api.live.bilibili.com/xlive/web-ucenter/user/live_info",
"method": "GET",
"verify": false,
"params": null,
"comment": "获取自己粉丝牌,大航海等数据"
},
"general_info": {
"url": "https://api.live.bilibili.com/xlive/fuxi-interface/general/half/initial",
"method": "GET",
"verify": true,
"params": {
"actId": "未知大航海信息100061",
"roomId": "真实房间号",
"uid": "直播者uid",
"csrf,csrf_token": "要给两个"
},
"comment": "获取自己在当前房间的大航海信息, 是否开通,等级,当前经验,同时可获得自己开通的所有航海日志"
},
"seven_rank": {
"url": "https://api.live.bilibili.com/rankdb/v1/RoomRank/webSevenRank",
"method": "GET",
"verify": false,
"params": {
"roomid": "int: 真实房间号",
"ruid": "int: 全称 room_uid从 room_play_info 里头的 uid 可以找到"
},
"comment": "获取七日榜"
},
"fans_medal_rank": {
"url": "https://api.live.bilibili.com/rankdb/v1/RoomRank/webMedalRank",
"method": "GET",
"verify": false,
"params": {
"roomid": "int: 真实房间号",
"ruid": "int: 全称 room_uid从 room_play_info 里头的 uid 可以找到"
},
"comment": "获取粉丝勋章排行榜"
},
"black_list": {
"url": "https://api.live.bilibili.com/xlive/web-ucenter/v1/banned/GetSilentUserList",
"method": "POST",
"verify": true,
"params": {
"room_id": "int: 真实房间号",
"ps": "const int: 1"
},
"comment": "获取房间黑名单列表,登录账号需要是该房间房管"
},
"room_play_url": {
"url": "https://api.live.bilibili.com/xlive/web-room/v1/playUrl/playUrl",
"method": "GET",
"verify": false,
"params": {
"cid": "int: 真实房间号",
"platform": "const str: web",
"qn": "int: 清晰度编号,原画 10000蓝光 400超清 250高清 150流畅 80",
"https_url_req": "const int: 1",
"ptype": "const int: 16"
},
"comment": "获取房间直播流列表"
},
"room_play_info_v2": {
"url": "https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo",
"method": "GET",
"verify": false,
"params": {
"room_id": "int: 真实房间号",
"protocol": "int: 流协议0 为 FLV 流1 为 HLS 流。默认0,1",
"format": "int: 容器格式0 为 flv 格式1 为 ts 格式(仅限 hls 流2 为 fmp4 格式(仅限 hls 流。默认0,2",
"codec": "int: 视频编码0 为 avc 编码1 为 hevc 编码。默认0,1",
"qn": "int: 清晰度编号原画10000建议4K800蓝光(杜比)401蓝光400超清250高清150流畅80默认0",
"platform": "const str: web",
"ptype": "const int: 16"
},
"comment": "获取房间信息及可用清晰度列表"
},
"gift_common": {
"url": "https://api.live.bilibili.com/xlive/web-room/v1/giftPanel/giftData",
"method": "GET",
"verify": false,
"params": {
"room_id": "int: 显示房间号",
"platform": "const str: pc",
"source": "const str: live",
"area_id": "int: 子分区 ID 可以不用填",
"area_parent_id": "int: 父分区 ID 可以不用填, 获取分区 ID 可使用 get_area_info 方法"
},
"comment": "获取该直播间通用礼物的信息,此 API 只返回 gift_id ,不包含礼物 price 参数"
},
"gift_special": {
"url": "https://api.live.bilibili.com//xlive/web-room/v1/giftPanel/tabRoomGiftList",
"method": "GET",
"verify": false,
"params": {
"room_id": "int: 显示房间号",
"platform": "const str: pc",
"source": "const str: live",
"tab_id": "int: 礼物tab编号2 为特权礼物3 为定制礼物",
"build": "int: 未知作用, 默认 1",
"area_id": "int: 子分区 ID 可以不用填",
"area_parent_id": "int: 父分区 ID 可以不用填, 获取分区id可使用 get_area_info 方法"
},
"comment": "获取该直播间特殊礼物的信息"
},
"gift_config": {
"url": "https://api.live.bilibili.com/xlive/web-room/v1/giftPanel/giftConfig",
"method": "GET",
"verify": false,
"params": {
"room_id": "int: 显示房间号 可以不用填",
"platform": "const str: pc",
"source": "const str: live",
"area_id": "int: 子分区 ID 可以不用填",
"area_parent_id": "int: 父分区 ID 可以不用填, 获取分区id可使用 get_area_info 方法"
},
"comment": "获取所有礼物信息,三个字段可以不用填,但填了有助于减小返回内容的大小,置空返回约 2.7w 行,填了三个对应值返回约 1.4w 行"
},
"followers_live_info": {
"url": "https://api.live.bilibili.com/xlive/app-interface/v1/relation/liveAnchor",
"method": "GET",
"verify": false,
"params": {
"filterRule": "int: 0 ,未知",
"need_recommend": "int: 是否接受推荐直播间, 0为不接受, 1为接受"
},
"comment": "获取关注列表中正在直播的直播间信息, 包括房间直播热度, 房间名称及标题, 清晰度, 是否官方认证等信息."
},
"followers_unlive_info": {
"url": "https://api.live.bilibili.com/xlive/app-interface/v1/relation/unliveAnchor",
"method": "GET",
"verify": false,
"params": {
"page": "int: 页码",
"pagesize": "每页数量,过多可能报错 默认30"
},
"comment": "获取关注列表中未在直播的直播间信息, 包括上次开播时间, 上次开播的类别, 直播间公告, 是否有录播等."
}
},
"operate": {
"send_danmaku": {
"url": "https://api.live.bilibili.com/msg/send",
"method": "POST",
"verify": true,
"params": {
"roomid": "int: 真实房间号",
"color": "int: 十进制颜色,有权限限制",
"fontsize": "int: 字体大小,默认 25",
"mode": "int: 弹幕模式1 飞行 5 顶部 4 底部",
"msg": "str: 弹幕信息",
"rnd": "int: 当前时间戳",
"bubble": "int: 默认 0功能不知",
"csrf,csrf_token": "str: 要给两个"
},
"comment": "发送直播间弹幕,有的参数不确定因为自己不搞这块没权限发一些样式的弹幕"
},
"add_block": {
"url": "https://api.live.bilibili.com/xlive/web-ucenter/v1/banned/AddSilentUser",
"method": "POST",
"verify": true,
"params": {
"room_id": "int: 真实房间号",
"tuid": "int: 封禁用户 UID",
"mobile_app": "str: 设备类型",
"visit_id": "str: 空"
},
"comment": "封禁用户"
},
"del_block": {
"url": "https://api.live.bilibili.com/banned_service/v1/Silent/del_room_block_user",
"method": "POST",
"verify": true,
"params": {
"roomid": "int: 真实房间号",
"id": "int: 封禁 ID从 ID.info.black_list 中获取或者 ID.operate.black_list 的返回值获取",
"visit_id": "str: 空"
},
"comment": "解封用户"
},
"sign_up_dahanghai": {
"url": "https://api.live.bilibili.com/xlive/activity-interface/v2/userTask/UserTaskSignUp",
"method": "POST",
"verify": true,
"params": {
"task_id": "int: 任务 id签到1447可能还有别的",
"uid": "int: 真实房间号",
"csrf,csrf_token": "要给两个"
},
"comment": "航海日志签到"
},
"send_gift_from_bag": {
"url": "https://api.live.bilibili.com/xlive/revenue/v1/gift/sendBag",
"method": "POST",
"verify": true,
"params": {
"uid": "int: 赠送用户的 UID",
"bag_id": "int: 礼物包裹的id",
"gift_id": "int: 礼物id",
"gift_num": "int: 赠送数量",
"platform": "const str: pc",
"send_ruid": "int: 未知作用默认0",
"storm_beat_id": "int: 未知作用默认0",
"price": "int: 礼物单价背包中的礼物价值默认0",
"biz_code": "const str: live",
"biz_id": "int: room_display_id 房间显示 ID",
"ruid": "int: 全称 room_uid从 room_play_info 里头的 UID 可以找到",
"csrf,csrf_token": "要给两个"
},
"comment": "在直播间中赠送包裹中的礼物,包裹信息可用 get_self_bag 方法获取"
},
"send_gift_gold": {
"url": "https://api.live.bilibili.com/xlive/revenue/v1/gift/sendGold",
"method": "POST",
"verify": true,
"params": {
"uid": "int: 赠送用户的 UID",
"gift_id": "int: 礼物 ID",
"ruid": "int: 全称 room_uid从 room_play_info 里头的uid可以找到",
"send_ruid": "int: 未知作用默认0",
"gift_num": "int: 赠送数量",
"coin_type": "const str: gold",
"bag_id": "int: 0",
"platform": "const str: pc",
"biz_code": "const str: Live",
"biz_id": "int: room_display_id 房间显示 ID",
"rnd": "int: 当前时间戳",
"storm_beat_id": "int: 未知作用默认0",
"price": "int: 礼物单价",
"visit_id": "void: 空",
"csrf,csrf_token": "要给两个"
},
"comment": "在直播间中赠送金瓜子礼物"
},
"send_gift_silver": {
"url": "https://api.live.bilibili.com/xlive/revenue/v1/gift/sendSilver",
"method": "POST",
"verify": true,
"params": {
"uid": "int: 赠送用户的 UID",
"gift_id": "int: 礼物 ID 辣条的 ID 为 1",
"ruid": "int: 全称 room_uid从 room_play_info 里头的 UID 可以找到",
"send_ruid": "int: 未知作用默认0",
"gift_num": "int: 赠送数量",
"coin_type": "const str: silver",
"bag_id": "int: 0",
"platform": "const str: pc",
"biz_code": "const str: Live",
"biz_id": "int: room_display_id 房间显示id",
"rnd": "int: 当前时间戳",
"storm_beat_id": "int: 未知作用默认0",
"price": "int: 礼物单价 辣条单价为100",
"visit_id": "int: 空",
"csrf,csrf_token": "要给两个"
},
"comment": "在直播间中赠送银瓜子礼物"
},
"receive_reward": {
"url": "https://api.live.bilibili.com/xlive/activity-interface/v2/spec-act/sep-guard/receive/awards",
"method": "POST",
"verify": true,
"params": {
"ruid": "int: 房间真实id",
"receive_type": "int: 领取类型, 全部领取2",
"csrf,csrf_token": "要给两个"
},
"comment": "领取航海日志奖励"
}
}
}

244
starbot/api/user.json Normal file
View File

@@ -0,0 +1,244 @@
{
"info": {
"my_info": {
"url": "https://api.bilibili.com/x/space/myinfo",
"method": "GET",
"verify": true,
"comment": "获取自己的信息"
},
"info": {
"url": "https://api.bilibili.com/x/space/acc/info",
"method": "GET",
"verify": false,
"params": {
"mid": "int: uid"
},
"comment": "用户基本信息"
},
"relation": {
"url": "https://api.bilibili.com/x/relation/stat",
"method": "GET",
"verify": false,
"params": {
"vmid": "int: uid"
},
"comment": "关注数,粉丝数"
},
"upstat": {
"url": "https://api.bilibili.com/x/space/upstat",
"method": "GET",
"verify": false,
"params": {
"mid": "int: uid"
},
"comment": "视频播放量,文章阅读量,总点赞数"
},
"live": {
"url": "https://api.bilibili.com/x/space/acc/info",
"method": "GET",
"verify": false,
"params": {
"mid": "int: uid"
},
"comment": "直播间基本信息"
},
"video": {
"url": "https://api.bilibili.com/x/space/arc/search",
"method": "GET",
"verify": false,
"params": {
"mid": "int: uid",
"ps": "const int: 30",
"tid": "int: 分区 ID0 表示全部",
"pn": "int: 页码",
"keyword": "str: 关键词,可为空",
"order": "str: pubdate 上传日期pubdate 播放量pubdate 收藏量"
},
"comment": "搜索用户视频"
},
"audio": {
"url": "https://api.bilibili.com/audio/music-service/web/song/upper",
"method": "GET",
"verify": false,
"params": {
"uid": "int: uid",
"ps": "const int: 30",
"pn": "int: 页码",
"order": "int: 1 最新发布2 最多播放3 最多收藏"
},
"comment": "音频"
},
"article": {
"url": "https://api.bilibili.com/x/space/article",
"method": "GET",
"verify": false,
"params": {
"mid": "int: uid",
"ps": "const int: 30",
"pn": "int: 页码",
"sort": "str: publish_time 最新发布publish_time 最多阅读publish_time 最多收藏"
},
"comment": "专栏"
},
"article_lists": {
"url": "https://api.bilibili.com/x/article/up/lists",
"method": "GET",
"verify": false,
"params": {
"mid": "int: uid",
"sort": "int: 0 最近更新1 最多阅读"
},
"comment": "专栏文集"
},
"dynamic": {
"url": "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history",
"method": "GET",
"verify": false,
"params": {
"host_uid": "int: uid",
"offset_dynamic_id": "int: 动态偏移用,第一页为 0",
"need_top": "int bool: 是否显示置顶动态"
},
"comment": "用户动态信息"
},
"bangumi": {
"url": "https://api.bilibili.com/x/space/bangumi/follow/list",
"method": "GET",
"verify": false,
"params": {
"vmid": "int: uid",
"pn": "int: 页码",
"ps": "const int: 15",
"type": "int: 1 追番2 追剧"
},
"comment": "用户追番列表"
},
"followings": {
"url": "https://api.bilibili.com/x/relation/followings",
"method": "GET",
"verify": true,
"params": {
"vmid": "int: uid",
"ps": "const int: 20",
"pn": "int: 页码",
"order": "str: desc 倒序, asc 正序"
},
"comment": "获取用户关注列表(不是自己只能访问前 5 页)"
},
"followers": {
"url": "https://api.bilibili.com/x/relation/followers",
"method": "GET",
"verify": true,
"params": {
"vmid": "int: uid",
"ps": "const int: 20",
"pn": "int: 页码",
"order": "str: desc 倒序, asc 正序"
},
"comment": "获取用户粉丝列表(不是自己只能访问前 5 页,是自己也不能获取全部的样子)"
},
"overview": {
"url": "https://api.bilibili.com/x/space/navnum",
"method": "GET",
"verify": false,
"params": {
"mid": "int: uid",
"jsonp": "const str: jsonp"
},
"comment": "获取用户的简易订阅和投稿信息(主要是这些的数量统计)"
},
"self_subscribe_group": {
"url": "https://api.bilibili.com/x/relation/tags",
"method": "GET",
"verify": true,
"params": {},
"comment": "获取自己的关注分组列表,用于操作关注"
},
"get_user_in_which_subscribe_groups": {
"url": "https://api.bilibili.com/x/relation/tag/user",
"method": "GET",
"verify": true,
"params": {
"fid": "int: uid"
},
"comment": "获取用户在哪一个分组"
},
"history": {
"url": "https://api.bilibili.com/x/v2/history",
"method": "GET",
"verify": true,
"params": {
"pn": "int: 页码",
"ps": "const int: 100"
},
"comment": "用户浏览历史记录"
}
},
"operate": {
"modify": {
"url": "https://api.bilibili.com/x/relation/modify",
"method": "POST",
"verify": true,
"data": {
"fid": "int: UID",
"act": "int: 1 关注 2 取关 3 悄悄关注 5 拉黑 6 取消拉黑 7 移除粉丝",
"re_src": "const int: 11"
},
"comment": "用户关系操作"
},
"send_msg": {
"url": "https://api.vc.bilibili.com/web_im/v1/web_im/send_msg",
"method": "POST",
"verify": true,
"data": {
"msg[sender_uid]": "int: 自己的 UID",
"msg[receiver_id]": "int: 对方 UID",
"msg[receiver_type]": "const int: 1",
"msg[msg_type]": "const int: 1",
"msg[msg_status]": "const int: 0",
"msg[content]": {
"content": "str: 文本内容"
}
},
"comment": "给用户发信息"
},
"create_subscribe_group": {
"url": "https://api.bilibili.com/x/relation/tag/create",
"method": "POST",
"verify": true,
"data": {
"tag": "str: 分组名"
},
"comment": "添加关注分组"
},
"del_subscribe_group": {
"url": "https://api.bilibili.com/x/relation/tag/del",
"method": "POST",
"verify": true,
"data": {
"tagid": "int: 分组 id"
},
"comment": "删除关注分组"
},
"rename_subscribe_group": {
"url": "https://api.bilibili.com/x/relation/tag/update",
"method": "POST",
"verify": true,
"data": {
"tagid": "int: 分组 id",
"name": "str: 新的分组名"
},
"comment": "重命名分组"
},
"set_user_subscribe_group": {
"url": "https://api.bilibili.com/x/relation/tags/addUsers",
"method": "POST",
"verify": true,
"data": {
"fids": "int: UID",
"tagids": "commaSeparatedList[int]: 分组的 tagids逗号分隔"
},
"comment": "移动用户到关注分组"
}
}
}

0
starbot/core/__init__.py Normal file
View File

1053
starbot/core/live.py Normal file

File diff suppressed because it is too large Load Diff

491
starbot/core/user.py Normal file
View File

@@ -0,0 +1,491 @@
"""
由 bilibili-api 二次开发
源仓库: https://github.com/MoyuScript/bilibili-api
"""
import json
import time
from enum import Enum
from typing import List
from ..utils.Credential import Credential
from ..utils.network import request
from ..utils.utils import get_api
API = get_api("user")
class VideoOrder(Enum):
"""
视频排序顺序
+ PUBDATE : 上传日期倒序
+ FAVORITE : 收藏量倒序
+ VIEW : 播放量倒序
"""
PUBDATE = "pubdate"
FAVORITE = "stow"
VIEW = "click"
class AudioOrder(Enum):
"""
音频排序顺序
+ PUBDATE : 上传日期倒序
+ VIEW : 播放量倒序
+ FAVORITE : 收藏量倒序
"""
PUBDATE = 1
VIEW = 2
FAVORITE = 3
class ArticleOrder(Enum):
"""
专栏排序顺序
+ PUBDATE : 发布日期倒序
+ FAVORITE : 收藏量倒序
+ VIEW : 阅读量倒序
"""
PUBDATE = "publish_time"
FAVORITE = "fav"
VIEW = "view"
class ArticleListOrder(Enum):
"""
文集排序顺序
+ LATEST : 最近更新倒序
+ VIEW : 总阅读量倒序
"""
LATEST = 0
VIEW = 1
class BangumiType(Enum):
"""
番剧类型
+ BANGUMI : 番剧
+ DRAMA : 电视剧/纪录片等
"""
BANGUMI = 1
DRAMA = 2
class RelationType(Enum):
"""
用户关系操作类型
+ SUBSCRIBE : 关注
+ UNSUBSCRIBE : 取关
+ SUBSCRIBE_SECRETLY : 悄悄关注
+ BLOCK : 拉黑
+ UNBLOCK : 取消拉黑
+ REMOVE_FANS : 移除粉丝
"""
SUBSCRIBE = 1
UNSUBSCRIBE = 2
SUBSCRIBE_SECRETLY = 3
BLOCK = 5
UNBLOCK = 6
REMOVE_FANS = 7
class User:
"""
用户相关
"""
def __init__(self, uid: int, credential: Credential = None):
"""
Args:
uid: 用户 UID
credential: 凭据。默认None
"""
self.uid = uid
if credential is None:
credential = Credential()
self.credential = credential
self.__self_info = None
async def get_user_info(self):
"""
获取用户信息(昵称,性别,生日,签名,头像 URL空间横幅 URL 等)
"""
api = API["info"]["info"]
params = {
"mid": self.uid
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
async def __get_self_info(self):
"""
获取自己的信息,如果存在缓存则使用缓存
"""
if self.__self_info is not None:
return self.__self_info
self.__self_info = await get_self_info(credential=self.credential)
return self.__self_info
async def get_relation_info(self):
"""
获取用户关系信息(关注数,粉丝数,悄悄关注,黑名单数)
"""
api = API["info"]["relation"]
params = {
"vmid": self.uid
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
async def get_up_stat(self):
"""
获取 UP 主数据信息(视频总播放量,文章总阅读量,总点赞数)
"""
self.credential.raise_for_no_bili_jct()
api = API["info"]["upstat"]
params = {
"mid": self.uid
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
async def get_live_info(self):
"""
获取用户直播间信息
"""
api = API["info"]["live"]
params = {
"mid": self.uid
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
async def get_videos(self,
tid: int = 0,
pn: int = 1,
keyword: str = "",
order: VideoOrder = VideoOrder.PUBDATE
):
"""
获取用户投稿视频信息
Args:
tid: 分区 ID。默认0全部
pn: 页码,从 1 开始。默认1
keyword: 搜索关键词。默认:""
order: 排序方式。默认VideoOrder.PUBDATE
"""
api = API["info"]["video"]
params = {
"mid": self.uid,
"ps": 30,
"tid": tid,
"pn": pn,
"keyword": keyword,
"order": order.value
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
async def get_audios(self, order: AudioOrder = AudioOrder.PUBDATE, pn: int = 1):
"""
获取用户投稿音频
Args:
order: 排序方式。默认AudioOrder.PUBDATE
pn: 页码数,从 1 开始。默认1
"""
api = API["info"]["audio"]
params = {
"uid": self.uid,
"ps": 30,
"pn": pn,
"order": order.value
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
async def get_articles(self, pn: int = 1, order: ArticleOrder = ArticleOrder.PUBDATE):
"""
获取用户投稿专栏
Args:
pn: 页码数,从 1 开始。默认1
order: 排序方式。默认ArticleOrder.PUBDATE
"""
api = API["info"]["article"]
params = {
"mid": self.uid,
"ps": 30,
"pn": pn,
"sort": order.value
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
async def get_article_list(self, order: ArticleListOrder = ArticleListOrder.LATEST):
"""
获取用户专栏文集
Args:
order: 排序方式。默认ArticleListOrder.LATEST
"""
api = API["info"]["article_lists"]
params = {
"mid": self.uid,
"sort": order.value
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
async def get_dynamics(self, offset: int = 0, need_top: bool = False):
"""
获取用户动态
Args:
offset: 该值为第一次调用本方法时,数据中会有个 next_offset 字段,指向下一动态列表第一条动态(类似单向链表)。
根据上一次获取结果中的 next_offset 字段值循环填充该值即可获取到全部动态。0 为从头开始。
默认0
need_top: 显示置顶动态。默认False
"""
api = API["info"]["dynamic"]
params = {
"host_uid": self.uid,
"offset_dynamic_id": offset,
"need_top": 1 if need_top else 0
}
data = await request("GET", url=api["url"], params=params, credential=self.credential)
# card 字段自动转换成 JSON
if 'cards' in data:
for card in data["cards"]:
card["card"] = json.loads(card["card"])
card["extend_json"] = json.loads(card["extend_json"])
return data
async def get_subscribed_bangumis(self, pn: int = 1, type_: BangumiType = BangumiType.BANGUMI):
"""
获取用户追番/追剧列表
Args:
pn: 页码数,从 1 开始。默认1
type_: 资源类型。默认BangumiType.BANGUMI
"""
api = API["info"]["bangumi"]
params = {
"vmid": self.uid,
"pn": pn,
"ps": 15,
"type": type_.value
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
async def get_followings(self, pn: int = 1, desc: bool = True):
"""
获取用户关注列表(不是自己只能访问前 5 页)
Args:
pn: 页码,从 1 开始。默认1
desc: 倒序排序。默认True
"""
api = API["info"]["followings"]
params = {
"vmid": self.uid,
"ps": 20,
"pn": pn,
"order": "desc" if desc else "asc"
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
async def get_followers(self, pn: int = 1, desc: bool = True):
"""
获取用户粉丝列表(不是自己只能访问前 5 页,是自己也不能获取全部的样子)
Args:
pn: 页码,从 1 开始。默认1
desc: 倒序排序。默认True
"""
api = API["info"]["followers"]
params = {
"vmid": self.uid,
"ps": 20,
"pn": pn,
"order": "desc" if desc else "asc"
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
async def get_overview_stat(self):
"""
获取用户的简易订阅和投稿信息
"""
api = API["info"]["overview"]
params = {
"mid": self.uid,
"jsonp": "jsonp"
}
return await request("GET", url=api["url"], params=params, credential=self.credential)
# 操作用户
async def modify_relation(self, relation: RelationType):
"""
修改和用户的关系,比如拉黑、关注、取关等
Args:
relation: 操作类型
"""
self.credential.raise_for_no_sessdata()
self.credential.raise_for_no_bili_jct()
api = API["operate"]["modify"]
data = {
"fid": self.uid,
"act": relation.value,
"re_src": 11
}
return await request("POST", url=api["url"], data=data, credential=self.credential)
async def send_msg(self, text: str):
"""
给用户发送私聊信息。目前仅支持纯文本
Args:
text: 信息内容
"""
self.credential.raise_for_no_sessdata()
self.credential.raise_for_no_bili_jct()
api = API["operate"]["send_msg"]
self_info = await self.__get_self_info()
sender_uid = self_info["mid"]
data = {
"msg[sender_uid]": sender_uid,
"msg[receiver_id]": self.uid,
"msg[receiver_type]": 1,
"msg[msg_type]": 1,
"msg[msg_status]": 0,
"msg[content]": json.dumps({"content": text}),
"msg[dev_id]": "B9A37BF3-AA9D-4076-A4D3-366AC8C4C5DB",
"msg[new_face_version]": "0",
"msg[timestamp]": int(time.time()),
"from_filework": 0,
"build": 0,
"mobi_app": "web"
}
return await request("POST", url=api["url"], data=data, credential=self.credential)
async def get_self_info(credential: Credential):
"""
获取自己的信息
Args:
credential: 凭据
"""
api = API["info"]["my_info"]
credential.raise_for_no_sessdata()
return await request("GET", api["url"], credential=credential)
async def create_subscribe_group(name: str, credential: Credential):
"""
创建用户关注分组
Args:
name: 分组名
credential: 凭据
"""
credential.raise_for_no_sessdata()
credential.raise_for_no_bili_jct()
api = API["operate"]["create_subscribe_group"]
data = {
"tag": name
}
return await request("POST", api["url"], data=data, credential=credential)
async def delete_subscribe_group(group_id: int, credential: Credential):
"""
删除用户关注分组
Args:
group_id: 分组 ID
credential: 凭据
"""
credential.raise_for_no_sessdata()
credential.raise_for_no_bili_jct()
api = API["operate"]["del_subscribe_group"]
data = {
"tagid": group_id
}
return await request("POST", api["url"], data=data, credential=credential)
async def rename_subscribe_group(group_id: int, new_name: str, credential: Credential):
"""
重命名关注分组
Args:
group_id: 分组 ID
new_name: 新的分组名
credential: 凭据
"""
credential.raise_for_no_sessdata()
credential.raise_for_no_bili_jct()
api = API["operate"]["rename_subscribe_group"]
data = {
"tagid": group_id,
"name": new_name
}
return await request("POST", api["url"], data=data, credential=credential)
async def set_subscribe_group(uids: List[int], group_ids: List[int], credential: Credential):
"""
设置用户关注分组
Args:
uids: 要设置的用户 UID 列表,必须已关注
group_ids: 要复制到的分组列表
credential: 凭据
"""
credential.raise_for_no_sessdata()
credential.raise_for_no_bili_jct()
api = API["operate"]["set_user_subscribe_group"]
data = {
"fids": ",".join(map(lambda x: str(x), uids)),
"tagids": ",".join(map(lambda x: str(x), group_ids))
}
return await request("POST", api["url"], data=data, credential=credential)
async def get_self_history(credential: Credential, page_num: int = 1, per_page_item: int = 100):
"""
获取用户浏览历史记录
Args:
credential: 凭据
page_num: 页码数。默认1
per_page_item: 每页多少条历史记录。默认100
"""
if not credential:
credential = Credential()
credential.raise_for_no_sessdata()
api = API["info"]["history"]
params = {
"pn": page_num,
"ps": per_page_item
}
return await request("GET", url=api["url"], params=params, credential=credential)

View File

@@ -0,0 +1,16 @@
"""
API 异常基类
"""
class ApiException(Exception):
"""
API 异常基类
"""
def __init__(self, msg: str = "出现了错误, 但是未说明具体原因"):
super().__init__(msg)
self.msg = msg
def __str__(self):
return self.msg

View File

@@ -0,0 +1,15 @@
"""
Credential 类未提供 bili_jct 时的异常
"""
from .ApiException import ApiException
class CredentialNoBiliJctException(ApiException):
"""
Credential 类未提供 bili_jct 时的异常
"""
def __init__(self):
super().__init__()
self.msg = "Credential 类未提供 bili_jct"

View File

@@ -0,0 +1,15 @@
"""
Credential 类未提供 BUVID3 时的异常
"""
from .ApiException import ApiException
class CredentialNoBuvid3Exception(ApiException):
"""
Credential 类未提供 BUVID3 时的异常
"""
def __init__(self):
super().__init__()
self.msg = "Credential 类未提供 BUVID3"

View File

@@ -0,0 +1,15 @@
"""
Credential 类未提供 SESSDATA 时的异常
"""
from .ApiException import ApiException
class CredentialNoSessdataException(ApiException):
"""
Credential 类未提供 SESSDATA 时的异常
"""
def __init__(self):
super().__init__()
self.msg = "Credential 类未提供 SESSDATA"

View File

@@ -0,0 +1,15 @@
"""
连接直播间期间发生的异常
"""
from .ApiException import ApiException
class LiveException(ApiException):
"""
连接直播间期间发生的异常
"""
def __init__(self, msg: str):
super().__init__()
self.msg = msg

View File

@@ -0,0 +1,24 @@
"""
网络错误
"""
from .ApiException import ApiException
class NetworkException(ApiException):
"""
网络错误
"""
def __init__(self, status: int, msg: str):
"""
Args:
status: 状态码
msg: 状态消息
"""
super().__init__(msg)
self.status = status
self.msg = f"网络错误, 状态码: {status} - {msg}"
def __str__(self):
return self.msg

View File

@@ -0,0 +1,26 @@
"""
API 返回 code 错误
"""
from .ApiException import ApiException
class ResponseCodeException(ApiException):
"""
API 返回 code 错误
"""
def __init__(self, code: int, msg: str, raw: dict = None):
"""
Args:
code: 错误代码
msg: 错误信息
raw: 原始响应数据。默认None
"""
super().__init__(msg)
self.msg = msg
self.code = code
self.raw = raw
def __str__(self):
return f"接口返回错误代码: {self.code}, 信息: {self.msg}"

View File

@@ -0,0 +1,19 @@
"""
API 响应异常
"""
from .ApiException import ApiException
class ResponseException(ApiException):
"""
API 响应异常
"""
def __init__(self, msg: str):
"""
Args:
msg: 错误消息
"""
super().__init__(msg)
self.msg = msg

View File

@@ -0,0 +1,8 @@
from .ApiException import *
from .CredentialNoBiliJctException import *
from .CredentialNoBuvid3Exception import *
from .CredentialNoSessdataException import *
from .LiveException import *
from .NetworkException import *
from .ResponseCodeException import *
from .ResponseException import *

View File

@@ -0,0 +1,81 @@
"""
发布-订阅模式异步事件类支持
"""
import asyncio
from typing import Any, Coroutine
class AsyncEvent:
"""
发布-订阅模式异步事件类支持
特殊事件__ALL__ 所有事件均触发
"""
def __init__(self):
self.__handlers = {}
def add_event_listener(self, name: str, handler: Coroutine):
"""
注册事件监听器
Args:
name: 事件名
handler: 回调异步函数
"""
name = name.upper()
if name not in self.__handlers:
self.__handlers[name] = []
self.__handlers[name].append(handler)
def on(self, event_name: str):
"""
装饰器注册事件监听器
Args:
event_name: 事件名
"""
def decorator(func: Coroutine):
self.add_event_listener(event_name, func)
return func
return decorator
def remove_event_listener(self, name: str, handler: Coroutine) -> bool:
"""
移除事件监听函数
Args:
name: 事件名
handler: 要移除的函数
Returns:
是否移除成功
"""
name = name.upper()
if name in self.__handlers:
if handler in self.__handlers[name]:
self.__handlers[name].remove(handler)
return True
return False
def dispatch(self, name: str, data: Any = None):
"""
异步发布事件
Args:
name: 事件名
data: 事件附加数据。默认None
"""
name = name.upper()
if name in self.__handlers:
for coroutine in self.__handlers[name]:
asyncio.create_task(coroutine(data))
if name != '__ALL__':
self.dispatch('__ALL__', {
"name": name,
"data": data
})

View File

@@ -0,0 +1,80 @@
"""
凭据类,用于各种请求操作的验证
"""
from typing import Dict
from ..exception import CredentialNoBiliJctException, CredentialNoBuvid3Exception, CredentialNoSessdataException
class Credential:
"""
凭据类,用于各种请求操作的验证
"""
def __init__(self, sessdata: str = None, bili_jct: str = None, buvid3: str = None):
"""
Args:
sessdata: 浏览器 Cookies 中的 SESSDATA 字段值。默认None
bili_jct: 浏览器 Cookies 中的 bili_jct 字段值。默认None
buvid3: 浏览器 Cookies 中的 BUVID3 字段值。默认None
"""
self.sessdata = sessdata
self.bili_jct = bili_jct
self.buvid3 = buvid3
def get_cookies(self) -> Dict:
"""
获取请求 Cookies 字典
Returns:
请求 Cookies 字典
"""
return {"SESSDATA": self.sessdata, "buvid3": self.buvid3, 'bili_jct': self.bili_jct}
def has_sessdata(self) -> bool:
"""
是否提供了 SESSDATA
Returns:
是否提供了 SESSDATA
"""
return self.sessdata is not None
def has_bili_jct(self) -> bool:
"""
是否提供了 bili_jct
Returns:
是否提供了 bili_jct
"""
return self.bili_jct is not None
def has_buvid3(self) -> bool:
"""
是否提供了 BUVID3
Returns:
是否提供了 BUVID3
"""
return self.buvid3 is not None
def raise_for_no_sessdata(self):
"""
没有提供 SESSDATA 时抛出异常
"""
if not self.has_sessdata():
raise CredentialNoSessdataException()
def raise_for_no_bili_jct(self):
"""
没有提供 bili_jct 时抛出异常
"""
if not self.has_bili_jct():
raise CredentialNoBiliJctException()
def raise_for_no_buvid3(self):
"""
没有提供 BUVID3 时抛出异常
"""
if not self.has_buvid3():
raise CredentialNoBuvid3Exception()

91
starbot/utils/Danmaku.py Normal file
View File

@@ -0,0 +1,91 @@
"""
弹幕类
"""
import time
from enum import Enum
class FontSize(Enum):
"""
字体大小枚举
"""
EXTREME_SMALL = 12
SUPER_SMALL = 16
SMALL = 18
NORMAL = 25
BIG = 36
SUPER_BIG = 45
EXTREME_BIG = 64
class Mode(Enum):
"""
弹幕模式枚举
"""
FLY = 1
TOP = 5
BOTTOM = 4
REVERSE = 6
class Danmaku:
"""
弹幕类
"""
def __init__(self,
text: str,
dm_time: float = 0.0,
send_time: float = time.time(),
crc32_id: str = None,
color: str = 'ffffff',
weight: int = -1,
id_: int = -1,
id_str: str = "",
action: str = "",
mode: Mode = Mode.FLY,
font_size: FontSize = FontSize.NORMAL,
is_sub: bool = False,
pool: int = -1,
attr: int = -1):
"""
Args:
text: 弹幕文本
dm_time: 弹幕在视频中的位置单位为秒。默认0.0
send_time: 弹幕发送的时间。默认time.time()
crc32_id: 弹幕发送者 UID 经 CRC32 算法取摘要后的值。默认None
color: 弹幕十六进制颜色。默认:"ffffff"
weight: 弹幕在弹幕列表显示的权重。默认:-1
id_: 弹幕 ID。默认-1
id_str: 弹幕字符串 ID。默认""
action: 暂不清楚。默认:""
mode: 弹幕模式。默认Mode.FLY
font_size: 弹幕字体大小。默认FontSize.NORMAL
is_sub: 是否为字幕弹幕。默认False
pool: 暂不清楚。默认:-1
attr: 暂不清楚。默认:-1
"""
self.text = text
self.dm_time = dm_time
self.send_time = send_time
self.crc32_id = crc32_id
self.color = color
self.weight = weight
self.id = id_
self.id_str = id_str
self.action = action
self.mode = mode
self.font_size = font_size
self.is_sub = is_sub
self.pool = pool
self.attr = attr
self.uid = None
def __str__(self):
ret = "%s, %s, %s" % (self.send_time, self.dm_time, self.text)
return ret
def __len__(self):
return len(self.text)

View File

189
starbot/utils/network.py Normal file
View File

@@ -0,0 +1,189 @@
"""
与网络请求相关的模块。能对会话进行管理(复用 TCP 连接)
"""
import asyncio
import atexit
import json
import re
from typing import Any, Union, Dict
import aiohttp
from aiohttp import TCPConnector
from ..exception import ResponseCodeException, ResponseException, NetworkException
from .Credential import Credential
__session_pool = {}
@atexit.register
def __clean():
"""
程序退出清理操作
"""
try:
loop = asyncio.get_running_loop()
except RuntimeError:
return
async def __clean_task():
await __session_pool[loop].close()
if loop.is_closed():
loop.run_until_complete(__clean_task())
else:
loop.create_task(__clean_task())
async def request(method: str,
url: str,
params: dict = None,
data: Any = None,
credential: Credential = None,
no_csrf: bool = False,
json_body: bool = False,
**kwargs) -> Union[Dict, None]:
"""
向接口发送请求
Args:
method: 请求方法
url: 请求 URL
params: 请求参数。默认None
data: 请求载荷。默认None
credential: Credential 实例。默认None
no_csrf: 不要自动添加 CSRF。默认False
json_body: 载荷是否为 JSON。默认False
kwargs: 暂不使用
Returns:
接口未返回数据时,返回 None否则返回该接口提供的 data 或 result 字段的数据
"""
if credential is None:
credential = Credential()
method = method.upper()
# 请求为非 GET 且 no_csrf 不为 True 时要求 bili_jct
if method != 'GET' and not no_csrf:
credential.raise_for_no_bili_jct()
# 使用 Referer 和 UA 请求头以绕过反爬虫机制
default_headers = {
"Referer": "https://www.bilibili.com",
"User-Agent": "Mozilla/5.0"
}
headers = default_headers
if params is None:
params = {}
# 自动添加 csrf
if not no_csrf and method in ['POST', 'DELETE', 'PATCH']:
if data is None:
data = {}
data['csrf'] = credential.bili_jct
data['csrf_token'] = credential.bili_jct
# jsonp
if params.get("jsonp", "") == "jsonp":
params["callback"] = "callback"
config = {
"method": method,
"url": url,
"params": params,
"data": data,
"headers": headers,
"cookies": credential.get_cookies()
}
config.update(kwargs)
if json_body:
config["headers"]["Content-Type"] = "application/json"
config["data"] = json.dumps(config["data"])
# 如果用户提供代理则设置代理
proxy = config.get("PROXY")
if proxy:
config["proxy"] = proxy
session = get_session()
async with session.request(**config) as resp:
# 检查状态码
try:
resp.raise_for_status()
except aiohttp.ClientResponseError as e:
raise NetworkException(e.status, e.message)
# 检查响应头 Content-Length
content_length = resp.headers.get("content-length")
if content_length and int(content_length) == 0:
return None
# 检查响应头 Content-Type
content_type = resp.headers.get("content-type")
# 不是 application/json
if content_type.lower().index("application/json") == -1:
raise ResponseException("响应不是 application/json 类型")
raw_data = await resp.text()
resp_data: dict
if 'callback' in params:
# JSONP 请求
resp_data = json.loads(
re.match("^.*?({.*}).*$", raw_data, re.S).group(1))
else:
# JSON
resp_data = json.loads(raw_data)
# 检查 code
code = resp_data.get("code", None)
if code is None:
raise ResponseCodeException(-1, "API 返回数据未含 code 字段", resp_data)
if code != 0:
msg = resp_data.get('msg', None)
if msg is None:
msg = resp_data.get('message', None)
if msg is None:
msg = "接口未返回错误信息"
raise ResponseCodeException(code, msg, resp_data)
real_data = resp_data.get("data", None)
if real_data is None:
real_data = resp_data.get("result", None)
return real_data
def get_session() -> aiohttp.ClientSession:
"""
获取当前模块的 aiohttp.ClientSession 对象,用于自定义请求
Returns:
ClientSession 实例
"""
loop = asyncio.get_running_loop()
session = __session_pool.get(loop, None)
if session is None:
session = aiohttp.ClientSession(loop=loop, connector=TCPConnector(loop=loop, limit=0))
__session_pool[loop] = session
return session
def set_session(session: aiohttp.ClientSession):
"""
用户手动设置 Session
Args:
session: aiohttp.ClientSession 实例
"""
loop = asyncio.get_running_loop()
__session_pool[loop] = session

23
starbot/utils/utils.py Normal file
View File

@@ -0,0 +1,23 @@
"""
通用工具库
"""
import json
import os
from typing import Dict
def get_api(field: str) -> Dict:
"""
获取 API
Args:
field: API 所属分类,即 data/api 下的文件名(不含后缀名)
Returns:
该 API 的内容
"""
path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "api", f"{field.lower()}.json"))
if os.path.exists(path):
with open(path, encoding="utf8") as f:
return json.loads(f.read())