├── config.json.example
├── README.md
├── spider.json
├── LICENSE
└── spider.py
/config.json.example:
--------------------------------------------------------------------------------
1 | {
2 | "uin": 机器人qq号,
3 | "password": "机器人qq密码",
4 | "encrypt_password": false,
5 | "password_encrypted": "",
6 | "enable_db": false,
7 | "access_token": "",
8 | "relogin": {
9 | "enabled": true,
10 | "relogin_delay": 3,
11 | "max_relogin_times": 5
12 | },
13 | "_rate_limit": {
14 | "enabled": false,
15 | "frequency": 1,
16 | "bucket_size": 1
17 | },
18 | "ignore_invalid_cqcode": false,
19 | "force_fragmented": false,
20 | "heartbeat_interval": 0,
21 | "http_config": {
22 | "enabled": true,
23 | "host": "127.0.0.1",
24 | "port": 5700,
25 | "timeout": 0,
26 | "post_urls": {}
27 | },
28 | "ws_config": {
29 | "enabled": false,
30 | "host": "127.0.0.1",
31 | "port": 8080
32 | },
33 | "ws_reverse_servers": [
34 | {
35 | "enabled": true,
36 | "reverse_url": "ws://127.0.0.1:8080/ws/",
37 | "reverse_api_url": "ws://127.0.0.1:8080/api/",
38 | "reverse_event_url": "ws://127.0.0.1:8080/event/",
39 | "reverse_reconnect_interval": 3000
40 | }
41 | ],
42 | "post_message_format": "string",
43 | "debug": false,
44 | "log_level": "",
45 | "web_ui": {
46 | "enabled": false,
47 | "host": "0.0.0.0",
48 | "web_ui_port": 9999,
49 | "web_input": false
50 | }
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 功能介绍
2 | * spider.py、spider.json为检测脚本及相应设置文件。支持youtube频道 直播留言 社区帖子 推送、twitter用户信息 用户推特 推特搜索、twitcast频道 直播留言、fanbox用户信息 帖子的检测、bilibili频道 直播留言、lol steam osu用户数据,支持推送到qq用户、qq群、喵提醒、discord、telegram。
3 | * release中发布的exe版本可以在windows中直接运行,无需依赖、点开即用。
4 |
5 | 感谢[太古oo](https://www.bilibili.com/read/cv4603796)提供的灵感和检测方法,感谢[24h-raspberry-live-on-bilibili](https://github.com/chenxuuu/24h-raspberry-live-on-bilibili/tree/master)与[blivedm](https://github.com/xfgryujk/blivedm)的b站弹幕接口。
6 |
7 |
8 | # 环境依赖
9 | ## 检测脚本本体
10 | ##### 安装方法
11 | 在命令行中运行`pip3 install requests; pip3 install bs4; pip3 install lxml; pip3 install websocket-client`安装脚本依赖的python库,将spider.py和spider.json文件下载到相同的目录(注意至少还需要在配置文件中设置cookies和要推送的qq账户才能正常运行)。
12 | ##### 启动方法
13 | 在命令行中运行`python3 spider.py`,按照提示选择配置文件。
14 |
15 | ## 推送方式
16 | ##### qq推送
17 | 基于[go-cqhttp](https://github.com/Mrs4s/go-cqhttp)
18 | 在命令行中运行`wget "https://github.com/Mrs4s/go-cqhttp/releases/download/v1.0.0-beta1/go-cqhttp_linux_386.tar.gz" ; tar -xvf go-cqhttp_linux_386.tar.gz ; chmod +x go-cqhttp ; ./go-cqhttp`运行mirai机器人并按照提示登录。初次运行后关闭mirai机器人,修改config.json中的内容为config.json设置示例中的内容,注意修改其中'用作机器人的QQ号'为机器人的qq号。之后再次运行`./go-cqhttp`即可。
19 |
20 | ##### 喵提醒
21 | 在喵提醒微信公众号中选择'提醒'-'添加提醒',完成设置后将会收到一个喵提醒号。更加详细的说明可以在其微信公众号中查看。
22 |
23 | ##### discord推送
24 | 在discord频道右键-编辑频道-webhook-创建webhook(需要编辑webhook权限),完成设置后将会产生一个webhook链接。
25 |
26 | ##### telegram推送
27 | 在telegram中搜索botfather-发送/newbot-输入bot用户名-输入bot id,完成设置后将会产生一个bot token。用户群聊或频道号为邀请链接中的t.me/后面的部分,如果需要在群聊或频道中发言需要先邀请bot进群。
28 |
29 |
30 | # 脚本详解
31 | ### 脚本运行原理
32 | * 脚本将会按照设置文件中的设置来启动许多子监视器线程以完成不同的监视任务,你可以为每一个监视器线程指定不同的运行参数,也可以让一些监视器线程共用一些运行参数的同时为每个监视器添加各自特有的运行参数。
33 | * 每个子监视器线程都会定时检测指定的内容,如youtube视频列表、twitter用户信息、twitcast直播状态等,并将更新的信息与指定的关键词进行匹配,对符合条件的信息向用户进行推送。
34 | * 关键词匹配基本由设置文件中的"vip_dic"和"word_dic"指定,"vip_dic"中的关键词用于匹配检测的频道、发送的用户、提及的用户之类的信息,"word_dic"中的关键词用于匹配标题、简介、消息内容之类的信息;关键词有对应的"权重类型"和相应的"权重值"。当匹配到相应的关键词时,关键词对应的"权重类型"的"权重值"将会被分别记录并累加。例如当检测的用户发送了一条消息`小红和小蓝在玩`时,如果`"word_dic": {'小红': {'red': 1, 'small': 1}, '小蓝': {'blue': 1, 'small': 2}}`,则这条消息的"权重"将会为`{'red': 1, 'blue': 1, 'small': 3}`。
35 | * 用户推送由设置文件中的"push_dic"指定,其中除了如推送类型和qq号等基本信息之外,还有指定"接收权重"的参数"color_dic",当一条消息的"推送权重"中有任何一个"权重类型"的"权重值"大于或等于"接收权重"中的指定值时这条消息就会向该用户推送。对于上述例子中的消息,如果用户的`"color_dic": {'green': 1, 'small': 2}`,那么因为"small"这种色彩的值大于用户接收权重的值,所以这条消息将会向该用户推送。
36 |
37 |
38 | ### 配置文件详解
39 | ```
40 | {
41 | "submonitor_dic": { # 子监视器列表
42 | "YoutubeLive 神楽めあ": {"class": "YoutubeLive", "target": "UCWCc8tO-uUl_7SJXIKJACMw", "target_name": "神楽めあ", "config_name": "youtube_config"},
43 | "TwitterUser 神楽めあ": {"class": "TwitterUser", "target": "KaguraMea_VoV", "target_name": "神楽めあ", "config_name": "twitter_config"}
44 | # "子监视器名称 用于特异的标记子监视器 不能重复": {"class": "子监视器类名 用于启动不同类型的子监视器来完成不同的监视任务 与脚本中的类名相同", "target": "要检测的频道编号 注意大小写敏感", "target_name": "检测的频道名称 将会被添加到推送消息中表明消息来源", "config_name": "要使用的配置名称"}
45 | },
46 |
47 | "youtube_config": { # 配置名称 即上面"config_name"后指定的名称
48 | "interval": 180, # 检测循环间隔
49 | "timezone": 8, # 推送时转换为指定的时区 如北京时间即东八区则为8
50 | "vip_dic": { # 用于匹配"target"中指定的频道或者直播留言的发送者 注意大小写敏感
51 | "UCWCc8tO-uUl_7SJXIKJACMw": {"mea": 10},
52 | "UCu5eCcfs67GkeCFbhsMrEjA": {"mea": 10},
53 | "UCZU5rKvh3aAFs1PyyfeLWcg": {"mea": 10}
54 | },
55 | "word_dic": { # 用于匹配直播标题和简介或者直播留言的的内容
56 | "神楽めあ": {"mea": 4},
57 | "めあちゃん": {"mea": 4},
58 | "めあだ": {"mea": 4},
59 | "神楽": {"mea": 2},
60 | "めあ": {"mea": 2},
61 | "かぐら": {"mea": 2},
62 | "メア": {"mea": 2}
63 | },
64 | "cookies": {}, # 检测所用的cookies,可以在浏览器中打开youtube页面时按下f12,在"网络"中寻找POST类型的请求并复制其cookies即可,注意可能需要删除开头的"{请求cookies"和结尾的多余的"}",不指定可以留空或删除此项
65 | "proxy": {"http": "socks5://127.0.0.1:1080","https": "socks5://127.0.0.1:1080"}, # 指定代理,如果使用非sock5代理可设置为{"http": "127.0.0.1:1080", "https": "127.0.0.1:1080"},不使用代理可以留空或删除此项
66 | "push_list": [ # 指定推送对象
67 | {"type": "qq_user", "id": "qq号", "port": 5700, "color_dic": {"mea": 1}}, # qq_user与qq_group可以通过"ip"键来指定推送到的ip地址,默认为127.0.0.1。修改此设置需要同时修改qq机器人的监听ip,如果指定为本地以外的ip地址可能会导致安全性问题。
68 | {"type": "qq_group", "id": "qq群号", "port": 5700, "color_dic": {"mea": 1}},
69 | {"type": "miaotixing", "id": "喵提醒号", "color_dic": {"mea": 1}},
70 | {"type": "miaotixing_simple", "id": "喵提醒号", "color_dic": {"mea": 1}}, #不推送文字,防止语音或者短信推送失效
71 | {"type": "discord", "id": "discord webhook链接", "color_dic": {"mea": 1}, "proxy": {"http": "socks5://127.0.0.1:1080","https": "socks5://127.0.0.1:1080"}}, #推送对象也可以指定代理
72 | {"type": "telegram", "id": "telegram @用户群聊频道名 或 -聊天id号", "bot_id": "telegram bot token", "color_dic": {"mea": 1}}
73 | ]
74 | }
75 | }
76 | ```
77 |
78 | * 监视器运行所需的参数由"submonitor_dic"中的项和其中"config_name"指向的配置组成,其中"class"、"target"、"target_name"、"config_name"四个项目作为必选参数需要在"submonitor_dic"中指定,其他参数既可以添加在"submonitor_dic"中也可以添加在"config_name"指向的配置中;注意当有同名参数同时存在于两个位置时,"submonitor_dic"中的参数将会生效。例如spider.json中就在部分"submonitor_dic"监视器信息中额外指定了"interval"的值,以便让这些监视有更短的检测间隔。
79 | * 基于Monitor类的监视器可以启动自己的子监视器,只要指定的配置中还有"submonitor_dic"项并且添加了相应的子监视器信息的话。例如spider.json中就先启动了基于Monitor类的"Youtube"、"Twitter"、"Fanbox"等几个监视器,这几个监视器又启动了各自配置中"submonitor_dic"项中指定的子监视器。
80 | * YoutubeLive、TwitcastLive和BilibiliLive监视器可以在一定情况下启动自己的YoutubeChat、TwitcastChat和BilibiliChat子监视器,这些子监视器将会继承父监视器的config_name所指向的配置,但不会继承父监视器submonitor_dic中额外指定的参数(即submonitor_dic中指定的参数不被继承,而config_name中指定的参数将被继承),如果想让Live监视器和Chat监视器有不同的设置则可以分别在submonitor_dic和config_name所指向的配置中设定两者的参数。
81 | * YoutubeChat、TwitcastChat和BilibiliChat子监视器会对直播评论发送者和评论内容进行关键词匹配(用于监视本人出现在其他人的直播间或者其他直播提到特定内容的情况)。为了防止vip本人的直播中的评论触发推送,如果子监视器的"target"项和"vip_dic"中关键词匹配的话,"推送权重"将会减去"vip_dic"中相应"关键词的权重"。另外为了防止某场直播中的出现大量评论频繁触发推送,每次推送时如果"推送权重"中如果有大于0的项,那么后续推送中这种权重类型将会被增加1的推送惩罚;当权重类型名字中含有"vip"字样时,这种权重类型不会受到推送惩罚。
82 |
83 | ### 子监视器详解
84 | __子监视器类名__|作用|__通用必选参数__|vip_dic匹配内容|word_dic匹配内容|cookies作用|__特有可选参数__|说明
85 | :---|:---|:---|:---|:---|:---|:---|:---
86 | Monitor|作为基本监视器管理子监视器组|interval|||||
87 | YoutubeLive|监视youtube直播和视频|interval、timezone、vip_dic、word_dic、cookies、proxy、push_list|target|标题、简介|可留空|"standby_chat","standby_chat_onstart","no_chat","status_push","regen","regen_amount"|standby_chat为是否检测待机直播间的弹幕 默认为"False" 可选"True",standby_chat_onstart是否检测在第一次检测时已开启的待机直播间的弹幕 默认为"False" 可选"True",no_chat为是否不记录弹幕 默认为"False" 可选"True",status_push为推送相应类型的更新 默认为"等待\|开始\|结束\|上传\|删除",regen为推送惩罚恢复间隔 默认为"False" 可选"间隔秒数",regen_amount为每次推送惩罚恢复量 默认为"1" 可选"恢复数量"
88 | YoutubeChat|监视youtube直播评论|同上|父监视器target(取负)、直播评论发送频道|直播评论文字|||通常由YoutubeLive监视器创建 无需在配置文件中指定
89 | YoutubeCom|监视youtube社区帖子|同上|target|帖子文字|付费帖子,可留空|||
90 | YoutubeNote|监视cookies对应用户的通知|同上||通知文字内容(包括superchat)|用户通知,必要|||
91 | TwitterUser|监视twitter用户基本信息|同上|target||必要|"no_increase","no_repeat"|no_increase为是否不推送推文和媒体数量的增加 默认为"False" 可选"True",no_repeat为是否不推送短时间内重复的推文和媒体数量 默认为"False" 可选"间隔秒数"
92 | TwitterTweet|监视twitter用户的推文|同上|target、推文@对象|推文文字(包括#、@和链接)|必要|||
93 | TwitterSearch|监视推特搜索结果|同上|target、推文@对象|推文文字(包括#、@和链接)|必要|"only_live", "only_liveorvideo"|only_live为是否只推送有链接指向正在进行的youtube直播的推文 默认为"False" 可选"True",only_liveorvideo为是否只推送有链接指向youtube直播或视频的推文 默认为"False" 可选"True",当两者同时开启时则only_liveorvideo生效
94 | TwitcastLive|监视twitcast直播|同上|target|标题|可留空|"no_chat","status_push","regen","regen_amount"|no_chat为是否不记录弹幕 默认为"False" 可选"True",status_push为推送相应类型的更新 默认为"开始\|结束",regen为推送惩罚恢复间隔 默认为"False" 可选"间隔秒数",regen_amount为每次推送惩罚恢复量 默认为"1" 可选"恢复数量"
95 | TwitcastChat|监视twitcast直播评论|同上|父监视器target(取负)、直播评论发送频道|直播评论文字|||通常由TwitcastLive监视器创建 无需在配置文件中指定
96 | FanboxUser|监视fanbox用户基本信息|同上|target||可留空|||
97 | FanboxPost|监视fanbox用户帖子|同上|target|帖子文字|付费帖子,可留空|||
98 | BilibiliLive|监视bilibili直播|同上|target|标题|可留空|"offline_chat","simple_mode","no_chat","status_push","regen","regen_amount"|offline_chat为是否监测离线直播间的弹幕 默认为"False" 可选"True",simple_mode为只推送弹幕文字 如果为数字则会将相应数量的弹幕整合推送 默认为"False" 可选"合并数量",no_chat为是否不记录弹幕 默认为"False" 可选"True",status_push为推送相应类型的更新 默认为"开始\|结束",regen为推送惩罚恢复间隔 默认为"False" 可选"间隔秒数",regen_amount为每次推送惩罚恢复量 默认为"1" 可选"恢复数量"
99 | BilibiliChat|监视bilibili直播评论|同上|父监视器target(取负)、直播评论发送频道|直播评论文字|||通常由BilibiliLive监视器创建 无需在配置文件中指定,只能使用http代理 格式应为"proxy": {"http": "ip地址:端口号"}
100 | LolUser|监视lol比赛状况与最近比赛结果|同上|target||可留空|"user_region","ingame_onstart"|user_region为账号所在的地区 即[jp.op.gg](https://jp.op.gg/summoner/l=en_US&userName=%E3%81%8B%E3%81%90%E3%82%89%E3%82%81%E3%81%82%E3%81%A3)网站开头部分 默认为"jp",ingame_onstart为是否在初次启动时就在游戏中的情况下进行推送 默认为"True" 可选"False",由于op.gg最短更新间隔限制为120秒 所以将检测间隔设置为小于120秒意义不大
101 | SteamUser|监视steam在线状况与基本信息|同上|target||查看自己或好友可见的内容,可留空|"online_onstart"|online_onstart为是否在初次启动时就在线的情况下进行推送 默认为"True" 可选"False"
102 | OsuUser|监视osu在线状况与基本信息|同上|target||查看自己或好友可见的内容,可留空|"online_onstart"|online_onstart为是否在初次启动时就在线的情况下进行推送 默认为"True" 可选"False"
103 |
104 | ### 常见故障
105 | ##### 运行闪退
106 | 请确保使用的配置文件(默认为spider.json)为标准的json格式,py脚本与json配置文件为utf-8编码。
107 | ##### 运行后出现很多error信息
108 | 如果同时存在youtube和twitter监视器的error信息,可能是网络原因导致的,由于在脚本刚开始运行时会产生比较多的请求,可能会导致一些请求超时。可以尝试等待一段时间或者重启脚本。
109 | 如果只有twitter监视器的error信息,可能是twitter配置下的cookies不正确导致的,请确保cookies也为json格式,下面是一个cookies示例。
110 | ```
111 | "cookies": {"_ga":"12345678","_gid":"12345678","_twitter_sess":"12345678","ads_prefs":""12345678"","auth_token":"12345678","csrf_same_site":"12345678","csrf_same_site_set":"12345678","ct0":"12345678","dnt":"12345678","gt":"12345678","guest_id":"12345678","kdt":"12345678","lang":"12345678","personalization_id":""12345678"","remember_checked_on":"12345678","rweb_optin":"12345678","twid":"u=12345678"},
112 | ```
113 | ##### 更新后无法推送
114 | 检查配置文件中的push_list项是否已调整为新的格式
115 |
116 | # 想做的事
117 | * 添加bilibili视频与动态监视器
118 | * 更换youtube视频信息接口
119 | * 更换twitcast评论接口
120 | * 对视频标题与简介中出现的关键字也减去相应权重
121 | * 添加apex、amazon等监视器
122 |
--------------------------------------------------------------------------------
/spider.json:
--------------------------------------------------------------------------------
1 | {
2 | "submonitor_dic": {
3 | "Youtube": {"class": "Monitor", "target": "youtube", "target_name": "youtube", "config_name": "youtube"},
4 | "Twitter": {"class": "Monitor", "target": "twitter", "target_name": "twitter", "config_name": "twitter"},
5 | "Fanbox": {"class": "Monitor", "target": "fanbox", "target_name": "fanbox", "config_name": "fanbox"},
6 | "Bilibili": {"class": "Monitor", "target": "bilibili", "target_name": "bilibili", "config_name": "bilibili"},
7 | "Other": {"class": "Monitor", "target": "other", "target_name": "other", "config_name": "other"}
8 | },
9 | "youtube": {
10 | "submonitor_dic": {
11 | "YoutubeLive 神楽めあ": {"class": "YoutubeLive", "target": "UCWCc8tO-uUl_7SJXIKJACMw", "target_name": "神楽めあ", "config_name": "youtube_config", "interval": 180, "standby_chat": "True"},
12 | "YoutubeCom 神楽めあ": {"class": "YoutubeCom", "target": "UCWCc8tO-uUl_7SJXIKJACMw", "target_name": "神楽めあ", "config_name": "youtube_config", "interval": 180},
13 | "YoutubeLive 神楽めあ子频道": {"class": "YoutubeLive", "target": "UCu5eCcfs67GkeCFbhsMrEjA", "target_name": "神楽めあ子频道", "config_name": "youtube_config"},
14 | "YoutubeCom 神楽めあ子频道": {"class": "YoutubeCom", "target": "UCu5eCcfs67GkeCFbhsMrEjA", "target_name": "神楽めあ子频道", "config_name": "youtube_config"},
15 | "YoutubeLive 愛繋璃": {"class": "YoutubeLive", "target": "UCZU5rKvh3aAFs1PyyfeLWcg", "target_name": "愛繋璃", "config_name": "youtube_config"},
16 | "YoutubeCom 愛繋璃": {"class": "YoutubeCom", "target": "UCZU5rKvh3aAFs1PyyfeLWcg", "target_name": "愛繋璃", "config_name": "youtube_config"},
17 | "YoutubeLive 湊あくあ": {"class": "YoutubeLive", "target": "UC1opHUrw8rvnsadT-iGp7Cg", "target_name": "湊あくあ", "config_name": "youtube_config"},
18 | "YoutubeLive 如月こより": {"class": "YoutubeLive", "target": "UCWmJy4zKFf9Y_UQdS5sWuUg", "target_name": "如月こより", "config_name": "youtube_config"},
19 | "YoutubeLive 八乙女のえ": {"class": "YoutubeLive", "target": "UCnzGlm7IC7Mkm00AccSEBEw", "target_name": "八乙女のえ", "config_name": "youtube_config"},
20 | "YoutubeLive 森永みう": {"class": "YoutubeLive", "target": "UChN7P9OhRltW3w9IesC92PA", "target_name": "森永みう", "config_name": "youtube_config"},
21 | "YoutubeLive 犬山たまき": {"class": "YoutubeLive", "target": "UC8NZiqKx6fsDT3AVcMiVFyA", "target_name": "犬山たまき", "config_name": "youtube_config"},
22 | "YoutubeLive 日ノ隈らん": {"class": "YoutubeLive", "target": "UCRvpMpzAXBRKJQuk-8-Sdvg", "target_name": "日ノ隈らん", "config_name": "youtube_config"},
23 | "YoutubeLive 因幡はねる": {"class": "YoutubeLive", "target": "UC0Owc36U9lOyi9Gx9Ic-4qg", "target_name": "因幡はねる", "config_name": "youtube_config"},
24 | "YoutubeLive 物述有栖": {"class": "YoutubeLive", "target": "UCt0clH12Xk1-Ej5PXKGfdPA", "target_name": "物述有栖", "config_name": "youtube_config"},
25 | "YoutubeLive 宇志海いちご": {"class": "YoutubeLive", "target": "UCmUjjW5zF1MMOhYUwwwQv9Q", "target_name": "宇志海いちご", "config_name": "youtube_config"},
26 | "YoutubeLive けんき": {"class": "YoutubeLive", "target": "UCNicQVuAQJYlCQKEw1TKn1A", "target_name": "けんき", "config_name": "youtube_config"}
27 | },
28 | "youtube_config": {
29 | "interval": 300,
30 | "timezone": 8,
31 | "regen": 900,
32 | "regen_amount": 4,
33 | "vip_dic": {
34 | "UCWCc8tO-uUl_7SJXIKJACMw": {"mea": 10, "mea_vip": 10},
35 | "UCu5eCcfs67GkeCFbhsMrEjA": {"mea": 10, "mea_vip": 10},
36 | "UCZU5rKvh3aAFs1PyyfeLWcg": {"maturin": 10, "maturin_vip": 10}
37 | },
38 | "word_dic": {
39 | "神楽": {"mea": 2},
40 | "かぐら": {"mea": 2},
41 | "めあ": {"mea": 2},
42 | "メア": {"mea": 2},
43 | "kagura": {"mea": 2},
44 | "mea": {"mea": 2},
45 | "めあちゃん": {"mea": 2},
46 | "めあだ": {"mea": 2},
47 | "めあり": {"mea": -2},
48 | "メアリ": {"mea": -2},
49 | "mean": {"mea": -2},
50 | "meas": {"mea": -2}
51 | },
52 | "cookies": {},
53 | "proxy": {},
54 | "push_list": [
55 | {"type": "qq_user", "id": "qq用户号", "port": 5700, "color_dic": {"mea": 1, "mea_vip": 1, "maturin": 1, "maturin_vip": 1}},
56 | {"type": "qq_group", "id": "qq群号", "port": 5700, "color_dic": {"mea": 4, "mea_vip": 4, "maturin": 4, "maturin_vip": 4}},
57 | {"type": "discord", "id": "discord webhook链接", "color_dic": {"mea": 1, "mea_vip": 1}}
58 | ]
59 | }
60 | },
61 | "twitter": {
62 | "submonitor_dic": {
63 | "TwitterUser 神楽めあ": {"class": "TwitterUser", "target": "KaguraMea_VoV", "target_name": "神楽めあ", "config_name": "twitter_config", "interval": 60, "no_increase": "True", "no_repeat": 7200},
64 | "TwitterTweet 神楽めあ": {"class": "TwitterTweet", "target": "KaguraMea_VoV", "target_name": "神楽めあ", "config_name": "twitter_config", "interval": 60},
65 | "TwitterFleet 神楽めあ": {"class": "TwitterFleet", "target": "KaguraMea_VoV", "target_name": "神楽めあ", "config_name": "twitter_config", "interval": 60},
66 | "TwitcastLive 神楽めあ": {"class": "TwitcastLive", "target": "KaguraMea_VoV", "target_name": "神楽めあ", "config_name": "twitter_config", "interval": 60},
67 | "TwitcastLive 愛繋璃": {"class": "TwitcastLive", "target": "maturin_love221", "target_name": "愛繋璃", "config_name": "twitter_config", "interval": 60},
68 | "TwitterTweet 湊あくあ": {"class": "TwitterTweet", "target": "minatoaqua", "target_name": "湊あくあ", "config_name": "twitter_config"},
69 | "TwitterTweet 如月こより": {"class": "TwitterTweet", "target": "KisaragiKoyori", "target_name": "如月こより", "config_name": "twitter_config"},
70 | "TwitterTweet 八乙女のえ": {"class": "TwitterTweet", "target": "yaotomenoe", "target_name": "八乙女のえ", "config_name": "twitter_config"},
71 | "TwitterTweet 森永みう": {"class": "TwitterTweet", "target": "morinaga_miu", "target_name": "森永みう", "config_name": "twitter_config"},
72 | "TwitcastLive 森永みう": {"class": "TwitcastLive", "target": "morinaga_miu", "target_name": "森永みう", "config_name": "twitter_config"},
73 | "TwitterTweet 犬山たまき": {"class": "TwitterTweet", "target": "norioo_", "target_name": "犬山たまき", "config_name": "twitter_config"},
74 | "TwitcastLive 犬山たまき": {"class": "TwitcastLive", "target": "norioo_", "target_name": "犬山たまき", "config_name": "twitter_config"},
75 | "TwitterTweet 日ノ隈らん": {"class": "TwitterTweet", "target": "Ran_Hinokuma", "target_name": "日ノ隈らん", "config_name": "twitter_config"},
76 | "TwitterTweet 因幡はねる": {"class": "TwitterTweet", "target": "Haneru_Inaba", "target_name": "因幡はねる", "config_name": "twitter_config"},
77 | "TwitterTweet 物述有栖": {"class": "TwitterTweet", "target": "AliceMononobe", "target_name": "物述有栖", "config_name": "twitter_config"},
78 | "TwitterTweet 宇志海いちご": {"class": "TwitterTweet", "target": "ushimi_ichigo", "target_name": "宇志海いちご", "config_name": "twitter_config"}
79 | },
80 | "twitter_config": {
81 | "interval": 180,
82 | "timezone": 8,
83 | "regen": 900,
84 | "regen_amount": 4,
85 | "vip_dic": {
86 | "KaguraMea_VoV": {"mea": 10, "mea_vip": 10},
87 | "maturin_love221": {"maturin": 10, "maturin_vip": 10}
88 | },
89 | "word_dic": {
90 | "神楽": {"mea": 2},
91 | "かぐら": {"mea": 2},
92 | "めあ": {"mea": 2},
93 | "メア": {"mea": 2},
94 | "めあちゃん": {"mea": 2},
95 | "めあだ": {"mea": 2},
96 | "めあり": {"mea": -1},
97 | "メアリ": {"mea": -1}
98 | },
99 | "cookies": {},
100 | "proxy": {},
101 | "push_list": [
102 | {"type": "qq_user", "id": "qq用户号", "port": 5700, "color_dic": {"mea": 1, "mea_vip": 1, "maturin": 1, "maturin_vip": 1}},
103 | {"type": "qq_group", "id": "qq群号", "port": 5700, "color_dic": {"mea": 4, "mea_vip": 4, "maturin": 4, "maturin_vip": 4}},
104 | {"type": "discord", "id": "discord webhook链接", "color_dic": {"mea": 1, "mea_vip": 1}}
105 | ]
106 | }
107 | },
108 | "fanbox": {
109 | "submonitor_dic": {
110 | "FanboxUser 神楽めあ": {"class": "FanboxUser", "target": "mea", "target_name": "神楽めあ", "config_name": "fanbox_config"},
111 | "FanboxPost 神楽めあ": {"class": "FanboxPost", "target": "mea", "target_name": "神楽めあ", "config_name": "fanbox_config"}
112 | },
113 | "fanbox_config": {
114 | "interval": 180,
115 | "timezone": 8,
116 | "vip_dic": {
117 | "mea": {"mea": 10}
118 | },
119 | "word_dic": {},
120 | "cookies": {},
121 | "proxy": {},
122 | "push_list": [
123 | {"type": "qq_user", "id": "qq用户号", "port": 5700, "color_dic": {"mea": 1}}
124 | ]
125 | }
126 | },
127 | "bilibili": {
128 | "submonitor_dic": {
129 | "BilibiliLive 神楽めあ": {"class": "BilibiliLive", "target": "12235923", "target_name": "神楽めあ", "config_name": "bilibili_config", "interval": 60, "simple_mode": "10"}
130 | },
131 | "bilibili_config": {
132 | "interval": 180,
133 | "timezone": 8,
134 | "vip_dic": {
135 | "12235923": {"mea": 10}
136 | },
137 | "word_dic": {
138 | "【": {"trans_vip": 4},
139 | "】": {"trans_vip": 4},
140 | "\"": {"trans_vip": 4},
141 | "'": {"trans_vip": 4},
142 | "“": {"trans_vip": 4},
143 | "”": {"trans_vip": 4}
144 | },
145 | "cookies": {},
146 | "proxy": {},
147 | "push_list": [
148 | {"type": "qq_user", "id": "qq用户号", "port": 5700, "color_dic": {"mea": 1}},
149 | {"type": "qq_group", "id": "qq群号", "port": 5700, "color_dic": {"mea": 4, "trans_vip": 4}},
150 | {"type": "discord", "id": "discord webhook链接", "color_dic": {"mea": 1, "trans_vip": 4}}
151 | ]
152 | }
153 | },
154 | "other": {
155 | "submonitor_dic": {
156 | "LolUser 神楽めあ": {"class": "LolUser", "target": "かぐらめあっ", "target_name": "神楽めあ", "config_name": "other_config"},
157 | "SteamUser 神楽めあ": {"class": "SteamUser", "target": "76561198841028918", "target_name": "神楽めあ", "config_name": "other_config"},
158 | "OsuUser 神楽めあ": {"class": "OsuUser", "target": "13533714", "target_name": "神楽めあ", "config_name": "other_config"}
159 | },
160 | "other_config": {
161 | "interval": 180,
162 | "timezone": 8,
163 | "vip_dic": {
164 | "かぐらめあっ": {"mea": 10},
165 | "76561198841028918": {"mea": 10},
166 | "13533714": {"mea": 10}
167 | },
168 | "word_dic": {},
169 | "cookies": {},
170 | "proxy": {},
171 | "push_list": [
172 | {"type": "qq_user", "id": "qq用户号", "port": 5700, "color_dic": {"mea": 1, "maturin": 1}},
173 | {"type": "qq_group", "id": "qq群号", "port": 5700, "color_dic": {"mea": 4}}
174 | ]
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/spider.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | import copy
4 | import datetime
5 | import json
6 | import os
7 | import re
8 | import struct
9 | import threading
10 | import time
11 | import zlib
12 | from urllib.parse import quote, unquote
13 |
14 | import requests
15 | import websocket
16 | from bs4 import BeautifulSoup
17 |
18 |
19 | # 仅从cfg和cfg_mod中获取参数,不会启动子监视器
20 | class SubMonitor(threading.Thread):
21 | def __init__(self, name, tgt, tgt_name, cfg, **cfg_mod):
22 | super().__init__()
23 | self.name = name
24 | self.tgt = tgt
25 | self.tgt_name = tgt_name
26 |
27 | self.interval = 60
28 | self.timezone = 8
29 | self.vip_dic = {}
30 | self.word_dic = {}
31 | self.cookies = {}
32 | self.proxy = {}
33 | self.push_list = []
34 | # 不要直接修改通过cfg引用传递定义的列表和变量,请deepcopy后再修改
35 | for var in cfg:
36 | setattr(self, var, cfg[var])
37 | for var in cfg_mod:
38 | setattr(self, var, cfg_mod[var])
39 |
40 | self.stop_now = False
41 |
42 | def checksubmonitor(self):
43 | pass
44 |
45 | def run(self):
46 | while not self.stop_now:
47 | time.sleep(self.interval)
48 |
49 | def stop(self):
50 | self.stop_now = True
51 |
52 |
53 | # 保留cfg(cfg_mod并不修改cfg本身),可以启动子监视器
54 | class Monitor(SubMonitor):
55 | # 初始化
56 | def __init__(self, name, tgt, tgt_name, cfg, **cfg_mod):
57 | super().__init__(name, tgt, tgt_name, cfg, **cfg_mod)
58 | self.cfg = copy.deepcopy(cfg)
59 |
60 | self.submonitor_config_name = "cfg"
61 | self.submonitor_threads = {}
62 | self.submonitor_cnt = 0
63 | self.submonitor_live_cnt = 0
64 | self.submonitor_checknow = False
65 |
66 | self.stop_now = False
67 |
68 | # 重设submonitorconfig名字并初始化
69 | def submonitorconfig_setname(self, submonitor_config_name):
70 | self.submonitor_config_name = submonitor_config_name
71 | submonitor_config = getattr(self, submonitor_config_name, {"submonitor_dic": {}})
72 | setattr(self, self.submonitor_config_name, submonitor_config)
73 |
74 | # 向submonitorconfig添加预设的config
75 | def submonitorconfig_addconfig(self, config_name, config):
76 | submonitor_config = getattr(self, self.submonitor_config_name)
77 | submonitor_config[config_name] = config
78 | setattr(self, self.submonitor_config_name, submonitor_config)
79 |
80 | # 向submonitorconfig的submonitor_dic中添加子线程信息以启动子线程
81 | def submonitorconfig_addmonitor(self, monitor_name, monitor_class, monitor_target, monitor_target_name,
82 | monitor_config_name, **config_mod):
83 | submonitor_config = getattr(self, self.submonitor_config_name)
84 | if monitor_name not in submonitor_config["submonitor_dic"]:
85 | submonitor_config["submonitor_dic"][monitor_name] = {}
86 | submonitor_config["submonitor_dic"][monitor_name]["class"] = monitor_class
87 | submonitor_config["submonitor_dic"][monitor_name]["target"] = monitor_target
88 | submonitor_config["submonitor_dic"][monitor_name]["target_name"] = monitor_target_name
89 | submonitor_config["submonitor_dic"][monitor_name]["config_name"] = monitor_config_name
90 | for mod in config_mod:
91 | submonitor_config["submonitor_dic"][monitor_name][mod] = config_mod[mod]
92 | setattr(self, self.submonitor_config_name, submonitor_config)
93 |
94 | # 从submonitorconfig的submonitor_dic中删除对应的子线程
95 | def submonitorconfig_delmonitor(self, monitor_name):
96 | submonitor_config = getattr(self, self.submonitor_config_name)
97 | if monitor_name in submonitor_config["submonitor_dic"]:
98 | submonitor_config["submonitor_dic"].pop(monitor_name)
99 | setattr(self, self.submonitor_config_name, submonitor_config)
100 |
101 | # 按照submonitorconfig检查子线程池
102 | def checksubmonitor(self):
103 | if not self.submonitor_checknow:
104 | self.submonitor_checknow = True
105 | submonitorconfig = getattr(self, self.submonitor_config_name)
106 | if "submonitor_dic" in submonitorconfig:
107 | self.submonitor_cnt = len(submonitorconfig["submonitor_dic"])
108 | for monitor_name in submonitorconfig["submonitor_dic"]:
109 | if monitor_name not in self.submonitor_threads:
110 | # 按照submonitorconfig启动子线程并添加到子线程池
111 | monitor_thread = createmonitor(monitor_name, submonitorconfig)
112 | self.submonitor_threads[monitor_name] = monitor_thread
113 |
114 | self.submonitor_live_cnt = 0
115 | for monitor_name in list(self.submonitor_threads):
116 | if monitor_name not in submonitorconfig["submonitor_dic"]:
117 | # 按照submonitorconfig关闭子线程并清理子线程池
118 | if self.submonitor_threads[monitor_name].is_alive():
119 | self.submonitor_threads[monitor_name].stop()
120 | self.submonitor_live_cnt += 1
121 | else:
122 | self.submonitor_threads.pop(monitor_name)
123 | else:
124 | # 从子线程池检查并重启
125 | if self.submonitor_threads[monitor_name].is_alive():
126 | self.submonitor_threads[monitor_name].checksubmonitor()
127 | self.submonitor_live_cnt += 1
128 | else:
129 | self.submonitor_threads[monitor_name].stop()
130 | monitor_thread = createmonitor(monitor_name, submonitorconfig)
131 | self.submonitor_threads[monitor_name] = monitor_thread
132 | if self.submonitor_live_cnt > 0 or self.submonitor_cnt > 0:
133 | printlog(
134 | '[Check] "%s" 子线程运行情况:%s/%s' % (self.name, self.submonitor_live_cnt, self.submonitor_cnt))
135 | self.submonitor_checknow = False
136 |
137 | # 启动
138 | def run(self):
139 | self.checksubmonitor()
140 | while not self.stop_now:
141 | time.sleep(self.interval)
142 |
143 | # 停止线程
144 | def stop(self):
145 | self.stop_now = True
146 | for monitor_name in self.submonitor_threads:
147 | self.submonitor_threads[monitor_name].stop()
148 |
149 |
150 | # vip=tgt, word=title+description, standby_chat="True"/"False", standby_chat_onstart="True"/"False", no_chat="True"/"False", status_push="等待|开始|结束|上传|删除", regen="False"/"间隔秒数", regen_amount="1"/"恢复数量"
151 | class YoutubeLive(Monitor):
152 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
153 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
154 |
155 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
156 | if not os.path.exists('./log/%s' % self.__class__.__name__):
157 | os.mkdir('./log/%s' % self.__class__.__name__)
158 |
159 | # 重新设置submonitorconfig用于启动子线程,并添加频道id信息到子进程使用的cfg中
160 | self.submonitorconfig_setname("youtubechat_submonitor_cfg")
161 | self.submonitorconfig_addconfig("youtubechat_config", self.cfg)
162 |
163 | self.is_firstrun = True
164 | # video_id为字符
165 | self.videodic = {}
166 | # 是否检测待机直播间的弹幕
167 | self.standby_chat = getattr(self, "standby_chat", "False")
168 | # 是否检测在第一次检测时已开启的待机直播间的弹幕
169 | self.standby_chat_onstart = getattr(self, "standby_chat_onstart", "False")
170 | # 不记录弹幕
171 | self.no_chat = getattr(self, "no_chat", "False")
172 | # 需要推送的情况,其中等待|开始|结束是直播和首播才有的情况,上传是视频才有的情况,删除则都存在
173 | self.status_push = getattr(self, "status_push", "等待|开始|结束|上传|删除")
174 | # 推送惩罚恢复间隔
175 | self.regen = getattr(self, "regen", "False")
176 | # 每次推送惩罚恢复量
177 | self.regen_amount = getattr(self, "regen_amount", 1)
178 |
179 | def run(self):
180 | while not self.stop_now:
181 | # 更新视频列表
182 | try:
183 | videodic_new = getyoutubevideodic(self.tgt, self.cookies, self.proxy)
184 | for video_id in videodic_new:
185 | if video_id not in self.videodic:
186 | self.videodic[video_id] = videodic_new[video_id]
187 | if not self.is_firstrun or videodic_new[video_id][
188 | "video_status"] == "等待" and self.standby_chat_onstart == "True" or videodic_new[video_id][
189 | "video_status"] == "开始":
190 | self.push(video_id)
191 | if self.is_firstrun:
192 | writelog(self.logpath,
193 | '[Info] "%s" getyoutubevideodic %s: %s' % (self.name, self.tgt, videodic_new))
194 | self.is_firstrun = False
195 | writelog(self.logpath, '[Success] "%s" getyoutubevideodic %s' % (self.name, self.tgt))
196 | except Exception as e:
197 | printlog('[Error] "%s" getyoutubevideodic %s: %s' % (self.name, self.tgt, e))
198 | writelog(self.logpath, '[Error] "%s" getyoutubevideodic %s: %s' % (self.name, self.tgt, e))
199 |
200 | # 更新视频状态
201 | for video_id in self.videodic:
202 | if self.videodic[video_id]["video_status"] == "等待" or self.videodic[video_id]["video_status"] == "开始":
203 | try:
204 | video_status = getyoutubevideostatus(video_id, self.cookies, self.proxy)
205 | if self.videodic[video_id]["video_status"] != video_status:
206 | self.videodic[video_id]["video_status"] = video_status
207 | self.push(video_id)
208 | writelog(self.logpath, '[Success] "%s" getyoutubevideostatus %s' % (self.name, video_id))
209 | except Exception as e:
210 | printlog("[Error] %s getvideostatus %s: %s" % (self.name, video_id, e))
211 | writelog(self.logpath, '[Error] "%s" getyoutubevideostatus %s: %s' % (self.name, video_id, e))
212 | time.sleep(self.interval)
213 |
214 | def push(self, video_id):
215 | if self.status_push.count(self.videodic[video_id]["video_status"]):
216 | # 获取视频简介
217 | try:
218 | video_description = getyoutubevideodescription(video_id, self.cookies, self.proxy)
219 | writelog(self.logpath,
220 | '[Success] "%s" getyoutubevideodescription %s' % (self.name, video_id))
221 | except Exception as e:
222 | printlog('[Error] "%s" getyoutubevideodescription %s: %s' % (self.name, video_id, e))
223 | writelog(self.logpath, '[Error] "%s" getyoutubevideodescription %s: %s' % (self.name, video_id, e))
224 | video_description = ""
225 |
226 | # 计算推送力度
227 | pushcolor_vipdic = getpushcolordic(self.tgt, self.vip_dic)
228 | pushcolor_worddic = getpushcolordic("%s\n%s" % (self.videodic[video_id]["video_title"], video_description),
229 | self.word_dic)
230 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
231 |
232 | # 进行推送
233 | if pushcolor_dic:
234 | pushtext = "【%s %s %s%s】\n标题:%s\n时间:%s\n网址:https://www.youtube.com/watch?v=%s" % (
235 | self.__class__.__name__, self.tgt_name, self.videodic[video_id]["video_type"],
236 | self.videodic[video_id]["video_status"], self.videodic[video_id]["video_title"],
237 | formattime(self.videodic[video_id]["video_timestamp"], self.timezone), video_id)
238 | pushall(pushtext, pushcolor_dic, self.push_list)
239 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
240 | writelog(self.logpath,
241 | '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
242 |
243 | if self.no_chat != "True":
244 | # 开始记录弹幕
245 | if self.videodic[video_id]["video_status"] == "等待" and self.standby_chat == "True" or \
246 | self.videodic[video_id]["video_status"] == "开始":
247 | monitor_name = "%s - YoutubeChat %s" % (self.name, video_id)
248 | if monitor_name not in getattr(self, self.submonitor_config_name)["submonitor_dic"]:
249 | self.submonitorconfig_addmonitor(monitor_name, "YoutubeChat", video_id, self.tgt_name,
250 | "youtubechat_config", tgt_channel=self.tgt, interval=2,
251 | regen=self.regen, regen_amount=self.regen_amount)
252 | self.checksubmonitor()
253 | printlog('[Info] "%s" startsubmonitor %s' % (self.name, monitor_name))
254 | writelog(self.logpath, '[Info] "%s" startsubmonitor %s' % (self.name, monitor_name))
255 | # 停止记录弹幕
256 | else:
257 | monitor_name = "%s - YoutubeChat %s" % (self.name, video_id)
258 | if monitor_name in getattr(self, self.submonitor_config_name)["submonitor_dic"]:
259 | self.submonitorconfig_delmonitor(monitor_name)
260 | self.checksubmonitor()
261 | printlog('[Info] "%s" stopsubmonitor %s' % (self.name, monitor_name))
262 | writelog(self.logpath, '[Info] "%s" stopsubmonitor %s' % (self.name, monitor_name))
263 |
264 |
265 | # vip=userchannel, word=text, punish=tgt+push(不包括含有'vip'的类型)
266 | class YoutubeChat(SubMonitor):
267 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
268 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
269 |
270 | self.logpath = './log/%s/%s/%s.txt' % (
271 | self.__class__.__name__, self.tgt_name, self.name)
272 | if not os.path.exists('./log/%s' % self.__class__.__name__):
273 | os.mkdir('./log/%s' % self.__class__.__name__)
274 | if not os.path.exists('./log/%s/%s' % (self.__class__.__name__, self.tgt_name)):
275 | os.mkdir('./log/%s/%s' % (self.__class__.__name__, self.tgt_name))
276 | self.chatpath = './log/%s/%s/%s_chat.txt' % (
277 | self.__class__.__name__, self.tgt_name, self.name)
278 |
279 | # continuation为字符
280 | self.continuation = False
281 | self.key = False
282 | self.pushpunish = {}
283 | self.regen_time = 0
284 | self.tgt_channel = getattr(self, "tgt_channel", "")
285 | self.regen = getattr(self, "regen", "False")
286 | self.regen_amount = getattr(self, "regen_amount", 1)
287 |
288 | def run(self):
289 | while not self.stop_now:
290 | # 获取continuation
291 | if not self.continuation:
292 | try:
293 | self.continuation, self.key = getyoutubechatcontinuation(self.tgt, self.proxy)
294 | writelog(self.logpath,
295 | '[Info] "%s" getyoutubechatcontinuation %s: %s(%s)' % (self.name, self.tgt, self.continuation, self.key))
296 | writelog(self.logpath, '[Success] "%s" getyoutubechatcontinuation %s' % (self.name, self.tgt))
297 | except Exception as e:
298 | printlog('[Error] "%s" getyoutubechatcontinuation %s: %s' % (self.name, self.tgt, e))
299 | writelog(self.logpath, '[Error] "%s" getyoutubechatcontinuation %s: %s' % (self.name, self.tgt, e))
300 | time.sleep(5)
301 | continue
302 |
303 | # 获取直播评论列表
304 | if self.continuation:
305 | try:
306 | chatlist, self.continuation = getyoutubechatlist(self.tgt, self.continuation, self.key, self.proxy)
307 | for chat in chatlist:
308 | self.push(chat)
309 |
310 | # 目标每次请求获取5条评论,间隔时间应在0.1~2秒之间
311 | if len(chatlist) > 0:
312 | self.interval = self.interval * 5 / len(chatlist)
313 | else:
314 | self.interval = 2
315 | if self.interval > 2:
316 | self.interval = 2
317 | if self.interval < 0.1:
318 | self.interval = 0.1
319 | except Exception as e:
320 | printlog('[Error] "%s" getyoutubechatlist %s(%s): %s' % (self.name, self.continuation, self.key, e))
321 | writelog(self.logpath, '[Error] "%s" getyoutubechatlist %s(%s): %s' % (self.name, self.continuation, self.key, e))
322 | time.sleep(self.interval)
323 |
324 | def push(self, chat):
325 | writelog(self.chatpath, "%s\t%s\t%s\t%s\t%s" % (
326 | chat["chat_timestamp"], chat["chat_username"], chat["chat_userchannel"], chat["chat_type"],
327 | chat["chat_text"]))
328 |
329 | pushcolor_vipdic = getpushcolordic(chat["chat_userchannel"], self.vip_dic)
330 | pushcolor_worddic = getpushcolordic(chat["chat_text"], self.word_dic)
331 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
332 |
333 | if pushcolor_dic:
334 | pushcolor_dic = self.punish(pushcolor_dic)
335 |
336 | pushtext = "【%s %s 直播评论】\n用户:%s\n内容:%s\n类型:%s\n时间:%s\n网址:https://www.youtube.com/watch?v=%s" % (
337 | self.__class__.__name__, self.tgt_name, chat["chat_username"], chat["chat_text"], chat["chat_type"],
338 | formattime(chat["chat_timestamp"], self.timezone), self.tgt)
339 | pushall(pushtext, pushcolor_dic, self.push_list)
340 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
341 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
342 |
343 | def punish(self, pushcolor_dic):
344 | # 推送惩罚恢复
345 | if self.regen != "False":
346 | time_now = getutctimestamp()
347 | regen_amt = int(int((time_now - self.regen_time) / float(self.regen)) * float(self.regen_amount))
348 | if regen_amt:
349 | self.regen_time = time_now
350 | for color in list(self.pushpunish):
351 | if self.pushpunish[color] > regen_amt:
352 | self.pushpunish[color] -= regen_amt
353 | else:
354 | self.pushpunish.pop(color)
355 |
356 | # 去除来源频道的相关权重
357 | if self.tgt_channel in self.vip_dic:
358 | for color in self.vip_dic[self.tgt_channel]:
359 | if color in pushcolor_dic and not color.count("vip"):
360 | pushcolor_dic[color] -= self.vip_dic[self.tgt_channel][color]
361 |
362 | # 只对pushcolor_dic存在的键进行修改,不同于addpushcolordic
363 | for color in self.pushpunish:
364 | if color in pushcolor_dic and not color.count("vip"):
365 | pushcolor_dic[color] -= self.pushpunish[color]
366 |
367 | # 更新pushpunish
368 | for color in pushcolor_dic:
369 | if pushcolor_dic[color] > 0 and not color.count("vip"):
370 | if color in self.pushpunish:
371 | self.pushpunish[color] += 1
372 | else:
373 | self.pushpunish[color] = 1
374 | return pushcolor_dic
375 |
376 |
377 | # vip=tgt, word=text
378 | class YoutubeCom(SubMonitor):
379 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
380 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
381 |
382 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
383 | if not os.path.exists('./log/%s' % self.__class__.__name__):
384 | os.mkdir('./log/%s' % self.__class__.__name__)
385 |
386 | self.is_firstrun = True
387 | # post_id为字符
388 | self.postlist = []
389 |
390 | def run(self):
391 | while not self.stop_now:
392 | # 获取帖子列表
393 | try:
394 | postdic_new = getyoutubepostdic(self.tgt, self.cookies, self.proxy)
395 | for post_id in postdic_new:
396 | if post_id not in self.postlist:
397 | self.postlist.append(post_id)
398 | if not self.is_firstrun:
399 | self.push(post_id, postdic_new)
400 | if self.is_firstrun:
401 | writelog(self.logpath,
402 | '[Info] "%s" getyoutubepostdic %s: %s' % (self.name, self.tgt, postdic_new))
403 | self.is_firstrun = False
404 | writelog(self.logpath, '[Success] "%s" getyoutubepostdic %s' % (self.name, self.tgt))
405 | except Exception as e:
406 | printlog('[Error] "%s" getyoutubepostdic %s: %s' % (self.name, self.tgt, e))
407 | writelog(self.logpath, '[Error] "%s" getyoutubepostdic %s: %s' % (self.name, self.tgt, e))
408 | time.sleep(self.interval)
409 |
410 | def push(self, post_id, postdic):
411 | pushcolor_vipdic = getpushcolordic(self.tgt, self.vip_dic)
412 | pushcolor_worddic = getpushcolordic(postdic[post_id]["post_text"], self.word_dic)
413 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
414 |
415 | # 进行推送
416 | if pushcolor_dic:
417 | pushtext = "【%s %s 社区帖子】\n内容:%s\n链接:%s\n时间:%s\n网址:https://www.youtube.com/post/%s" % (
418 | self.__class__.__name__, self.tgt_name, postdic[post_id]["post_text"][0:3000],
419 | postdic[post_id]["post_link"], postdic[post_id]["post_time"], post_id)
420 | pushall(pushtext, pushcolor_dic, self.push_list)
421 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
422 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
423 |
424 |
425 | # word=text
426 | class YoutubeNote(SubMonitor):
427 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
428 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
429 |
430 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
431 | if not os.path.exists('./log/%s' % self.__class__.__name__):
432 | os.mkdir('./log/%s' % self.__class__.__name__)
433 |
434 | self.is_firstrun = True
435 | self.token = False
436 | # note_id为整数
437 | self.note_id_old = 0
438 |
439 | def run(self):
440 | while not self.stop_now:
441 | # 获取token
442 | if not self.token:
443 | try:
444 | self.token = getyoutubetoken(self.cookies, self.proxy)
445 | writelog(self.logpath, '[Info] "%s" getyoutubetoken %s: %s' % (self.name, self.tgt, self.token))
446 | writelog(self.logpath, '[Success] "%s" getyoutubetoken %s' % (self.name, self.tgt))
447 | except Exception as e:
448 | printlog('[Error] "%s" getyoutubetoken %s: %s' % (self.name, self.tgt, e))
449 | writelog(self.logpath, '[Error] "%s" getyoutubetoken %s: %s' % (self.name, self.tgt, e))
450 | time.sleep(5)
451 | continue
452 |
453 | # 获取订阅通知列表
454 | if self.token:
455 | try:
456 | notedic_new = getyoutubenotedic(self.token, self.cookies, self.proxy)
457 | if self.is_firstrun:
458 | if notedic_new:
459 | self.note_id_old = sorted(notedic_new, reverse=True)[0]
460 | writelog(self.logpath,
461 | '[Info] "%s" getyoutubenotedic %s: %s' % (self.name, self.tgt, notedic_new))
462 | self.is_firstrun = False
463 | else:
464 | for note_id in notedic_new:
465 | if note_id > self.note_id_old:
466 | self.push(note_id, notedic_new)
467 | if notedic_new:
468 | self.note_id_old = sorted(notedic_new, reverse=True)[0]
469 | writelog(self.logpath, '[Success] "%s" getyoutubenotedic %s' % (self.name, self.tgt))
470 | except Exception as e:
471 | printlog('[Error] "%s" getyoutubenotedic %s: %s' % (self.name, self.tgt, e))
472 | writelog(self.logpath, '[Error] "%s" getyoutubenotedic %s: %s' % (self.name, self.tgt, e))
473 | time.sleep(self.interval)
474 |
475 | def push(self, note_id, notedic):
476 | pushcolor_worddic = getpushcolordic(notedic[note_id]["note_text"], self.word_dic)
477 | pushcolor_dic = pushcolor_worddic
478 |
479 | if pushcolor_dic:
480 | pushtext = "【%s %s 订阅通知】\n内容:%s\n时间:%s\n网址:https://www.youtube.com/watch?v=%s" % (
481 | self.__class__.__name__, self.tgt_name, notedic[note_id]["note_text"],
482 | notedic[note_id]["note_time"], notedic[note_id]["note_videoid"])
483 | pushall(pushtext, pushcolor_dic, self.push_list)
484 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
485 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
486 |
487 |
488 | # vip=tgt, no_increase="True"/"False", no_repeat="False"/"间隔秒数"
489 | class TwitterUser(SubMonitor):
490 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
491 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
492 |
493 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
494 | if not os.path.exists('./log/%s' % self.__class__.__name__):
495 | os.mkdir('./log/%s' % self.__class__.__name__)
496 |
497 | self.is_firstrun = True
498 | self.userdata_dic = {}
499 | # 是否不推送推文和媒体数量的增加
500 | self.no_increase = getattr(self, "no_increase", "False")
501 | # 是否不推送短时间内重复的推文和媒体数量
502 | self.no_repeat = getattr(self, "no_repeat", "False")
503 | self.statuses_dic = {}
504 | self.media_dic = {}
505 | self.favourites_dic = {}
506 |
507 | def run(self):
508 | while not self.stop_now:
509 | # 获取用户信息
510 | try:
511 | user_datadic_new = gettwitteruser(self.tgt, self.cookies, self.proxy)
512 | if self.is_firstrun:
513 | self.userdata_dic = user_datadic_new
514 | writelog(self.logpath,
515 | '[Info] "%s" gettwitteruser %s: %s' % (self.name, self.tgt, user_datadic_new))
516 | self.is_firstrun = False
517 | else:
518 | pushtext_body = ""
519 | for key in user_datadic_new:
520 | if key not in self.userdata_dic:
521 | pushtext_body += "新键:%s\n值:%s\n" % (key, str(user_datadic_new[key]))
522 | self.userdata_dic[key] = user_datadic_new[key]
523 | elif self.userdata_dic[key] != user_datadic_new[key]:
524 | if self.no_repeat != "False" and key == "statuses_count":
525 | time_now = getutctimestamp()
526 | if user_datadic_new[key] in self.statuses_dic:
527 | if time_now < self.statuses_dic[user_datadic_new[key]] + float(self.no_repeat):
528 | self.userdata_dic[key] = user_datadic_new[key]
529 | self.statuses_dic[user_datadic_new[key]] = time_now
530 | continue
531 | self.statuses_dic[user_datadic_new[key]] = time_now
532 | if self.no_repeat != "False" and key == "media_count":
533 | time_now = getutctimestamp()
534 | if user_datadic_new[key] in self.media_dic:
535 | if time_now < self.media_dic[user_datadic_new[key]] + float(self.no_repeat):
536 | self.userdata_dic[key] = user_datadic_new[key]
537 | self.media_dic[user_datadic_new[key]] = time_now
538 | continue
539 | self.media_dic[user_datadic_new[key]] = time_now
540 | if self.no_repeat != "False" and key == "favourites_count":
541 | time_now = getutctimestamp()
542 | if user_datadic_new[key] in self.favourites_dic:
543 | if time_now < self.favourites_dic[user_datadic_new[key]] + float(self.no_repeat):
544 | self.userdata_dic[key] = user_datadic_new[key]
545 | self.favourites_dic[user_datadic_new[key]] = time_now
546 | continue
547 | self.favourites_dic[user_datadic_new[key]] = time_now
548 | if self.no_increase == "True" and (key == "statuses_count" or key == "media_count"):
549 | if self.userdata_dic[key] < user_datadic_new[key]:
550 | self.userdata_dic[key] = user_datadic_new[key]
551 | continue
552 | pushtext_body += "键:%s\n原值:%s\n现值:%s\n" % (
553 | key, str(self.userdata_dic[key]), str(user_datadic_new[key]))
554 | self.userdata_dic[key] = user_datadic_new[key]
555 |
556 | if pushtext_body:
557 | self.push(pushtext_body.strip())
558 | writelog(self.logpath, '[Success] "%s" gettwitteruser %s' % (self.name, self.tgt))
559 | except Exception as e:
560 | printlog('[Error] "%s" gettwitteruser %s: %s' % (self.name, self.tgt, e))
561 | writelog(self.logpath, '[Error] "%s" gettwitteruser %s: %s' % (self.name, self.tgt, e))
562 | time.sleep(self.interval)
563 |
564 | def push(self, pushtext_body):
565 | pushcolor_vipdic = getpushcolordic(self.tgt, self.vip_dic)
566 | pushcolor_dic = pushcolor_vipdic
567 |
568 | if pushcolor_dic:
569 | pushtext = "【%s %s 数据改变】\n%s\n时间:%s\n网址:https://twitter.com/%s" % (
570 | self.__class__.__name__, self.tgt_name, pushtext_body, formattime(None, self.timezone), self.tgt)
571 | pushall(pushtext, pushcolor_dic, self.push_list)
572 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
573 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
574 |
575 |
576 | # vip=tgt+mention, word=text
577 | class TwitterTweet(SubMonitor):
578 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
579 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
580 |
581 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
582 | if not os.path.exists('./log/%s' % self.__class__.__name__):
583 | os.mkdir('./log/%s' % self.__class__.__name__)
584 |
585 | self.is_firstrun = True
586 | self.tgt_restid = False
587 | # tweet_id为整数
588 | self.tweet_id_old = 0
589 |
590 | def run(self):
591 | while not self.stop_now:
592 | # 获取用户restid
593 | if not self.tgt_restid:
594 | try:
595 | tgt_dic = gettwitteruser(self.tgt, self.cookies, self.proxy)
596 | self.tgt_restid = tgt_dic["rest_id"]
597 | writelog(self.logpath, '[Info] "%s" gettwitteruser %s: %s' % (self.name, self.tgt, self.tgt_restid))
598 | writelog(self.logpath, '[Success] "%s" gettwitteruser %s' % (self.name, self.tgt))
599 | except Exception as e:
600 | printlog('[Error] "%s" gettwitteruser %s: %s' % (self.name, self.tgt, e))
601 | writelog(self.logpath, '[Error] "%s" gettwitteruser %s: %s' % (self.name, self.tgt, e))
602 | time.sleep(5)
603 | continue
604 |
605 | # 获取推特列表
606 | if self.tgt_restid:
607 | try:
608 | tweetdic_new = gettwittertweetdic(self.tgt_restid, self.cookies, self.proxy)
609 | if self.is_firstrun:
610 | if tweetdic_new:
611 | self.tweet_id_old = sorted(tweetdic_new, reverse=True)[0]
612 | writelog(self.logpath,
613 | '[Info] "%s" gettwittertweetdic %s: %s' % (self.name, self.tgt, tweetdic_new))
614 | self.is_firstrun = False
615 | else:
616 | for tweet_id in tweetdic_new:
617 | if tweet_id > self.tweet_id_old:
618 | self.push(tweet_id, tweetdic_new)
619 | if tweetdic_new:
620 | self.tweet_id_old = sorted(tweetdic_new, reverse=True)[0]
621 | writelog(self.logpath, '[Success] "%s" gettwittertweetdic %s' % (self.name, self.tgt_restid))
622 | except Exception as e:
623 | printlog('[Error] "%s" gettwittertweetdic %s: %s' % (self.name, self.tgt_restid, e))
624 | writelog(self.logpath, '[Error] "%s" gettwittertweetdic %s: %s' % (self.name, self.tgt_restid, e))
625 | time.sleep(self.interval)
626 |
627 | def push(self, tweet_id, tweetdic):
628 | # 获取用户推特时大小写不敏感,但检测用户和提及的时候大小写敏感
629 | pushcolor_vipdic = getpushcolordic("%s\n%s" % (self.tgt, tweetdic[tweet_id]['tweet_mention']),
630 | self.vip_dic)
631 | pushcolor_worddic = getpushcolordic(tweetdic[tweet_id]['tweet_text'], self.word_dic)
632 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
633 |
634 | if pushcolor_dic:
635 | pushmedia = ""
636 | if tweetdic[tweet_id]["tweet_media"]:
637 | pushmedia = "媒体:%s\n" % tweetdic[tweet_id]["tweet_media"]
638 | pushurl = ""
639 | if tweetdic[tweet_id]["tweet_urls"]:
640 | pushurl = "链接:%s\n" % tweetdic[tweet_id]["tweet_urls"]
641 | pushtext = "【%s %s 推特%s】\n内容:%s\n%s%s时间:%s\n网址:https://twitter.com/%s/status/%s" % (
642 | self.__class__.__name__, self.tgt_name, tweetdic[tweet_id]["tweet_type"],
643 | tweetdic[tweet_id]["tweet_text"], pushmedia, pushurl,
644 | formattime(tweetdic[tweet_id]["tweet_timestamp"], self.timezone), self.tgt, tweet_id)
645 | pushall(pushtext, pushcolor_dic, self.push_list)
646 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
647 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
648 |
649 |
650 | # vip=tgt+mention, word=text
651 | class TwitterFleet(SubMonitor):
652 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
653 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
654 |
655 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
656 | if not os.path.exists('./log/%s' % self.__class__.__name__):
657 | os.mkdir('./log/%s' % self.__class__.__name__)
658 |
659 | self.is_firstrun = True
660 | self.tgt_restid = False
661 | # fleet_id为整数
662 | self.fleet_id_old = 0
663 |
664 | def run(self):
665 | while not self.stop_now:
666 | # 获取用户restid
667 | if not self.tgt_restid:
668 | try:
669 | tgt_dic = gettwitteruser(self.tgt, self.cookies, self.proxy)
670 | self.tgt_restid = tgt_dic["rest_id"]
671 | writelog(self.logpath, '[Info] "%s" gettwitteruser %s: %s' % (self.name, self.tgt, self.tgt_restid))
672 | writelog(self.logpath, '[Success] "%s" gettwitteruser %s' % (self.name, self.tgt))
673 | except Exception as e:
674 | printlog('[Error] "%s" gettwitteruser %s: %s' % (self.name, self.tgt, e))
675 | writelog(self.logpath, '[Error] "%s" gettwitteruser %s: %s' % (self.name, self.tgt, e))
676 | time.sleep(5)
677 | continue
678 |
679 | # 获取fleet列表
680 | if self.tgt_restid:
681 | try:
682 | fleetdic_new = gettwitterfleetdic(self.tgt_restid, self.cookies, self.proxy)
683 | if self.is_firstrun:
684 | if fleetdic_new:
685 | self.fleet_id_old = sorted(fleetdic_new, reverse=True)[0]
686 | writelog(self.logpath,
687 | '[Info] "%s" gettwitterfleetdic %s: %s' % (self.name, self.tgt, fleetdic_new))
688 | self.is_firstrun = False
689 | else:
690 | for fleet_id in fleetdic_new:
691 | if fleet_id > self.fleet_id_old:
692 | self.push(fleet_id, fleetdic_new)
693 | if fleetdic_new:
694 | self.fleet_id_old = sorted(fleetdic_new, reverse=True)[0]
695 | writelog(self.logpath, '[Success] "%s" gettwitterfleetdic %s' % (self.name, self.tgt_restid))
696 | except Exception as e:
697 | printlog('[Error] "%s" gettwitterfleetdic %s: %s' % (self.name, self.tgt_restid, e))
698 | writelog(self.logpath, '[Error] "%s" gettwitterfleetdic %s: %s' % (self.name, self.tgt_restid, e))
699 | time.sleep(self.interval)
700 |
701 | def push(self, fleet_id, fleetdic):
702 | # 获取用户推特时大小写不敏感,但检测用户和提及的时候大小写敏感
703 | pushcolor_vipdic = getpushcolordic("%s\n%s" % (self.tgt, fleetdic[fleet_id]['fleet_mention']),
704 | self.vip_dic)
705 | pushcolor_worddic = getpushcolordic(fleetdic[fleet_id]['fleet_text'], self.word_dic)
706 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
707 |
708 | if pushcolor_dic:
709 | pushtext = "【%s %s fleet发布】\n内容:%s\n时间:%s\n网址:%s" % (
710 | self.__class__.__name__, self.tgt_name, fleetdic[fleet_id]["fleet_text"],
711 | formattime(fleetdic[fleet_id]["fleet_timestamp"], self.timezone), fleetdic[fleet_id]["fleet_urls"])
712 | pushall(pushtext, pushcolor_dic, self.push_list)
713 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
714 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
715 |
716 |
717 | # vip=tgt+mention, word=text, only_live="True"/"False", only_liveorvideo="True"/"False", "no_chat"="True"/"False"
718 | class TwitterSearch(SubMonitor):
719 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
720 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
721 |
722 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
723 | if not os.path.exists('./log/%s' % self.__class__.__name__):
724 | os.mkdir('./log/%s' % self.__class__.__name__)
725 |
726 | self.is_firstrun = True
727 | self.tweet_id_old = 0
728 | # 是否只推送有链接指向正在进行的youtube直播的推文
729 | self.only_live = getattr(self, "only_live", "False")
730 | # 是否只推送有链接指向youtube直播或视频的推文
731 | self.only_liveorvideo = getattr(self, "only_liveorvideo", "False")
732 |
733 | def run(self):
734 | while not self.stop_now:
735 | # 获取推特列表
736 | try:
737 | tweetdic_new = gettwittersearchdic(self.tgt, self.cookies, self.proxy)
738 | if self.is_firstrun:
739 | if tweetdic_new:
740 | self.tweet_id_old = sorted(tweetdic_new, reverse=True)[0]
741 | writelog(self.logpath,
742 | '[Info] "%s" gettwittersearchdic %s: %s' % (self.name, self.tgt, tweetdic_new))
743 | self.is_firstrun = False
744 | else:
745 | for tweet_id in tweetdic_new:
746 | if tweet_id > self.tweet_id_old:
747 | self.push(tweet_id, tweetdic_new)
748 | if tweetdic_new:
749 | self.tweet_id_old = sorted(tweetdic_new, reverse=True)[0]
750 | writelog(self.logpath, '[Success] "%s" gettwittersearchdic %s' % (self.name, self.tgt))
751 | except Exception as e:
752 | printlog('[Error] "%s" gettwittersearchdic %s: %s' % (self.name, self.tgt, e))
753 | writelog(self.logpath, '[Error] "%s" gettwittersearchdic %s: %s' % (self.name, self.tgt, e))
754 | time.sleep(self.interval)
755 |
756 | def push(self, tweet_id, tweetdic):
757 | # 检测是否有链接指向正在进行的直播
758 | if self.only_live == "True":
759 | is_live = False
760 | for url in tweetdic[tweet_id]["tweet_urls"]:
761 | if url.count("https://youtu.be/"):
762 | if getyoutubevideostatus(url.replace("https://youtu.be/", ""), self.proxy) == "开始":
763 | is_live = True
764 | break
765 | else:
766 | is_live = True
767 |
768 | # 检测是否有链接指向直播或视频
769 | if self.only_liveorvideo == "True":
770 | is_liveorvideo = False
771 | for url in tweetdic[tweet_id]["tweet_urls"]:
772 | if url.count("https://youtu.be/"):
773 | is_liveorvideo = True
774 | break
775 | else:
776 | is_liveorvideo = True
777 |
778 | if is_live and is_liveorvideo:
779 | pushcolor_vipdic = getpushcolordic("%s\n%s" % (self.tgt, tweetdic[tweet_id]['tweet_mention']),
780 | self.vip_dic)
781 | pushcolor_worddic = getpushcolordic(tweetdic[tweet_id]['tweet_text'], self.word_dic)
782 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
783 |
784 | if pushcolor_dic:
785 | pushtext = "【%s %s 推特%s】\n内容:%s\n媒体:%s\n链接:%s\n时间:%s\n网址:https://twitter.com/a/status/%s" % (
786 | self.__class__.__name__, self.tgt_name, tweetdic[tweet_id]["tweet_type"],
787 | tweetdic[tweet_id]["tweet_text"], tweetdic[tweet_id]["tweet_media"],
788 | tweetdic[tweet_id]["tweet_urls"], formattime(tweetdic[tweet_id]["tweet_timestamp"], self.timezone),
789 | tweet_id)
790 | pushall(pushtext, pushcolor_dic, self.push_list)
791 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
792 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
793 |
794 |
795 | # vip=tgt, "no_chat"="True"/"False", "status_push" = "开始|结束", regen="False"/"间隔秒数", regen_amount="1"/"恢复数量"
796 | class TwitcastLive(Monitor):
797 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
798 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
799 |
800 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
801 | if not os.path.exists('./log/%s' % self.__class__.__name__):
802 | os.mkdir('./log/%s' % self.__class__.__name__)
803 |
804 | # 重新设置submonitorconfig用于启动子线程,并添加频道id信息到子进程使用的cfg中
805 | self.submonitorconfig_setname("twitcastchat_submonitor_cfg")
806 | self.submonitorconfig_addconfig("twitcastchat_config", self.cfg)
807 |
808 | self.livedic = {"": {"live_status": "结束", "live_title": ""}}
809 | self.no_chat = getattr(self, "no_chat", "False")
810 | self.status_push = getattr(self, "status_push", "开始|结束")
811 | self.regen = getattr(self, "regen", "False")
812 | self.regen_amount = getattr(self, "regen_amount", 1)
813 |
814 | def run(self):
815 | while not self.stop_now:
816 | # 获取直播状态
817 | try:
818 | livedic_new = gettwitcastlive(self.tgt, self.cookies, self.proxy)
819 | for live_id in livedic_new:
820 | if live_id not in self.livedic or livedic_new[live_id]["live_status"] == "结束":
821 | for live_id_old in self.livedic:
822 | if self.livedic[live_id_old]["live_status"] != "结束":
823 | self.livedic[live_id_old]["live_status"] = "结束"
824 | self.push(live_id_old)
825 |
826 | if live_id not in self.livedic:
827 | self.livedic[live_id] = livedic_new[live_id]
828 | self.push(live_id)
829 | # 返回非空的live_id则必定为正在直播的状态,不过还是保留防止问题
830 | elif self.livedic[live_id]["live_status"] != livedic_new[live_id]["live_status"]:
831 | self.livedic[live_id] = livedic_new[live_id]
832 | self.push(live_id)
833 | writelog(self.logpath, '[Success] "%s" gettwitcastlive %s' % (self.name, self.tgt))
834 | except Exception as e:
835 | printlog('[Error] "%s" gettwitcastlive %s: %s' % (self.name, self.tgt, e))
836 | writelog(self.logpath, '[Error] "%s" gettwitcastlive %s: %s' % (self.name, self.tgt, e))
837 | time.sleep(self.interval)
838 |
839 | def push(self, live_id):
840 | if self.status_push.count(self.livedic[live_id]["live_status"]):
841 | pushcolor_vipdic = getpushcolordic(self.tgt, self.vip_dic)
842 | pushcolor_worddic = getpushcolordic(self.livedic[live_id]["live_title"], self.word_dic)
843 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
844 |
845 | if pushcolor_dic:
846 | pushtext = "【%s %s 直播%s】\n标题:%s\n时间:%s\n网址:https://twitcasting.tv/%s" % (
847 | self.__class__.__name__, self.tgt_name, self.livedic[live_id]["live_status"],
848 | self.livedic[live_id]["live_title"], formattime(None, self.timezone), self.tgt)
849 | pushall(pushtext, pushcolor_dic, self.push_list)
850 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
851 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
852 |
853 | if self.no_chat != "True":
854 | # 开始记录弹幕
855 | if self.livedic[live_id]["live_status"] == "开始":
856 | monitor_name = "%s - TwitcastChat %s" % (self.name, live_id)
857 | if monitor_name not in getattr(self, self.submonitor_config_name)["submonitor_dic"]:
858 | self.submonitorconfig_addmonitor(monitor_name, "TwitcastChat", live_id, self.tgt_name,
859 | "twitcastchat_config", tgt_channel=self.tgt, interval=2,
860 | regen=self.regen, regen_amount=self.regen_amount)
861 | self.checksubmonitor()
862 | printlog('[Info] "%s" startsubmonitor %s' % (self.name, monitor_name))
863 | writelog(self.logpath, '[Info] "%s" startsubmonitor %s' % (self.name, monitor_name))
864 | # 停止记录弹幕
865 | else:
866 | monitor_name = "%s - TwitcastChat %s" % (self.name, live_id)
867 | if monitor_name in getattr(self, self.submonitor_config_name)["submonitor_dic"]:
868 | self.submonitorconfig_delmonitor(monitor_name)
869 | self.checksubmonitor()
870 | printlog('[Info] "%s" stopsubmonitor %s' % (self.name, monitor_name))
871 | writelog(self.logpath, '[Info] "%s" stopsubmonitor %s' % (self.name, monitor_name))
872 |
873 |
874 | '''
875 | # vip=chat_screenname, word=text, punish=tgt+push(不包括含有'vip'的类型)
876 | class TwitcastChat(SubMonitor):
877 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
878 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
879 |
880 | self.logpath = './log/%s/%s/%s.txt' % (
881 | self.__class__.__name__, self.tgt_name, self.name)
882 | if not os.path.exists('./log/%s' % self.__class__.__name__):
883 | os.mkdir('./log/%s' % self.__class__.__name__)
884 | if not os.path.exists('./log/%s/%s' % (self.__class__.__name__, self.tgt_name)):
885 | os.mkdir('./log/%s/%s' % (self.__class__.__name__, self.tgt_name))
886 | self.chatpath = './log/%s/%s/%s_chat.txt' % (
887 | self.__class__.__name__, self.tgt_name, self.name)
888 |
889 | self.chat_id_old = 0
890 | self.pushpunish = {}
891 | self.regen_time = 0
892 | self.tgt_channel = getattr(self, "tgt_channel", "")
893 | self.regen = getattr(self, "regen", "False")
894 | self.regen_amount = getattr(self, "regen_amount", 1)
895 |
896 | def run(self):
897 | while not self.stop_now:
898 | # 获取直播评论列表
899 | try:
900 | chatlist = gettwitcastchatlist(self.tgt, self.cookies, self.proxy)
901 | for chat in chatlist:
902 | # chatlist默认从小到大排列
903 | if self.chat_id_old < chat['chat_id']:
904 | self.chat_id_old = chat['chat_id']
905 | self.push(chat)
906 |
907 | # 目标每次请求获取5条评论,间隔时间应在0.1~2秒之间
908 | if len(chatlist) > 0:
909 | self.interval = self.interval * 5 / len(chatlist)
910 | else:
911 | self.interval = 2
912 | if self.interval > 2:
913 | self.interval = 2
914 | if self.interval < 0.1:
915 | self.interval = 0.1
916 | except Exception as e:
917 | printlog('[Error] "%s" gettwitcastchatlist %s: %s' % (self.name, self.chat_id_old, e))
918 | writelog(self.logpath, '[Error] "%s" gettwitcastchatlist %s: %s' % (self.name, self.chat_id_old, e))
919 | time.sleep(self.interval)
920 |
921 | def push(self, chat):
922 | writelog(self.chatpath, "%s\t%s\t%s\t%s" % (
923 | chat["chat_timestamp"], chat["chat_name"], chat["chat_screenname"], chat["chat_text"]))
924 |
925 | pushcolor_vipdic = getpushcolordic(chat["chat_screenname"], self.vip_dic)
926 | pushcolor_worddic = getpushcolordic(chat["chat_text"], self.word_dic)
927 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
928 |
929 | if pushcolor_dic:
930 | pushcolor_dic = self.punish(pushcolor_dic)
931 |
932 | pushtext = "【%s %s 直播评论】\n用户:%s(%s)\n内容:%s\n时间:%s\n网址:https://twitcasting.tv/%s" % (
933 | self.__class__.__name__, self.tgt_name, chat["chat_name"], chat["chat_screenname"], chat["chat_text"],
934 | formattime(chat["chat_timestamp"], self.timezone), self.tgt_channel)
935 | pushall(pushtext, pushcolor_dic, self.push_list)
936 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
937 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
938 |
939 | def punish(self, pushcolor_dic):
940 | if self.regen != "False":
941 | time_now = getutctimestamp()
942 | regen_amt = int(int((time_now - self.regen_time) / float(self.regen)) * float(self.regen_amount))
943 | if regen_amt:
944 | self.regen_time = time_now
945 | for color in list(self.pushpunish):
946 | if self.pushpunish[color] > regen_amt:
947 | self.pushpunish[color] -= regen_amt
948 | else:
949 | self.pushpunish.pop(color)
950 |
951 | if self.tgt_channel in self.vip_dic:
952 | for color in self.vip_dic[self.tgt_channel]:
953 | if color in pushcolor_dic and not color.count("vip"):
954 | pushcolor_dic[color] -= self.vip_dic[self.tgt_channel][color]
955 |
956 | for color in self.pushpunish:
957 | if color in pushcolor_dic and not color.count("vip"):
958 | pushcolor_dic[color] -= self.pushpunish[color]
959 |
960 | for color in pushcolor_dic:
961 | if pushcolor_dic[color] > 0 and not color.count("vip"):
962 | if color in self.pushpunish:
963 | self.pushpunish[color] += 1
964 | else:
965 | self.pushpunish[color] = 1
966 | return pushcolor_dic
967 | '''
968 |
969 |
970 | # vip=chat_screenname, word=text, punish=tgt+push(不包括含有'vip'的类型)
971 | class TwitcastChat(SubMonitor):
972 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
973 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
974 |
975 | self.logpath = './log/%s/%s/%s.txt' % (
976 | self.__class__.__name__, self.tgt_name, self.name)
977 | if not os.path.exists('./log/%s' % self.__class__.__name__):
978 | os.mkdir('./log/%s' % self.__class__.__name__)
979 | if not os.path.exists('./log/%s/%s' % (self.__class__.__name__, self.tgt_name)):
980 | os.mkdir('./log/%s/%s' % (self.__class__.__name__, self.tgt_name))
981 | self.chatpath = './log/%s/%s/%s_chat.txt' % (
982 | self.__class__.__name__, self.tgt_name, self.name)
983 |
984 | self.proxyhost = ""
985 | self.proxyport = ""
986 | if 'http' in self.proxy:
987 | self.proxyhost = self.proxy['http'].split(':')[-2].replace('/', '')
988 | self.proxyport = self.proxy['http'].split(':')[-1]
989 | self.pushpunish = {}
990 | self.regen_time = 0
991 | self.tgt_channel = getattr(self, "tgt_channel", "")
992 | self.regen = getattr(self, "regen", "False")
993 | self.regen_amount = getattr(self, "regen_amount", 1)
994 |
995 | def on_message(self, data):
996 | try:
997 | for chat_item in json.loads(data):
998 | chat_type = chat_item['type']
999 | chat_id = chat_item['id']
1000 | chat_screenname = ''
1001 | chat_name = ''
1002 | if 'author' in chat_item:
1003 | chat_screenname = chat_item['author']['screenName']
1004 | chat_name = chat_item['author']['name']
1005 | elif 'sender' in chat_item:
1006 | chat_screenname = chat_item['sender']['screenName']
1007 | chat_name = chat_item['sender']['name']
1008 | chat_timestamp = float(chat_item['createdAt']) / 1000
1009 | chat_text = chat_item['message']
1010 | if 'item' in chat_item:
1011 | chat_item['item']['name']
1012 | chat_type += ' %s' % chat_item['item']['name']
1013 | chat = {"chat_type": chat_type, "chat_id": chat_id, "chat_screenname": chat_screenname, "chat_name": chat_name,
1014 | "chat_timestamp": chat_timestamp, "chat_text": chat_text}
1015 | self.push(chat)
1016 | except Exception as e:
1017 | writelog(self.logpath, '[Error] "%s" error %s: %s' % (self.name, self.tgt, e))
1018 |
1019 | def on_error(self, error):
1020 | writelog(self.logpath, '[Error] "%s" error %s: %s' % (self.name, self.tgt, error))
1021 |
1022 | def on_close(self):
1023 | writelog(self.logpath, '[Stop] "%s" disconnect %s' % (self.name, self.tgt))
1024 |
1025 | def run(self):
1026 | while not self.stop_now:
1027 | try:
1028 | chaturl = gettwitcastchaturl(self.tgt, self.cookies, self.proxy)
1029 | writelog(self.logpath,
1030 | '[Info] "%s" gettwitcastchaturl %s: %s' % (self.name, self.tgt, chaturl))
1031 | writelog(self.logpath, '[Success] "%s" gettwitcastchaturl %s' % (self.name, self.tgt))
1032 | except:
1033 | printlog('[Error] "%s" gettwitcastchaturl %s: %s' % (self.name, self.tgt, e))
1034 | writelog(self.logpath, '[Error] "%s" gettwitcastchaturl %s: %s' % (self.name, self.tgt, e))
1035 | time.sleep(5)
1036 | continue
1037 |
1038 | writelog(self.logpath, '[Start] "%s" connect %s' % (self.name, self.tgt))
1039 | self.ws = websocket.WebSocketApp(chaturl, on_message=self.on_message, on_error=self.on_error, on_close=self.on_close)
1040 | self.ws.run_forever(http_proxy_host=self.proxyhost, http_proxy_port=self.proxyport)
1041 | time.sleep(1)
1042 |
1043 | def push(self, chat):
1044 | writelog(self.chatpath, "%s\t%s\t%s\t%s\t%s" % (
1045 | chat["chat_timestamp"], chat["chat_name"], chat["chat_screenname"], chat["chat_type"], chat["chat_text"]))
1046 |
1047 | pushcolor_vipdic = getpushcolordic(chat["chat_screenname"], self.vip_dic)
1048 | pushcolor_worddic = getpushcolordic(chat["chat_text"], self.word_dic)
1049 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
1050 |
1051 | if pushcolor_dic:
1052 | pushcolor_dic = self.punish(pushcolor_dic)
1053 |
1054 | pushtext = "【%s %s 直播评论】\n用户:%s(%s)\n内容:%s\n类型:%s\n时间:%s\n网址:https://twitcasting.tv/%s" % (
1055 | self.__class__.__name__, self.tgt_name, chat["chat_name"], chat["chat_screenname"], chat["chat_type"], chat["chat_text"],
1056 | formattime(chat["chat_timestamp"], self.timezone), self.tgt_channel)
1057 | pushall(pushtext, pushcolor_dic, self.push_list)
1058 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1059 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1060 |
1061 | def punish(self, pushcolor_dic):
1062 | if self.regen != "False":
1063 | time_now = getutctimestamp()
1064 | regen_amt = int(int((time_now - self.regen_time) / float(self.regen)) * float(self.regen_amount))
1065 | if regen_amt:
1066 | self.regen_time = time_now
1067 | for color in list(self.pushpunish):
1068 | if self.pushpunish[color] > regen_amt:
1069 | self.pushpunish[color] -= regen_amt
1070 | else:
1071 | self.pushpunish.pop(color)
1072 |
1073 | if self.tgt_channel in self.vip_dic:
1074 | for color in self.vip_dic[self.tgt_channel]:
1075 | if color in pushcolor_dic and not color.count("vip"):
1076 | pushcolor_dic[color] -= self.vip_dic[self.tgt_channel][color]
1077 |
1078 | for color in self.pushpunish:
1079 | if color in pushcolor_dic and not color.count("vip"):
1080 | pushcolor_dic[color] -= self.pushpunish[color]
1081 |
1082 | for color in pushcolor_dic:
1083 | if pushcolor_dic[color] > 0 and not color.count("vip"):
1084 | if color in self.pushpunish:
1085 | self.pushpunish[color] += 1
1086 | else:
1087 | self.pushpunish[color] = 1
1088 | return pushcolor_dic
1089 |
1090 | def stop(self):
1091 | self.stop_now = True
1092 | self.ws.close()
1093 |
1094 | # vip=tgt
1095 | class FanboxUser(SubMonitor):
1096 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
1097 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
1098 |
1099 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
1100 | if not os.path.exists('./log/%s' % self.__class__.__name__):
1101 | os.mkdir('./log/%s' % self.__class__.__name__)
1102 |
1103 | self.is_firstrun = True
1104 | self.userdata_dic = {}
1105 |
1106 | def run(self):
1107 | while not self.stop_now:
1108 | # 获取用户信息
1109 | try:
1110 | user_datadic_new = getfanboxuser(self.tgt, self.proxy)
1111 | if self.is_firstrun:
1112 | self.userdata_dic = user_datadic_new
1113 | writelog(self.logpath,
1114 | '[Info] "%s" getfanboxuser %s: %s' % (self.name, self.tgt, user_datadic_new))
1115 | self.is_firstrun = False
1116 | else:
1117 | pushtext_body = ""
1118 | for key in user_datadic_new:
1119 | if key not in self.userdata_dic:
1120 | pushtext_body += "新键:%s\n值:%s\n" % (key, str(user_datadic_new[key]))
1121 | self.userdata_dic[key] = user_datadic_new[key]
1122 | elif self.userdata_dic[key] != user_datadic_new[key]:
1123 | pushtext_body += "键:%s\n原值:%s\n现值:%s\n" % (
1124 | key, str(self.userdata_dic[key]), str(user_datadic_new[key]))
1125 | self.userdata_dic[key] = user_datadic_new[key]
1126 |
1127 | if pushtext_body:
1128 | self.push(pushtext_body)
1129 | writelog(self.logpath, '[Success] "%s" getfanboxuser %s' % (self.name, self.tgt))
1130 | except Exception as e:
1131 | printlog('[Error] "%s" getfanboxuser %s: %s' % (self.name, self.tgt, e))
1132 | writelog(self.logpath, '[Error] "%s" getfanboxuser %s: %s' % (self.name, self.tgt, e))
1133 | time.sleep(self.interval)
1134 |
1135 | def push(self, pushtext_body):
1136 | pushcolor_vipdic = getpushcolordic(self.tgt, self.vip_dic)
1137 | pushcolor_dic = pushcolor_vipdic
1138 |
1139 | if pushcolor_dic:
1140 | pushtext = "【%s %s 数据改变】\n%s\n时间:%s网址:https://%s.fanbox.cc/" % (
1141 | self.__class__.__name__, self.tgt_name, pushtext_body, formattime(None, self.timezone), self.tgt)
1142 | pushall(pushtext, pushcolor_dic, self.push_list)
1143 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1144 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1145 |
1146 |
1147 | # vip=tgt, word=text
1148 | class FanboxPost(SubMonitor):
1149 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
1150 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
1151 |
1152 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
1153 | if not os.path.exists('./log/%s' % self.__class__.__name__):
1154 | os.mkdir('./log/%s' % self.__class__.__name__)
1155 |
1156 | self.is_firstrun = True
1157 | self.postlist = []
1158 |
1159 | def run(self):
1160 | while not self.stop_now:
1161 | # 获取帖子列表
1162 | try:
1163 | postdic_new = getfanboxpostdic(self.tgt, self.cookies, self.proxy)
1164 | for post_id in postdic_new:
1165 | if post_id not in self.postlist:
1166 | self.postlist.append(post_id)
1167 | if not self.is_firstrun:
1168 | self.push(post_id, postdic_new)
1169 | if self.is_firstrun:
1170 | writelog(self.logpath, '[Info] "%s" getfanboxpostdic %s: %s' % (self.name, self.tgt, postdic_new))
1171 | self.is_firstrun = False
1172 | writelog(self.logpath, '[Success] "%s" getfanboxpostdic %s' % (self.name, self.tgt))
1173 | except Exception as e:
1174 | printlog('[Error] "%s" getfanboxpostdic %s: %s' % (self.name, self.tgt, e))
1175 | writelog(self.logpath, '[Error] "%s" getfanboxpostdic %s: %s' % (self.name, self.tgt, e))
1176 | time.sleep(self.interval)
1177 |
1178 | def push(self, post_id, postdic):
1179 | pushcolor_vipdic = getpushcolordic(self.tgt, self.vip_dic)
1180 | pushcolor_worddic = getpushcolordic(postdic[post_id]["post_text"], self.word_dic)
1181 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
1182 |
1183 | if pushcolor_dic:
1184 | pushtext = "【%s %s 社区帖子】\n标题:%s\n内容:%s\n类型:%s\n档位:%s\n时间:%s\n网址:https://%s.fanbox.cc/posts/%s" % (
1185 | self.__class__.__name__, self.tgt_name, postdic[post_id]["post_title"],
1186 | postdic[post_id]["post_text"][0:2500],
1187 | postdic[post_id]["post_type"], postdic[post_id]['post_fee'],
1188 | formattime(postdic[post_id]["post_publishtimestamp"], self.timezone), self.tgt, post_id)
1189 | pushall(pushtext, pushcolor_dic, self.push_list)
1190 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1191 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1192 |
1193 |
1194 | # vip=tgt, "offline_chat"="True"/"False", "simple_mode"="True"/"False"/"合并数量", "no_chat"="True"/"False", "status_push" = "开始|结束", regen="False"/"间隔秒数", regen_amount="1"/"恢复数量"
1195 | class BilibiliLive(Monitor):
1196 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
1197 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
1198 |
1199 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
1200 | if not os.path.exists('./log/%s' % self.__class__.__name__):
1201 | os.mkdir('./log/%s' % self.__class__.__name__)
1202 |
1203 | # 重新设置submonitorconfig用于启动子线程,并添加频道id信息到子进程使用的cfg中
1204 | self.submonitorconfig_setname("bilibilichat_submonitor_cfg")
1205 | self.submonitorconfig_addconfig("bilibilichat_config", self.cfg)
1206 |
1207 | self.livedic = {"": {"live_status": "结束", "live_title": ""}}
1208 | self.offline_chat = getattr(self, "offline_chat", "False")
1209 | self.simple_mode = getattr(self, "simple_mode", "False")
1210 | self.no_chat = getattr(self, "no_chat", "False")
1211 | self.status_push = getattr(self, "status_push", "开始|结束")
1212 | self.regen = getattr(self, "regen", "False")
1213 | self.regen_amount = getattr(self, "regen_amount", 1)
1214 |
1215 | def run(self):
1216 | if self.offline_chat == "True" and self.no_chat != "True":
1217 | monitor_name = "%s - BilibiliChat %s" % (self.name, 'offline_chat')
1218 | if monitor_name not in getattr(self, self.submonitor_config_name)["submonitor_dic"]:
1219 | self.submonitorconfig_addmonitor(monitor_name, "BilibiliChat", self.tgt, self.tgt_name,
1220 | "bilibilichat_config", simple_mode=self.simple_mode)
1221 | self.checksubmonitor()
1222 | printlog('[Info] "%s" startsubmonitor %s' % (self.name, monitor_name))
1223 | writelog(self.logpath, '[Info] "%s" startsubmonitor %s' % (self.name, monitor_name))
1224 |
1225 | while not self.stop_now:
1226 | # 获取直播状态
1227 | try:
1228 | livedic_new = getbilibililivedic(self.tgt, self.proxy)
1229 | for live_id in livedic_new:
1230 | if live_id not in self.livedic or livedic_new[live_id]["live_status"] == "结束":
1231 | for live_id_old in self.livedic:
1232 | if self.livedic[live_id_old]["live_status"] != "结束":
1233 | self.livedic[live_id_old]["live_status"] = "结束"
1234 | self.push(live_id_old)
1235 |
1236 | if live_id not in self.livedic:
1237 | self.livedic[live_id] = livedic_new[live_id]
1238 | self.push(live_id)
1239 | elif self.livedic[live_id]["live_status"] != livedic_new[live_id]["live_status"]:
1240 | self.livedic[live_id] = livedic_new[live_id]
1241 | self.push(live_id)
1242 | writelog(self.logpath, '[Success] "%s" getbilibililivedic %s' % (self.name, self.tgt))
1243 | except Exception as e:
1244 | printlog('[Error] "%s" getbilibililivedic %s: %s' % (self.name, self.tgt, e))
1245 | writelog(self.logpath, '[Error] "%s" getbilibililivedic %s: %s' % (self.name, self.tgt, e))
1246 | time.sleep(self.interval)
1247 |
1248 | def push(self, live_id):
1249 | if self.status_push.count(self.livedic[live_id]["live_status"]):
1250 | pushcolor_vipdic = getpushcolordic(self.tgt, self.vip_dic)
1251 | pushcolor_worddic = getpushcolordic(self.livedic[live_id]["live_title"], self.word_dic)
1252 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
1253 |
1254 | if pushcolor_dic:
1255 | pushtext = "【%s %s 直播%s】\n标题:%s\n时间:%s\n网址:https://live.bilibili.com/%s" % (
1256 | self.__class__.__name__, self.tgt_name, self.livedic[live_id]["live_status"],
1257 | self.livedic[live_id]["live_title"], formattime(live_id, self.timezone), self.tgt)
1258 | pushall(pushtext, pushcolor_dic, self.push_list)
1259 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1260 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1261 |
1262 | if self.offline_chat != "True" and self.no_chat != "True":
1263 | # 开始记录弹幕
1264 | if self.livedic[live_id]["live_status"] == "开始":
1265 | monitor_name = "%s - BilibiliChat %s" % (self.name, live_id)
1266 | if monitor_name not in getattr(self, self.submonitor_config_name)["submonitor_dic"]:
1267 | self.submonitorconfig_addmonitor(monitor_name, "BilibiliChat", self.tgt, self.tgt_name,
1268 | "bilibilichat_config", simple_mode=self.simple_mode,
1269 | regen=self.regen, regen_amount=self.regen_amount)
1270 | self.checksubmonitor()
1271 | printlog('[Info] "%s" startsubmonitor %s' % (self.name, monitor_name))
1272 | writelog(self.logpath, '[Info] "%s" startsubmonitor %s' % (self.name, monitor_name))
1273 | # 停止记录弹幕
1274 | else:
1275 | monitor_name = "%s - BilibiliChat %s" % (self.name, live_id)
1276 | if monitor_name in getattr(self, self.submonitor_config_name)["submonitor_dic"]:
1277 | self.submonitorconfig_delmonitor(monitor_name)
1278 | self.checksubmonitor()
1279 | printlog('[Info] "%s" stopsubmonitor %s' % (self.name, monitor_name))
1280 | writelog(self.logpath, '[Info] "%s" stopsubmonitor %s' % (self.name, monitor_name))
1281 |
1282 |
1283 | # vip=userid, word=text, punish=tgt+push(不包括含有'vip'的类型), 获取弹幕的websocket连接只能使用http proxy
1284 | class BilibiliChat(SubMonitor):
1285 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
1286 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
1287 |
1288 | self.logpath = './log/%s/%s/%s.txt' % (
1289 | self.__class__.__name__, self.tgt_name, self.name)
1290 | if not os.path.exists('./log/%s' % self.__class__.__name__):
1291 | os.mkdir('./log/%s' % self.__class__.__name__)
1292 | if not os.path.exists('./log/%s/%s' % (self.__class__.__name__, self.tgt_name)):
1293 | os.mkdir('./log/%s/%s' % (self.__class__.__name__, self.tgt_name))
1294 | self.chatpath = './log/%s/%s/%s_chat.txt' % (
1295 | self.__class__.__name__, self.tgt_name, self.name)
1296 |
1297 | self.simple_mode = getattr(self, "simple_mode", "False")
1298 | if self.simple_mode != "False":
1299 | self.pushcount = 0
1300 | self.pushtext_old = ""
1301 | # self.pushtext_old = "【%s %s】\n" % (self.__class__.__name__, self.tgt_name)
1302 | self.pushcolor_dic_old = {}
1303 | try:
1304 | self.simple_mode = int(self.simple_mode)
1305 | if self.simple_mode == 0:
1306 | self.simple_mode = 1
1307 | except:
1308 | self.simple_mode = 1
1309 | self.proxyhost = ""
1310 | self.proxyport = ""
1311 | if 'http' in self.proxy:
1312 | self.proxyhost = self.proxy['http'].split(':')[-2].replace('/', '')
1313 | self.proxyport = self.proxy['http'].split(':')[-1]
1314 |
1315 | self.hostlist = []
1316 | self.hostcount = 1
1317 | self.ws = False
1318 | self.is_linked = False
1319 | self.pushpunish = {}
1320 | self.regen_time = 0
1321 | self.regen = getattr(self, "regen", "False")
1322 | self.regen_amount = getattr(self, "regen_amount", 1)
1323 |
1324 | def getpacket(self, data, operation):
1325 | """
1326 | packet_length, header_length, protocol_version, operation, sequence_id
1327 |
1328 | HANDSHAKE=0, HANDSHAKE_REPLY = 1, HEARTBEAT = 2, HEARTBEAT_REPLY = 3, SEND_MSG = 4
1329 | SEND_MSG_REPLY = 5, DISCONNECT_REPLY = 6, AUTH = 7, AUTH_REPLY = 8
1330 | RAW = 9, PROTO_READY = 10, PROTO_FINISH = 11, CHANGE_ROOM = 12
1331 | CHANGE_ROOM_REPLY = 13, REGISTER = 14, REGISTER_REPLY = 15, UNREGISTER = 16, UNREGISTER_REPLY = 17
1332 | """
1333 | body = json.dumps(data).encode('utf-8')
1334 | header = struct.pack('>I2H2I', 16 + len(body), 16, 1, operation, 1)
1335 | return header + body
1336 |
1337 | def prasepacket(self, packet):
1338 | try:
1339 | packet = zlib.decompress(packet[16:])
1340 | except:
1341 | pass
1342 |
1343 | packetlist = []
1344 | offset = 0
1345 | while offset < len(packet):
1346 | try:
1347 | header = packet[offset:offset + 16]
1348 | headertuple = struct.Struct('>I2H2I').unpack_from(header)
1349 | packet_length = headertuple[0]
1350 | operation = headertuple[3]
1351 |
1352 | body = packet[offset + 16:offset + packet_length]
1353 | try:
1354 | data = json.loads(body.decode('utf-8'))
1355 | packetlist.append({"data": data, "operation": operation})
1356 | except:
1357 | packetlist.append({"data": body, "operation": operation})
1358 |
1359 | offset += packet_length
1360 | except:
1361 | continue
1362 | return packetlist
1363 |
1364 | def heartbeat(self):
1365 | while not self.stop_now:
1366 | if self.is_linked:
1367 | self.ws.send(self.getpacket({}, 2))
1368 | time.sleep(30)
1369 | else:
1370 | time.sleep(1)
1371 |
1372 | def parsedanmu(self, chat_json):
1373 | try:
1374 | chat_cmd = chat_json['cmd']
1375 | '''
1376 | if chat_cmd == 'LIVE': # 直播开始
1377 | if chat_cmd == 'PREPARING': # 直播停止
1378 | if chat_cmd == 'WELCOME':
1379 | chat_user = chat_json['data']['uname']
1380 | '''
1381 | if chat_cmd == 'DANMU_MSG':
1382 | chat_type = 'message'
1383 | chat_text = chat_json['info'][1]
1384 | chat_userid = str(chat_json['info'][2][0])
1385 | chat_username = "%s %s %s" % (chat_json['info'][2][1], chat_json['info'][3][1], chat_json['info'][3][0])
1386 | chat_timestamp = float(chat_json['info'][0][4]) / 1000
1387 | # chat_isadmin = dic['info'][2][2] == '1'
1388 | # chat_isvip = dic['info'][2][3] == '1'
1389 | chat = {'chat_type': chat_type, 'chat_text': chat_text, 'chat_userid': chat_userid,
1390 | 'chat_username': chat_username, 'chat_timestamp': chat_timestamp}
1391 | self.push(chat)
1392 | elif chat_cmd == 'SEND_GIFT':
1393 | chat_type = 'gift %s %s' % (chat_json['data']['giftName'], chat_json['data']['num'])
1394 | chat_text = ''
1395 | chat_userid = str(chat_json['data']['uid'])
1396 | chat_username = "%s %s %s" % (chat_json['data']['uname'], chat_json['data']['medal_info']['medal_name'], chat_json['data']['medal_info']['medal_level'])
1397 | chat_timestamp = float(chat_json['data']['timestamp'])
1398 | chat = {'chat_type': chat_type, 'chat_text': chat_text, 'chat_userid': chat_userid,
1399 | 'chat_username': chat_username, 'chat_timestamp': chat_timestamp}
1400 | self.push(chat)
1401 | elif chat_cmd == 'SUPER_CHAT_MESSAGE':
1402 | chat_type = 'superchat CN¥%s' % chat_json['data']['price']
1403 | chat_text = chat_json['data']['message']
1404 | chat_userid = str(chat_json['data']['uid'])
1405 | chat_username = "%s %s %s" % (chat_json['data']['uname'], chat_json['data']['medal_info']['medal_name'], chat_json['data']['medal_info']['medal_level'])
1406 | chat_timestamp = float(chat_json['data']['start_time'])
1407 | chat = {'chat_type': chat_type, 'chat_text': chat_text, 'chat_userid': chat_userid,
1408 | 'chat_username': chat_username, 'chat_timestamp': chat_timestamp}
1409 | self.push(chat)
1410 | elif chat_cmd == 'INTERACT_WORD':
1411 | chat_type = 'enterroom'
1412 | chat_text = ''
1413 | chat_userid = str(chat_json['data']['uid'])
1414 | chat_username = "%s %s %s" % (chat_json['data']['uname'], chat_json['data']['fans_medal']['medal_name'], chat_json['data']['fans_medal']['medal_level'])
1415 | chat_timestamp = float(chat_json['data']['timestamp'])
1416 | chat = {'chat_type': chat_type, 'chat_text': chat_text, 'chat_userid': chat_userid,
1417 | 'chat_username': chat_username, 'chat_timestamp': chat_timestamp}
1418 | self.push(chat)
1419 | except Exception as e:
1420 | writelog(self.logpath, '[Error] "%s" error %s: %s' % (self.name, self.tgt, e))
1421 |
1422 | def on_open(self):
1423 | # 未登录uid则为0,注意int和str类有区别,protover为1则prasepacket中无需用zlib解压
1424 | auth_data = {
1425 | 'uid': 0,
1426 | 'roomid': int(self.tgt),
1427 | 'protover': 2,
1428 | 'platform': 'web',
1429 | 'clientver': '1.10.3',
1430 | 'type': 2,
1431 | 'key':
1432 | requests.get('https://api.live.bilibili.com/room/v1/Danmu/getConf', proxies=self.proxy).json()['data'][
1433 | 'token']
1434 | }
1435 | self.ws.send(self.getpacket(auth_data, 7))
1436 | writelog(self.logpath, '[Start] "%s" connect %s' % (self.name, self.tgt))
1437 |
1438 | def on_message(self, message):
1439 | packetlist = self.prasepacket(message)
1440 |
1441 | for packet in packetlist:
1442 | if packet["operation"] == 8:
1443 | self.is_linked = True
1444 | writelog(self.logpath, '[Success] "%s" connected %s' % (self.name, self.tgt))
1445 |
1446 | if packet["operation"] == 5:
1447 | if isinstance(packet["data"], dict):
1448 | self.parsedanmu(packet["data"])
1449 |
1450 | def on_error(self, error):
1451 | writelog(self.logpath, '[Error] "%s" error %s: %s' % (self.name, self.tgt, error))
1452 |
1453 | def on_close(self):
1454 | # 推送剩余的弹幕
1455 | if self.simple_mode != "False":
1456 | if self.pushtext_old:
1457 | pushall(self.pushtext_old, self.pushcolor_dic_old, self.push_list)
1458 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(self.pushcolor_dic_old), self.pushtext_old))
1459 | writelog(self.logpath,
1460 | '[Info] "%s" pushall %s\n%s' % (self.name, str(self.pushcolor_dic_old), self.pushtext_old))
1461 |
1462 | self.pushtext_old = ""
1463 | # self.pushtext_old = "【%s %s】\n" % (self.__class__.__name__, self.tgt_name)
1464 |
1465 | self.is_linked = False
1466 | writelog(self.logpath, '[Stop] "%s" disconnect %s' % (self.name, self.tgt))
1467 |
1468 | def run(self):
1469 | # 启动heartbeat线程
1470 | heartbeat_thread = threading.Thread(target=self.heartbeat, args=())
1471 | heartbeat_thread.Daemon = True
1472 | heartbeat_thread.start()
1473 |
1474 | while not self.stop_now:
1475 | if self.hostcount < len(self.hostlist):
1476 | host = self.hostlist[self.hostcount]
1477 | self.hostcount += 1
1478 |
1479 | self.ws = websocket.WebSocketApp(host, on_open=self.on_open, on_message=self.on_message,
1480 | on_error=self.on_error, on_close=self.on_close)
1481 | self.ws.run_forever(http_proxy_host=self.proxyhost, http_proxy_port=self.proxyport)
1482 | time.sleep(1)
1483 | else:
1484 | try:
1485 | self.hostlist = getbilibilichathostlist(self.proxy)
1486 | self.hostcount = 0
1487 | writelog(self.logpath,
1488 | '[Info] "%s" getbilibilichathostlist %s: %s' % (self.name, self.tgt, self.hostlist))
1489 | writelog(self.logpath, '[Success] "%s" getbilibilichathostlist %s' % (self.name, self.tgt))
1490 | except Exception as e:
1491 | printlog('[Error] "%s" getbilibilichathostlist %s: %s' % (self.name, self.tgt, e))
1492 | writelog(self.logpath, '[Error] "%s" getbilibilichathostlist %s: %s' % (self.name, self.tgt, e))
1493 | time.sleep(5)
1494 |
1495 | def push(self, chat):
1496 | writelog(self.chatpath, "%s\t%s\t%s\t%s\t%s" % (
1497 | chat["chat_timestamp"], chat["chat_username"], chat["chat_userid"], chat["chat_type"], chat["chat_text"]))
1498 |
1499 | pushcolor_vipdic = getpushcolordic(chat["chat_userid"], self.vip_dic)
1500 | pushcolor_worddic = getpushcolordic(chat["chat_text"], self.word_dic)
1501 | pushcolor_dic = addpushcolordic(pushcolor_vipdic, pushcolor_worddic)
1502 |
1503 | if pushcolor_dic:
1504 | pushcolor_dic = self.punish(pushcolor_dic)
1505 |
1506 | if self.simple_mode == "False":
1507 | pushtext = "【%s %s 直播评论】\n用户:%s(%s)\n内容:%s\n类型:%s\n时间:%s\n网址:https://live.bilibili.com/%s" % (
1508 | self.__class__.__name__, self.tgt_name, chat["chat_username"], chat["chat_userid"],
1509 | chat["chat_text"], chat["chat_type"], formattime(chat["chat_timestamp"], self.timezone),
1510 | self.tgt)
1511 | pushall(pushtext, pushcolor_dic, self.push_list)
1512 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1513 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1514 | else:
1515 | self.pushcount += 1
1516 | self.pushtext_old += chat["chat_text"]
1517 | for color in pushcolor_dic:
1518 | if color in self.pushcolor_dic_old:
1519 | if self.pushcolor_dic_old[color] < pushcolor_dic[color]:
1520 | self.pushcolor_dic_old[color] = pushcolor_dic[color]
1521 | else:
1522 | self.pushcolor_dic_old[color] = pushcolor_dic[color]
1523 |
1524 | if self.pushcount % self.simple_mode == 0:
1525 | pushall(self.pushtext_old, self.pushcolor_dic_old, self.push_list)
1526 | printlog(
1527 | '[Info] "%s" pushall %s\n%s' % (self.name, str(self.pushcolor_dic_old), self.pushtext_old))
1528 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (
1529 | self.name, str(self.pushcolor_dic_old), self.pushtext_old))
1530 | self.pushtext_old = ""
1531 | # self.pushtext_old = "【%s %s】\n" % (self.__class__.__name__, self.tgt_name)
1532 | self.pushcolor_dic_old = {}
1533 | else:
1534 | self.pushtext_old += "\n"
1535 |
1536 | def punish(self, pushcolor_dic):
1537 | if self.regen != "False":
1538 | time_now = getutctimestamp()
1539 | regen_amt = int(int((time_now - self.regen_time) / float(self.regen)) * float(self.regen_amount))
1540 | if regen_amt:
1541 | self.regen_time = time_now
1542 | for color in list(self.pushpunish):
1543 | if self.pushpunish[color] > regen_amt:
1544 | self.pushpunish[color] -= regen_amt
1545 | else:
1546 | self.pushpunish.pop(color)
1547 |
1548 | if self.tgt in self.vip_dic:
1549 | for color in self.vip_dic[self.tgt]:
1550 | if color in pushcolor_dic and not color.count("vip"):
1551 | pushcolor_dic[color] -= self.vip_dic[self.tgt][color]
1552 |
1553 | for color in self.pushpunish:
1554 | if color in pushcolor_dic and not color.count("vip"):
1555 | pushcolor_dic[color] -= self.pushpunish[color]
1556 |
1557 | for color in pushcolor_dic:
1558 | if pushcolor_dic[color] > 0 and not color.count("vip"):
1559 | if color in self.pushpunish:
1560 | self.pushpunish[color] += 1
1561 | else:
1562 | self.pushpunish[color] = 1
1563 | return pushcolor_dic
1564 |
1565 | def stop(self):
1566 | self.stop_now = True
1567 | self.ws.close()
1568 |
1569 |
1570 | # vip=tgt, "ingame_onstart"="True"/"False"
1571 | class LolUser(SubMonitor):
1572 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
1573 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
1574 |
1575 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
1576 | if not os.path.exists('./log/%s' % self.__class__.__name__):
1577 | os.mkdir('./log/%s' % self.__class__.__name__)
1578 |
1579 | self.is_firstrun = True
1580 | self.userdata_dic = {}
1581 | self.lastgametimestamp = 0
1582 | self.tgt_region = getattr(self, "tgt_region", "jp")
1583 | self.ingame_onstart = getattr(self, "ingame_onstart", "True")
1584 |
1585 | def run(self):
1586 | while not self.stop_now:
1587 | # 获取用户信息
1588 | try:
1589 | user_datadic_new = getloluser(self.tgt, self.tgt_region, self.proxy)
1590 | if self.is_firstrun:
1591 | # 首次在线即推送
1592 | if self.ingame_onstart == "True" and user_datadic_new['user_status'] == 'in_game':
1593 | pushtext = "【%s %s 当前比赛】\n时间:%s\n网址:https://%s.op.gg/summoner/userName=%s&l=en_US" % (
1594 | self.__class__.__name__, self.tgt_name,
1595 | formattime(user_datadic_new['user_gametimestamp'], self.timezone), self.tgt_region,
1596 | self.tgt)
1597 | self.push(pushtext)
1598 |
1599 | self.userdata_dic = user_datadic_new
1600 | if user_datadic_new['user_gamedic']:
1601 | self.lastgametimestamp = sorted(user_datadic_new['user_gamedic'], reverse=True)[0]
1602 | writelog(self.logpath, '[Info] "%s" getloluser %s: %s' % (self.name, self.tgt, user_datadic_new))
1603 | self.is_firstrun = False
1604 | else:
1605 | for key in user_datadic_new:
1606 | # 比赛结果 直接推送
1607 | if key == 'user_gamedic':
1608 | for gametimestamp in user_datadic_new['user_gamedic']:
1609 | if gametimestamp > self.lastgametimestamp:
1610 | pushtext = "【%s %s 比赛统计】\n结果:%s\nKDA:%s\n时间:%s\n网址:https://%s.op.gg/summoner/userName=%s&l=en_US" % (
1611 | self.__class__.__name__, self.tgt_name,
1612 | user_datadic_new['user_gamedic'][gametimestamp]['game_result'],
1613 | user_datadic_new['user_gamedic'][gametimestamp]['game_kda'],
1614 | formattime(gametimestamp, self.timezone), self.tgt_region, self.tgt)
1615 | self.push(pushtext)
1616 | if user_datadic_new['user_gamedic']:
1617 | self.lastgametimestamp = sorted(user_datadic_new['user_gamedic'], reverse=True)[0]
1618 | # 当前游戏 整合推送
1619 | elif key == 'user_status':
1620 | if user_datadic_new[key] != self.userdata_dic[key]:
1621 | if user_datadic_new[key] == 'in_game':
1622 | pushtext = "【%s %s 比赛开始】\n时间:%s\n网址:https://%s.op.gg/summoner/userName=%s&l=en_US" % (
1623 | self.__class__.__name__, self.tgt_name,
1624 | formattime(user_datadic_new['user_gametimestamp'], self.timezone),
1625 | self.tgt_region, self.tgt)
1626 | self.push(pushtext)
1627 | else:
1628 | pushtext = "【%s %s 比赛结束】\n时间:%s\n网址:https://%s.op.gg/summoner/userName=%s&l=en_US" % (
1629 | self.__class__.__name__, self.tgt_name, formattime(None, self.timezone),
1630 | self.tgt_region, self.tgt)
1631 | self.push(pushtext)
1632 | self.userdata_dic[key] = user_datadic_new[key]
1633 | # 其他 不推送
1634 | else:
1635 | self.userdata_dic[key] = user_datadic_new[key]
1636 | writelog(self.logpath, '[Success] "%s" getloluser %s' % (self.name, self.tgt))
1637 |
1638 | # 更新信息 最短间隔120秒
1639 | if getutctimestamp() - self.userdata_dic['renew_timestamp'] > 120:
1640 | try:
1641 | renewloluser(self.userdata_dic['user_id'], self.tgt_region, self.proxy)
1642 | writelog(self.logpath,
1643 | '[Success] "%s" renewloluser %s' % (self.name, self.userdata_dic['user_id']))
1644 | except Exception as e:
1645 | printlog('[Error] "%s" renewloluser %s: %s' % (self.name, self.userdata_dic['user_id'], e))
1646 | writelog(self.logpath,
1647 | '[Error] "%s" renewloluser %s: %s' % (self.name, self.userdata_dic['user_id'], e))
1648 | except Exception as e:
1649 | printlog('[Error] "%s" getloluser %s: %s' % (self.name, self.tgt, e))
1650 | writelog(self.logpath, '[Error] "%s" getloluser %s: %s' % (self.name, self.tgt, e))
1651 | time.sleep(self.interval)
1652 |
1653 | def push(self, pushtext):
1654 | pushcolor_vipdic = getpushcolordic(self.tgt, self.vip_dic)
1655 | pushcolor_dic = pushcolor_vipdic
1656 |
1657 | if pushcolor_dic:
1658 | pushall(pushtext, pushcolor_dic, self.push_list)
1659 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1660 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1661 |
1662 |
1663 | # vip=tgt, "online_onstart"="True"/"False"
1664 | class SteamUser(SubMonitor):
1665 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
1666 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
1667 |
1668 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
1669 | if not os.path.exists('./log/%s' % self.__class__.__name__):
1670 | os.mkdir('./log/%s' % self.__class__.__name__)
1671 |
1672 | self.is_firstrun = True
1673 | self.userdata_dic = {}
1674 | self.online_onstart = getattr(self, "online_onstart", "True")
1675 |
1676 | def run(self):
1677 | while not self.stop_now:
1678 | # 获取用户信息
1679 | try:
1680 | user_datadic_new = getsteamuser(self.tgt, self.cookies, self.proxy)
1681 | if self.is_firstrun:
1682 | # 首次在线即推送
1683 | if self.online_onstart == "True" and 'user_status' in user_datadic_new and (
1684 | user_datadic_new['user_status'] == 'Currently Online' or user_datadic_new[
1685 | 'user_status'] == '当前在线' or user_datadic_new['user_status'] == '現在オンラインです。'):
1686 | pushtext = "【%s %s 当前在线】\n时间:%s\n网址:https://steamcommunity.com/profiles/%s" % (
1687 | self.__class__.__name__, self.tgt_name, formattime(None, self.timezone), self.tgt)
1688 | self.push(pushtext)
1689 |
1690 | self.userdata_dic = user_datadic_new
1691 | writelog(self.logpath, '[Info] "%s" getsteamuser %s: %s' % (self.name, self.tgt, user_datadic_new))
1692 | self.is_firstrun = False
1693 | else:
1694 | pushtext_body = ""
1695 | for key in user_datadic_new:
1696 | if key not in self.userdata_dic:
1697 | pushtext_body += "新键:%s\n值:%s\n" % (key, str(user_datadic_new[key]))
1698 | self.userdata_dic[key] = user_datadic_new[key]
1699 | elif self.userdata_dic[key] != user_datadic_new[key]:
1700 | pushtext_body += "键:%s\n原值:%s\n现值:%s\n" % (
1701 | key, str(self.userdata_dic[key]), str(user_datadic_new[key]))
1702 | self.userdata_dic[key] = user_datadic_new[key]
1703 |
1704 | if pushtext_body:
1705 | pushtext = "【%s %s 数据改变】\n%s时间:%s\n网址:https://steamcommunity.com/profiles/%s" % (
1706 | self.__class__.__name__, self.tgt_name, pushtext_body, formattime(None, self.timezone),
1707 | self.tgt)
1708 | self.push(pushtext)
1709 | writelog(self.logpath, '[Success] "%s" getsteamuser %s' % (self.name, self.tgt))
1710 | except Exception as e:
1711 | printlog('[Error] "%s" getsteamuser %s: %s' % (self.name, self.tgt, e))
1712 | writelog(self.logpath, '[Error] "%s" getsteamuser %s: %s' % (self.name, self.tgt, e))
1713 | time.sleep(self.interval)
1714 |
1715 | def push(self, pushtext):
1716 | pushcolor_vipdic = getpushcolordic(self.tgt, self.vip_dic)
1717 | pushcolor_dic = pushcolor_vipdic
1718 |
1719 | if pushcolor_dic:
1720 | pushall(pushtext, pushcolor_dic, self.push_list)
1721 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1722 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1723 |
1724 |
1725 | # vip=tgt, "online_onstart"="True"/"False"
1726 | class OsuUser(SubMonitor):
1727 | def __init__(self, name, tgt, tgt_name, cfg, **config_mod):
1728 | super().__init__(name, tgt, tgt_name, cfg, **config_mod)
1729 |
1730 | self.logpath = './log/%s/%s.txt' % (self.__class__.__name__, self.name)
1731 | if not os.path.exists('./log/%s' % self.__class__.__name__):
1732 | os.mkdir('./log/%s' % self.__class__.__name__)
1733 |
1734 | self.is_firstrun = True
1735 | self.userdata_dic = {}
1736 | self.lastgameid = 0
1737 | self.online_onstart = getattr(self, "online_onstart", "True")
1738 |
1739 | def run(self):
1740 | while not self.stop_now:
1741 | # 获取用户信息
1742 | try:
1743 | user_datadic_new = getosuuser(self.tgt, self.cookies, self.proxy)
1744 | if self.is_firstrun:
1745 | # 首次在线即推送
1746 | if self.online_onstart == "True" and 'is_online' in user_datadic_new and user_datadic_new[
1747 | 'is_online'] == 'true':
1748 | pushtext = "【%s %s 当前在线】\n时间:%s\n网址:https://osu.ppy.sh/users/%s" % (
1749 | self.__class__.__name__, self.tgt_name, formattime(None, self.timezone), self.tgt)
1750 | self.push(pushtext)
1751 |
1752 | self.userdata_dic = user_datadic_new
1753 | if user_datadic_new['user_gamedic']:
1754 | self.lastgameid = sorted(user_datadic_new['user_gamedic'], reverse=True)[0]
1755 | writelog(self.logpath, '[Info] "%s" getosuuser %s: %s' % (self.name, self.tgt, user_datadic_new))
1756 | self.is_firstrun = False
1757 | else:
1758 | pushtext_body = ""
1759 | for key in user_datadic_new:
1760 | # 比赛结果 直接推送
1761 | if key == 'user_gamedic':
1762 | for gameid in user_datadic_new['user_gamedic']:
1763 | if gameid > self.lastgameid:
1764 | pushtext = "【%s %s 比赛统计】\n类型:%s\n结果:%s\n时间:%s\n网址:https://osu.ppy.sh/users/%s" % (
1765 | self.__class__.__name__, self.tgt_name,
1766 | user_datadic_new['user_gamedic'][gameid]['game_type'],
1767 | user_datadic_new['user_gamedic'][gameid]['game_result'],
1768 | formattime(user_datadic_new['user_gamedic'][gameid]['game_timestamp'],
1769 | self.timezone), self.tgt)
1770 | self.push(pushtext)
1771 | if user_datadic_new['user_gamedic']:
1772 | self.lastgameid = sorted(user_datadic_new['user_gamedic'], reverse=True)[0]
1773 | # 其他 整合推送
1774 | else:
1775 | if key not in self.userdata_dic:
1776 | pushtext_body += "新键:%s\n值:%s\n" % (key, str(user_datadic_new[key]))
1777 | self.userdata_dic[key] = user_datadic_new[key]
1778 | elif self.userdata_dic[key] != user_datadic_new[key]:
1779 | pushtext_body += "键:%s\n原值:%s\n现值:%s\n" % (
1780 | key, str(self.userdata_dic[key]), str(user_datadic_new[key]))
1781 | self.userdata_dic[key] = user_datadic_new[key]
1782 |
1783 | if pushtext_body:
1784 | pushtext = "【%s %s 数据改变】\n%s\n时间:%s网址:https://osu.ppy.sh/users/%s" % (
1785 | self.__class__.__name__, self.tgt_name, pushtext_body, formattime(None, self.timezone),
1786 | self.tgt)
1787 | self.push(pushtext)
1788 | writelog(self.logpath, '[Success] "%s" getosuuser %s' % (self.name, self.tgt))
1789 | except Exception as e:
1790 | printlog('[Error] "%s" getosuuser %s: %s' % (self.name, self.tgt, e))
1791 | writelog(self.logpath, '[Error] "%s" getosuuser %s: %s' % (self.name, self.tgt, e))
1792 | time.sleep(self.interval)
1793 |
1794 | def push(self, pushtext):
1795 | pushcolor_vipdic = getpushcolordic(self.tgt, self.vip_dic)
1796 | pushcolor_dic = pushcolor_vipdic
1797 |
1798 | if pushcolor_dic:
1799 | pushall(pushtext, pushcolor_dic, self.push_list)
1800 | printlog('[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1801 | writelog(self.logpath, '[Info] "%s" pushall %s\n%s' % (self.name, str(pushcolor_dic), pushtext))
1802 |
1803 |
1804 | def getyoutubevideodic(user_id, cookies, proxy):
1805 | try:
1806 | videodic = {}
1807 | url = "https://www.youtube.com/channel/%s/videos?view=57&flow=grid" % user_id
1808 | headers = {
1809 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36',
1810 | 'accept-language': 'zh-CN'}
1811 | response = requests.get(url, headers=headers, cookies=cookies, timeout=(3, 7), proxies=proxy)
1812 |
1813 | if response.text.count('window["ytInitialData"]'):
1814 | videolist_json = json.loads(re.findall('window\["ytInitialData"\] = (.*);', response.text)[0])
1815 | else:
1816 | videolist_json = json.loads(re.findall('>var ytInitialData = (.*?);', response.text)[0])
1817 | videolist = []
1818 |
1819 | def __search(key, json):
1820 | for k in json:
1821 | if k == key:
1822 | videolist.append(json[k])
1823 | elif isinstance(json[k], dict):
1824 | __search(key, json[k])
1825 | elif isinstance(json[k], list):
1826 | for item in json[k]:
1827 | if isinstance(item, dict):
1828 | __search(key, item)
1829 | return
1830 |
1831 | __search('gridVideoRenderer', videolist_json)
1832 | __search('videoRenderer', videolist_json)
1833 | for video_json in videolist:
1834 | video_id = video_json['videoId']
1835 | video_title = ''
1836 | if 'simpleText' in video_json['title']:
1837 | video_title = video_json['title']['simpleText']
1838 | elif 'runs' in video_json['title']:
1839 | for video_title_text in video_json['title']['runs']:
1840 | video_title += video_title_text['text']
1841 |
1842 | types = video_json['thumbnailOverlays'][0]['thumbnailOverlayTimeStatusRenderer']['text']['accessibility']['accessibilityData']['label']
1843 | if types == "PREMIERE" or types == "首播" or types == 'プレミア':
1844 | video_type = "首播"
1845 | elif types == "LIVE" or types == "直播" or types == 'ライブ':
1846 | video_type = "直播"
1847 | else:
1848 | video_type = "视频"
1849 |
1850 | status = video_json['thumbnailOverlays'][0]['thumbnailOverlayTimeStatusRenderer']['style']
1851 | if status == "UPCOMING":
1852 | video_status = "等待"
1853 | video_timestamp = float(video_json['upcomingEventData']['startTime'])
1854 | elif status == "LIVE":
1855 | video_status = "开始"
1856 | video_timestamp = getutctimestamp()
1857 | else:
1858 | # status == "DEFAULT"
1859 | video_status = "上传"
1860 | video_timestamp = getutctimestamp()
1861 |
1862 | videodic[video_id] = {"video_title": video_title, "video_type": video_type,
1863 | "video_status": video_status, "video_timestamp": video_timestamp}
1864 | return videodic
1865 | except Exception as e:
1866 | raise e
1867 |
1868 |
1869 | def getyoutubevideostatus(video_id, cookies, proxy):
1870 | try:
1871 | headers = {
1872 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}
1873 | params = (
1874 | ('alt', 'json'),
1875 | ('key', 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'),
1876 | )
1877 | data = {
1878 | "videoId": video_id,
1879 | "context": {
1880 | "client": {
1881 | "utcOffsetMinutes": "0",
1882 | "deviceId": "Chrome",
1883 | "deviceMake": "www",
1884 | "deviceModel": "www",
1885 | "browserName": "Chrome",
1886 | "browserVersion": "83.0.4103.61",
1887 | "osName": "Windows",
1888 | "osVersion": "10.0",
1889 | "clientName": "WEB",
1890 | "clientVersion": "2.20200529.02.01",
1891 | },
1892 | # "activePlayers": [{"playerContextParams":"Q0FFU0FnZ0M="}] #固定值,暂时不需要
1893 | },
1894 | # "cpn":"1111111111111111", #可变值,暂时不需要
1895 | "heartbeatToken": "",
1896 | "heartbeatRequestParams": {"heartbeatChecks": ["HEARTBEAT_CHECK_TYPE_LIVE_STREAM_STATUS"]}
1897 | }
1898 |
1899 | response = requests.post("https://www.youtube.com/youtubei/v1/player/heartbeat", headers=headers, params=params,
1900 | data=str(data), cookies=cookies, timeout=(3, 7), proxies=proxy)
1901 |
1902 | if "stopHeartbeat" in response.json():
1903 | video_status = "上传"
1904 | else:
1905 | if response.json()["playabilityStatus"]["status"] == "UNPLAYABLE":
1906 | video_status = "删除"
1907 | elif response.json()["playabilityStatus"]["status"] == "OK":
1908 | video_status = "开始"
1909 | elif "liveStreamability" not in response.json()["playabilityStatus"] or "displayEndscreen" in \
1910 | response.json()["playabilityStatus"]["liveStreamability"]["liveStreamabilityRenderer"]:
1911 | video_status = "结束"
1912 | else:
1913 | video_status = "等待"
1914 | return video_status
1915 | except Exception as e:
1916 | raise e
1917 |
1918 |
1919 | '''
1920 | def __getyoutubevideostatus(video_id, cookies, proxy):
1921 | """
1922 | 删除:
1923 |
1924 | 视频上传:"isLiveContent":false
1925 |
1926 | 直播等待:"isLiveContent":true,"isLiveNow":false
1927 | 直播开始:"isLiveContent":true,"isLiveNow":true
1928 | 直播结束:"isLiveContent":true,"isLiveNow":false,"endTimestamp":
1929 |
1930 | 首播等待:"isLiveContent":false,"isLiveNow":false
1931 | 首播开始:"isLiveContent":false,"isLiveNow":true
1932 | 首播结束:"isLiveContent":false,"isLiveNow":false,"endTimestamp":
1933 | """
1934 | try:
1935 | url = 'https://www.youtube.com/watch?v=%s' % video_id
1936 | headers = {
1937 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}
1938 | response = requests.get(url, headers=headers, cookies=cookies, timeout=(3, 7), proxies=proxy)
1939 | soup = BeautifulSoup(response.text, 'lxml')
1940 | script = eval('"""{}"""'.format(soup.find(string=re.compile(r'\\"isLiveContent\\":'))))
1941 | if script == "None":
1942 | video_status = "删除"
1943 | elif script.count('"isLiveNow":'):
1944 | if script.count('"endTimestamp":'):
1945 | video_status = "结束"
1946 | elif script.count('"isLiveNow":true'):
1947 | video_status = "开始"
1948 | else:
1949 | video_status = "等待"
1950 | else:
1951 | video_status = "上传"
1952 | return video_status
1953 | except Exception as e:
1954 | raise e
1955 | '''
1956 |
1957 |
1958 | def getyoutubevideodescription(video_id, cookies, proxy):
1959 | try:
1960 | url = 'https://www.youtube.com/watch?v=%s' % video_id
1961 | headers = {
1962 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}
1963 | response = requests.get(url, headers=headers, cookies=cookies, timeout=(3, 7), proxies=proxy)
1964 | if response.text.count(r'\"description\":{\"simpleText\":\"'):
1965 | video_description = re.findall(r'\\"description\\":{\\"simpleText\\":\\"(.*?)\\"}', response.text)[0]
1966 | else:
1967 | video_description = re.findall(r'\"description\":{\"simpleText\":\"(.*?)\"}', response.text)[0]
1968 | video_description = eval('"""{}"""'.format(video_description))
1969 | video_description = eval('"""{}"""'.format(video_description))
1970 | return video_description
1971 | except Exception as e:
1972 | raise e
1973 |
1974 |
1975 | def getyoutubechatcontinuation(video_id, proxy):
1976 | try:
1977 | url = 'https://www.youtube.com/live_chat?is_popout=1&v=%s' % video_id
1978 | headers = {
1979 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}
1980 | response = requests.get(url, headers=headers, timeout=(3, 7), proxies=proxy)
1981 | continuation = re.findall('"continuation":"([^"]*)"', response.text)[0]
1982 | key = re.findall('"INNERTUBE_API_KEY":"([^"]*)"', response.text)[0]
1983 | if continuation:
1984 | return continuation, key
1985 | else:
1986 | raise Exception("Invalid continuation")
1987 | except Exception as e:
1988 | raise e
1989 |
1990 |
1991 | def getyoutubechatlist(video_id, continuation, key, proxy):
1992 | try:
1993 | chatlist = []
1994 | url = "https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=%s" % key
1995 | headers = {
1996 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36',
1997 | 'x-youtube-client-name': '1',
1998 | 'x-youtube-client-version': '2.20210128.02.00',
1999 | 'referer': 'https://www.youtube.com/live_chat?is_popout=1&v=%s' % video_id
2000 | }
2001 | data = {
2002 | "context":{
2003 | "client":{
2004 | "userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)",
2005 | "clientName":"WEB",
2006 | "clientVersion":"2.20210128.02.00",
2007 | "originalUrl":"https://www.youtube.com/live_chat?is_popout=1&v=%s" % video_id,
2008 | "mainAppWebInfo":{
2009 | "graftUrl":"https://www.youtube.com/live_chat?is_popout=1&v=%s" % video_id
2010 | }
2011 | }
2012 | },
2013 | "continuation":continuation}
2014 | response = requests.post(url, headers=headers, json=data, timeout=(3, 7), proxies=proxy)
2015 | continuation_new = re.findall('"continuation": "([^"]*)"', response.text)[0]
2016 | chatlist_json = json.loads(response.text)['continuationContents']['liveChatContinuation']
2017 | if 'actions' in chatlist_json:
2018 | for chat in chatlist_json['actions']:
2019 | if 'addChatItemAction' in chat:
2020 | if 'liveChatTextMessageRenderer' in chat['addChatItemAction']['item']:
2021 | chat_type = 'message'
2022 | chat_dic = chat['addChatItemAction']['item']['liveChatTextMessageRenderer']
2023 | elif 'liveChatPaidMessageRenderer' in chat['addChatItemAction']['item']:
2024 | chat_type = 'superchat'
2025 | chat_dic = chat['addChatItemAction']['item']['liveChatPaidMessageRenderer']
2026 | elif 'liveChatPaidStickerRenderer' in chat['addChatItemAction']['item']:
2027 | chat_type = 'supersticker'
2028 | chat_dic = chat['addChatItemAction']['item']['liveChatPaidStickerRenderer']
2029 | elif 'liveChatMembershipItemRenderer' in chat['addChatItemAction']['item']:
2030 | chat_type = 'membership'
2031 | chat_dic = chat['addChatItemAction']['item']['liveChatMembershipItemRenderer']
2032 | else:
2033 | chat_type = ''
2034 | chat_dic = {}
2035 |
2036 | if chat_dic:
2037 | chat_timestamp = float(chat_dic['timestampUsec']) / 1000000
2038 | chat_username = chat_dic['authorName']['simpleText']
2039 | chat_userchannel = chat_dic['authorExternalChannelId']
2040 | chat_text = ''
2041 | if 'message' in chat_dic:
2042 | for chat_text_run in chat_dic['message']['runs']:
2043 | if 'text' in chat_text_run:
2044 | chat_text += chat_text_run['text']
2045 | elif 'emoji' in chat_text_run:
2046 | chat_text += chat_text_run['emoji']['shortcuts'][0]
2047 | if 'purchaseAmountText' in chat_dic:
2048 | chat_type += ' %s' % chat_dic['purchaseAmountText']['simpleText']
2049 | chatlist.append(
2050 | {"chat_timestamp": chat_timestamp, "chat_username": chat_username,
2051 | "chat_userchannel": chat_userchannel, "chat_type": chat_type,
2052 | "chat_text": chat_text})
2053 | return chatlist, continuation_new
2054 | except Exception as e:
2055 | raise e
2056 |
2057 |
2058 | def getyoutubepostdic(user_id, cookies, proxy):
2059 | try:
2060 | postlist = {}
2061 | url = 'https://www.youtube.com/channel/%s/community' % user_id
2062 | headers = {
2063 | 'authority': 'www.youtube.com',
2064 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36',
2065 | }
2066 | response = requests.get(url, headers=headers, cookies=cookies, timeout=(3, 7), proxies=proxy)
2067 | if response.text.count('window["ytInitialData"]'):
2068 | postpage_json = json.loads(re.findall('window\["ytInitialData"\] = (.*);', response.text)[0])
2069 | else:
2070 | postpage_json = json.loads(re.findall('>var ytInitialData = (.*?);', response.text)[0])
2071 | postlist_json = postpage_json['contents']['twoColumnBrowseResultsRenderer']['tabs'][3]['tabRenderer'][
2072 | 'content']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents']
2073 | for post in postlist_json:
2074 | if 'backstagePostThreadRenderer' in post:
2075 | post_info = post['backstagePostThreadRenderer']['post']['backstagePostRenderer']
2076 | post_id = post_info['postId']
2077 | post_time = ''
2078 | for post_time_run in post_info['publishedTimeText']['runs']:
2079 | post_time += post_time_run['text']
2080 | post_text = ''
2081 | for post_text_run in post_info['contentText']['runs']:
2082 | post_text += post_text_run['text']
2083 | post_link = ''
2084 | if 'backstageAttachment' in post_info:
2085 | if 'videoRenderer' in post_info['backstageAttachment']:
2086 | if 'videoId' in post_info['backstageAttachment']['videoRenderer']:
2087 | post_link = 'https://www.youtube.com/watch?v=%s' % post_info['backstageAttachment']['videoRenderer']['videoId']
2088 | postlist[post_id] = {"post_time": post_time, "post_text": post_text, "post_link": post_link}
2089 | return postlist
2090 | except Exception as e:
2091 | raise e
2092 |
2093 |
2094 | def getyoutubetoken(cookies, proxy):
2095 | try:
2096 | headers = {
2097 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}
2098 | response = requests.get('https://www.youtube.com', headers=headers, cookies=cookies, proxies=proxy)
2099 | token = re.findall('"XSRF_TOKEN":"([^"]*)"', response.text)[0]
2100 | if token:
2101 | return token
2102 | else:
2103 | raise Exception("Invalid token")
2104 | except Exception as e:
2105 | raise e
2106 |
2107 |
2108 | def getyoutubenotedic(token, cookies, proxy):
2109 | try:
2110 | youtubenotedic = {}
2111 | headers = {
2112 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}
2113 | params = (
2114 | ('name', 'signalServiceEndpoint'),
2115 | ('signal', 'GET_NOTIFICATIONS_MENU'),
2116 | )
2117 | data = {
2118 | 'sej': '{"clickTrackingParams":"CAkQovoBGAIiEwi9tvfcj5vnAhVUQ4UKHYyoBeQ=","commandMetadata":{"webCommandMetadata":{"url":"/service_ajax","sendPost":true,"apiUrl":"/youtubei/v1/notification/get_notification_menu"}},"signalServiceEndpoint":{"signal":"GET_NOTIFICATIONS_MENU","actions":[{"openPopupAction":{"popup":{"multiPageMenuRenderer":{"trackingParams":"CAoQ_6sBIhMIvbb33I-b5wIVVEOFCh2MqAXk","style":"MULTI_PAGE_MENU_STYLE_TYPE_NOTIFICATIONS","showLoadingSpinner":true}},"popupType":"DROPDOWN","beReused":true}}]}}',
2119 | 'session_token': token
2120 | }
2121 | response = requests.post('https://www.youtube.com/service_ajax', headers=headers, params=params,
2122 | data=data, cookies=cookies, timeout=(3, 7), proxies=proxy)
2123 | notesec_json = \
2124 | json.loads(response.text)['data']['actions'][0]['openPopupAction']['popup']['multiPageMenuRenderer'][
2125 | 'sections'][0]
2126 | if 'multiPageMenuNotificationSectionRenderer' in notesec_json:
2127 | for note in notesec_json['multiPageMenuNotificationSectionRenderer']['items']:
2128 | if 'notificationRenderer' in note:
2129 | note_id = int(note['notificationRenderer']['notificationId'])
2130 | note_text = note['notificationRenderer']['shortMessage']['simpleText']
2131 | note_time = note['notificationRenderer']['sentTimeText']['simpleText']
2132 | note_videoid = \
2133 | note['notificationRenderer']['navigationEndpoint']['commandMetadata']['webCommandMetadata'][
2134 | 'url'].replace("/watch?v=", "")
2135 | youtubenotedic[note_id] = {"note_text": note_text, "note_time": note_time,
2136 | "note_videoid": note_videoid}
2137 | return youtubenotedic
2138 | except Exception as e:
2139 | raise e
2140 |
2141 |
2142 | def gettwitteruser(user_screenname, cookies, proxy):
2143 | try:
2144 | headers = {
2145 | 'x-csrf-token': cookies['ct0'],
2146 | 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
2147 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36',
2148 | }
2149 | params = (
2150 | ('variables', '{"screen_name":"%s","withHighlightedLabel":false}' % user_screenname),
2151 | )
2152 | response = requests.get('https://api.twitter.com/graphql/G6Lk7nZ6eEKd7LBBZw9MYw/UserByScreenName',
2153 | headers=headers, params=params, cookies=cookies, timeout=(3, 7), proxies=proxy)
2154 |
2155 | user_data = response.json()['data']['user']
2156 | userdata_dic = user_data
2157 | for key in user_data['legacy']:
2158 | userdata_dic[key] = user_data['legacy'][key]
2159 | userdata_dic.pop('legacy')
2160 |
2161 | userdata_dic.pop('followers_count')
2162 | userdata_dic.pop('normal_followers_count')
2163 | userdata_dic.pop('listed_count')
2164 | userdata_dic.pop('notifications')
2165 | userdata_dic.pop('muting')
2166 | userdata_dic.pop('blocked_by')
2167 | userdata_dic.pop('blocking')
2168 | userdata_dic.pop('follow_request_sent')
2169 | userdata_dic.pop('followed_by')
2170 | userdata_dic.pop('following')
2171 |
2172 | return userdata_dic
2173 | except Exception as e:
2174 | raise e
2175 |
2176 |
2177 | def gettwittertweetdic(user_restid, cookies, proxy):
2178 | try:
2179 | tweet_dic = {}
2180 | params = (
2181 | ('include_profile_interstitial_type', '1'),
2182 | ('include_blocking', '1'),
2183 | ('include_blocked_by', '1'),
2184 | ('include_followed_by', '1'),
2185 | ('include_want_retweets', '1'),
2186 | ('include_mute_edge', '1'),
2187 | ('include_can_dm', '1'),
2188 | ('include_can_media_tag', '1'),
2189 | ('skip_status', '1'),
2190 | ('cards_platform', 'Web-12'),
2191 | ('include_cards', '1'),
2192 | ('include_composer_source', 'true'),
2193 | ('include_ext_alt_text', 'true'),
2194 | ('include_reply_count', '1'),
2195 | ('tweet_mode', 'extended'),
2196 | ('include_entities', 'true'),
2197 | ('include_user_entities', 'true'),
2198 | ('include_ext_media_color', 'true'),
2199 | ('include_ext_media_availability', 'true'),
2200 | ('send_error_codes', 'true'),
2201 | ('simple_quoted_tweets', 'true'),
2202 | ('include_tweet_replies', 'true'),
2203 | ('userId', user_restid),
2204 | ('count', '20'),
2205 | ('ext', 'mediaStats,cameraMoment')
2206 | )
2207 | headers = {
2208 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0',
2209 | 'Accept': '*/*',
2210 | 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
2211 | 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
2212 | 'x-twitter-auth-type': 'OAuth2Session',
2213 | 'x-twitter-client-language': 'zh-cn',
2214 | 'x-twitter-active-user': 'yes',
2215 | 'x-csrf-token': cookies['ct0'],
2216 | 'Origin': 'https://twitter.com',
2217 | 'Connection': 'keep-alive',
2218 | 'TE': 'Trailers'
2219 | }
2220 | response = requests.get('https://api.twitter.com/2/timeline/profile/%s.json' % user_restid, headers=headers,
2221 | params=params, cookies=cookies, timeout=(3, 7), proxies=proxy)
2222 |
2223 | if 'globalObjects' in response.json():
2224 | tweetlist_dic = response.json()['globalObjects']['tweets']
2225 | for tweet_id in tweetlist_dic:
2226 | if tweetlist_dic[tweet_id]['user_id_str'] == user_restid:
2227 | tweet_timestamp = phrasetimestamp(tweetlist_dic[tweet_id]['created_at'], '%a %b %d %H:%M:%S %z %Y')
2228 | tweet_text = tweetlist_dic[tweet_id]['full_text']
2229 | if 'retweeted_status_id_str' in tweetlist_dic[tweet_id]:
2230 | tweet_type = "转推"
2231 | elif 'user_mentions' in tweetlist_dic[tweet_id]['entities']:
2232 | tweet_type = "回复"
2233 | else:
2234 | tweet_type = "发布"
2235 | tweet_media = []
2236 | if 'media' in tweetlist_dic[tweet_id]['entities']:
2237 | for media in tweetlist_dic[tweet_id]['entities']['media']:
2238 | tweet_media.append(media['expanded_url'])
2239 | tweet_urls = []
2240 | if 'urls' in tweetlist_dic[tweet_id]['entities']:
2241 | for url in tweetlist_dic[tweet_id]['entities']['urls']:
2242 | tweet_urls.append(url['expanded_url'])
2243 | tweet_mention = ""
2244 | if 'user_mentions' in tweetlist_dic[tweet_id]['entities']:
2245 | for user_mention in tweetlist_dic[tweet_id]['entities']['user_mentions']:
2246 | tweet_mention += "%s\n" % user_mention['screen_name']
2247 | tweet_dic[int(tweet_id)] = {"tweet_timestamp": tweet_timestamp, "tweet_text": tweet_text,
2248 | "tweet_type": tweet_type, "tweet_media": tweet_media,
2249 | "tweet_urls": tweet_urls, "tweet_mention": tweet_mention}
2250 | return tweet_dic
2251 | except Exception as e:
2252 | raise e
2253 |
2254 |
2255 | def gettwitterfleetdic(user_restid, cookies, proxy):
2256 | try:
2257 | fleet_dic = {}
2258 | headers = {
2259 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0',
2260 | 'Accept': '*/*',
2261 | 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
2262 | 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
2263 | 'x-twitter-auth-type': 'OAuth2Session',
2264 | 'x-twitter-client-language': 'zh-cn',
2265 | 'x-twitter-active-user': 'yes',
2266 | 'x-csrf-token': cookies['ct0'],
2267 | 'Origin': 'https://twitter.com',
2268 | 'Connection': 'keep-alive',
2269 | 'TE': 'Trailers'
2270 | }
2271 | response = requests.get('https://api.twitter.com/fleets/v1/user_fleets?user_id=%s' % user_restid, headers=headers,
2272 | cookies=cookies, timeout=(3, 7), proxies=proxy)
2273 |
2274 | for fleetthread in response.json()['fleet_threads']:
2275 | for fleet in fleetthread['fleets']:
2276 | fleet_timestamp = phrasetimestamp(fleet['created_at'].split('.')[0], "%Y-%m-%dT%H:%M:%S")
2277 | fleet_text = ""
2278 | fleet_mention = ""
2279 | if 'media_bounding_boxes' in fleet:
2280 | for fleet_box in fleet['media_bounding_boxes']:
2281 | if fleet_box['entity']['type'] == 'text':
2282 | fleet_text += "%s\n" % fleet_box['entity']['value']
2283 | elif fleet_box['entity']['type'] == 'mention':
2284 | fleet_mention += "%s\n" % fleet_box['entity']['value'].replace('@', '')
2285 | fleet_url = fleet['media_entity']['media_url_https']
2286 | fleet_dic[int(fleet['fleet_id'].split('-')[1])] = {"fleet_timestamp": fleet_timestamp, "fleet_text": fleet_text.strip(), "fleet_mention": fleet_mention, "fleet_urls": fleet_url}
2287 | return fleet_dic
2288 | except Exception as e:
2289 | raise e
2290 |
2291 |
2292 | def gettwittersearchdic(qword, cookies, proxy):
2293 | try:
2294 | tweet_dic = {}
2295 | params = (
2296 | ('include_profile_interstitial_type', '1'),
2297 | ('include_blocking', '1'),
2298 | ('include_blocked_by', '1'),
2299 | ('include_followed_by', '1'),
2300 | ('include_want_retweets', '1'),
2301 | ('include_mute_edge', '1'),
2302 | ('include_can_dm', '1'),
2303 | ('include_can_media_tag', '1'),
2304 | ('skip_status', '1'),
2305 | ('cards_platform', 'Web-12'),
2306 | ('include_cards', '1'),
2307 | ('include_composer_source', 'true'),
2308 | ('include_ext_alt_text', 'true'),
2309 | ('include_reply_count', '1'),
2310 | ('tweet_mode', 'extended'),
2311 | ('include_entities', 'true'),
2312 | ('include_user_entities', 'true'),
2313 | ('include_ext_media_color', 'true'),
2314 | ('include_ext_media_availability', 'true'),
2315 | ('send_error_codes', 'true'),
2316 | ('simple_quoted_tweets', 'true'),
2317 | ('tweet_search_mode', 'live'),
2318 | ('count', '20'),
2319 | ('query_source', 'typed_query'),
2320 | ('pc', '1'),
2321 | ('spelling_corrections', '1'),
2322 | ('ext', 'mediaStats,cameraMoment'),
2323 | )
2324 | headers = {
2325 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0',
2326 | 'Accept': '*/*',
2327 | 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
2328 | 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
2329 | 'x-twitter-auth-type': 'OAuth2Session',
2330 | 'x-twitter-client-language': 'zh-cn',
2331 | 'x-twitter-active-user': 'yes',
2332 | 'x-csrf-token': cookies['ct0'],
2333 | 'Origin': 'https://twitter.com',
2334 | 'Connection': 'keep-alive',
2335 | 'TE': 'Trailers',
2336 | }
2337 | # 推文内容包括#话题标签的文字,filter:links匹配链接图片视频但不匹配#话题标签的链接,%%23相当于#话题标签
2338 | url = 'https://api.twitter.com/2/search/adaptive.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&skip_status=1&cards_platform=Web-12&include_cards=1&include_composer_source=true&include_ext_alt_text=true&include_reply_count=1&tweet_mode=extended&include_entities=true&include_user_entities=true&include_ext_media_color=true&include_ext_media_availability=true&send_error_codes=true&simple_quoted_tweets=true&q=' + qword + '&tweet_search_mode=live&count=20&query_source=typed_query&pc=1&spelling_corrections=1&ext=mediaStats%2CcameraMoment'
2339 | response = requests.get(url, headers=headers, params=params, cookies=cookies, timeout=(3, 7), proxies=proxy)
2340 |
2341 | if 'globalObjects' in response.json():
2342 | tweetlist_dic = response.json()['globalObjects']['tweets']
2343 | for tweet_id in tweetlist_dic.keys():
2344 | tweet_timestamp = phrasetimestamp(tweetlist_dic[tweet_id]['created_at'], '%a %b %d %H:%M:%S %z %Y')
2345 | tweet_text = tweetlist_dic[tweet_id]['full_text']
2346 | if 'retweeted_status_id_str' in tweetlist_dic[tweet_id]:
2347 | tweet_type = "转推"
2348 | # 不同于用户推特,总是有user_mentions键
2349 | elif tweetlist_dic[tweet_id]['entities']['user_mentions']:
2350 | tweet_type = "回复"
2351 | else:
2352 | tweet_type = "发布"
2353 | tweet_media = []
2354 | if 'media' in tweetlist_dic[tweet_id]['entities']:
2355 | for media in tweetlist_dic[tweet_id]['entities']['media']:
2356 | tweet_media.append(media['expanded_url'])
2357 | tweet_urls = []
2358 | if 'urls' in tweetlist_dic[tweet_id]['entities']:
2359 | for url in tweetlist_dic[tweet_id]['entities']['urls']:
2360 | tweet_urls.append(url['expanded_url'])
2361 | tweet_mention = ""
2362 | if 'user_mentions' in tweetlist_dic[tweet_id]['entities']:
2363 | for user_mention in tweetlist_dic[tweet_id]['entities']['user_mentions']:
2364 | tweet_mention += "%s\n" % user_mention['screen_name']
2365 | tweet_dic[int(tweet_id)] = {"tweet_timestamp": tweet_timestamp, "tweet_text": tweet_text,
2366 | "tweet_type": tweet_type, "tweet_media": tweet_media,
2367 | "tweet_urls": tweet_urls, "tweet_mention": tweet_mention}
2368 | return tweet_dic
2369 | except Exception as e:
2370 | raise e
2371 |
2372 |
2373 | def gettwitcastlive(user_id, cookies, proxy):
2374 | try:
2375 | live_dic = {}
2376 | url = 'https://twitcasting.tv/streamchecker.php?u=%s&v=999' % user_id
2377 | response = requests.get(url, cookies=cookies, timeout=(3, 7), proxies=proxy)
2378 | live = response.text.split("\t")
2379 | live_id = live[0]
2380 | if live_id:
2381 | live_status = "开始"
2382 | else:
2383 | live_status = "结束"
2384 | live_title = unquote(live[7])
2385 | live_dic[live_id] = {"live_status": live_status, "live_title": live_title}
2386 | return live_dic
2387 | except Exception as e:
2388 | raise e
2389 |
2390 |
2391 | '''
2392 | def gettwitcastchatlist(live_id, cookies, proxy):
2393 | try:
2394 | twitcastchatlist = []
2395 | url = 'https://twitcasting.tv/userajax.php?c=listall&m=%s&n=10&f=0k=0&format=json' % live_id
2396 | response = requests.get(url, cookies=cookies, timeout=(3, 7), proxies=proxy)
2397 | for chat in response.json()['comments']:
2398 | if chat['type'] == "comment":
2399 | chat_id = chat['id']
2400 | chat_screenname = chat['author']['screenName']
2401 | chat_name = chat['author']['name']
2402 | chat_timestamp = float(chat['createdAt']) / 1000
2403 | chat_text = chat['message']
2404 | twitcastchatlist.append(
2405 | {"chat_id": chat_id, "chat_screenname": chat_screenname, "chat_name": chat_name,
2406 | "chat_timestamp": chat_timestamp, "chat_text": chat_text})
2407 | return twitcastchatlist
2408 | except Exception as e:
2409 | raise e
2410 | '''
2411 |
2412 |
2413 | def gettwitcastchaturl(live_id, cookies, proxy):
2414 | headers = {'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary2C91D7bEcYKA0eGC'}
2415 | data = '------WebKitFormBoundary2C91D7bEcYKA0eGC\nContent-Disposition: form-data; name="movie_id"\n\n%s' % live_id
2416 | try:
2417 | response = requests.post('https://twitcasting.tv/eventpubsuburl.php', headers=headers, cookies=cookies, data=data, timeout=(3,7), proxies=proxy)
2418 | chaturl = "%s&comment=1&gift=1" % response.json()['url']
2419 | return(chaturl)
2420 | except Exception as e:
2421 | raise e
2422 |
2423 |
2424 | def getfanboxuser(user_id, proxy):
2425 | try:
2426 | headers = {
2427 | "Accept": "application/json, text/plain, */*",
2428 | "Accept-Encoding": "gzip, deflate, br",
2429 | "Accept-Language": "en-US,en;q=0.5",
2430 | "Cache-Control": "max-age=0",
2431 | "Connection": "keep-alive",
2432 | "DNT": "1",
2433 | "Host": "api.fanbox.cc",
2434 | "Origin": "https://%s.fanbox.cc" % user_id,
2435 | "Referer": "https://%s.fanbox.cc/" % user_id,
2436 | "TE": "Trailers",
2437 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0"
2438 | }
2439 | response = requests.get("https://api.fanbox.cc/creator.get?creatorId=%s" % user_id, headers=headers,
2440 | timeout=(3, 7), proxies=proxy)
2441 |
2442 | user_data = response.json()["body"]
2443 | userdata_dic = user_data
2444 | for key in user_data['user']:
2445 | userdata_dic[key] = user_data['user'][key]
2446 | userdata_dic.pop('user')
2447 |
2448 | userdata_dic.pop('isFollowed')
2449 | userdata_dic.pop('isSupported')
2450 |
2451 | return userdata_dic
2452 | except Exception as e:
2453 | raise e
2454 |
2455 |
2456 | def getfanboxpostdic(user_id, cookies, proxy):
2457 | try:
2458 | post_dic = {}
2459 | headers = {
2460 | "Accept": "application/json, text/plain, */*",
2461 | "Accept-Encoding": "gzip, deflate, br",
2462 | "Accept-Language": "en-US,en;q=0.5",
2463 | "Cache-Control": "max-age=0",
2464 | "Connection": "keep-alive",
2465 | "DNT": "1",
2466 | "Host": "api.fanbox.cc",
2467 | "Origin": "https://%s.fanbox.cc" % user_id,
2468 | "Referer": "https://%s.fanbox.cc/" % user_id,
2469 | "TE": "Trailers",
2470 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0"
2471 | }
2472 | response = requests.get("https://api.fanbox.cc/post.listCreator?creatorId=%s&limit=10" % user_id,
2473 | headers=headers, cookies=cookies, timeout=(3, 7), proxies=proxy)
2474 |
2475 | post_list = response.json()['body']['items']
2476 | for post in post_list:
2477 | post_id = post['id']
2478 | post_title = post['title']
2479 | # python3.6无法识别+00:00格式,只能识别+0000格式
2480 | try:
2481 | post_publishtimestamp = phrasetimestamp(post['publishedDatetime'], "%Y-%m-%dT%H:%M:%S%z")
2482 | except:
2483 | post_publishtimestamp = phrasetimestamp(post['publishedDatetime'].replace(':', ''), "%Y-%m-%dT%H%M%S%z")
2484 | post_type = post['type']
2485 | post_text = ""
2486 | if isinstance(post['body'], dict):
2487 | if 'text' in post['body']:
2488 | post_text = post['body']['text']
2489 | elif 'blocks' in post['body']:
2490 | for block in post['body']['blocks']:
2491 | post_text += "%s\n" % block['text']
2492 | post_fee = post['feeRequired']
2493 | post_dic[post_id] = {"post_title": post_title, "post_publishtimestamp": post_publishtimestamp,
2494 | "post_type": post_type, "post_text": post_text, "post_fee": post_fee}
2495 | return post_dic
2496 | except Exception as e:
2497 | raise e
2498 |
2499 |
2500 | def getbilibililivedic(room_id, proxy):
2501 | try:
2502 | live_dic = {}
2503 | response = requests.get("http://api.live.bilibili.com/room/v1/Room/get_info?room_id=%s" % room_id,
2504 | timeout=(3, 7), proxies=proxy)
2505 | live = response.json()['data']
2506 | try:
2507 | live_id = phrasetimestamp(live['live_time'] + " +0800", '%Y-%m-%d %H:%M:%S %z')
2508 | except:
2509 | live_id = ''
2510 | if live['live_status'] == 1:
2511 | live_status = "开始"
2512 | else:
2513 | live_status = "结束"
2514 | live_title = live['title']
2515 | live_dic[live_id] = {'live_status': live_status, 'live_title': live_title}
2516 | return live_dic
2517 | except Exception as e:
2518 | raise e
2519 |
2520 |
2521 | def getbilibilichathostlist(proxy):
2522 | hostlist = []
2523 | try:
2524 | response = requests.get("https://api.live.bilibili.com/room/v1/Danmu/getConf", proxies=proxy)
2525 | hostserver_list = response.json()['data']['host_server_list']
2526 | for hostserver in hostserver_list:
2527 | hostlist.append('wss://%s:%s/sub' % (hostserver['host'], hostserver['wss_port']))
2528 | if hostlist:
2529 | return hostlist
2530 | else:
2531 | raise Exception("Invalid hostlist")
2532 | except Exception as e:
2533 | raise e
2534 |
2535 |
2536 | def getloluser(user_name, user_region, proxy):
2537 | try:
2538 | userdata_dic = {}
2539 | response = requests.get("https://%s.op.gg/summoner/l=en_US&userName=%s" % (user_region, user_name),
2540 | timeout=(3, 7), proxies=proxy)
2541 | soup = BeautifulSoup(response.text, 'lxml')
2542 | # 用户id与时间戳
2543 | userdata_dic["user_id"] = int(soup.find(id="SummonerRefreshButton").get('onclick').split("'")[1])
2544 | userdata_dic["renew_timestamp"] = float(soup.find(class_="LastUpdate").span.get('data-datetime'))
2545 | # 比赛结果
2546 | userdata_dic["user_gamedic"] = {}
2547 | for gameitem in soup.find_all(class_='GameItemWrap'):
2548 | game_timestamp = float(gameitem.div.get('data-game-time'))
2549 | game_id = int(gameitem.div.get('data-game-id'))
2550 | game_result = gameitem.div.get('data-game-result')
2551 | game_kda = "%s/%s/%s" % (gameitem.find(class_='Kill').text, gameitem.find(class_='Death').text,
2552 | gameitem.find(class_='Assist').text)
2553 | userdata_dic["user_gamedic"][game_timestamp] = {"game_id": game_id, "game_result": game_result,
2554 | "game_kda": game_kda}
2555 |
2556 | response = requests.get("https://%s.op.gg/summoner/spectator/l=en_US&userName=%s" % (user_region, user_name),
2557 | timeout=(3, 7), proxies=proxy)
2558 | soup = BeautifulSoup(response.text, 'lxml')
2559 | # 当前游戏
2560 | current_gameitem = soup.find(class_="SpectateSummoner")
2561 | if current_gameitem:
2562 | userdata_dic["user_status"] = 'in_game'
2563 | userdata_dic["user_gametimestamp"] = float(current_gameitem.find(class_="Time").span.get("data-datetime"))
2564 | else:
2565 | userdata_dic["user_status"] = 'not_in_game'
2566 | userdata_dic["user_gametimestamp"] = False
2567 |
2568 | return userdata_dic
2569 | except Exception as e:
2570 | raise e
2571 |
2572 |
2573 | def renewloluser(user_id, user_region, proxy):
2574 | try:
2575 | headers = {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
2576 | "X-Requested-With": "XMLHttpRequest"}
2577 | data = "summonerId=%s" % user_id
2578 | response = requests.post("https://%s.op.gg/summoner/ajax/renew.json/" % user_region, headers=headers, data=data,
2579 | timeout=(3, 7), proxies=proxy)
2580 | if response.status_code != 200:
2581 | raise Exception("Refresh failed")
2582 | except Exception as e:
2583 | raise e
2584 |
2585 |
2586 | def getsteamuser(user_id, cookies, proxy):
2587 | try:
2588 | userdata_dic = {}
2589 | response = requests.get("https://steamcommunity.com/profiles/%s" % user_id, cookies=cookies, timeout=(3, 7),
2590 | proxies=proxy)
2591 | soup = BeautifulSoup(response.text, 'lxml')
2592 | if not soup.find(class_="profile_private_info"):
2593 | userdata_dic["user_position"] = soup.find(class_="header_real_name ellipsis").text.strip()
2594 | userdata_dic["user_level"] = soup.find(class_="friendPlayerLevelNum").text.strip()
2595 | userdata_dic["user_status"] = soup.find(class_="profile_in_game_header").text.strip()
2596 | userdata_dic["user_avatar"] = soup.find(class_="playerAvatarAutoSizeInner").img['src']
2597 | for item_count in soup.find_all(class_="profile_count_link ellipsis"):
2598 | userdata_dic["user_" + item_count.find(class_="count_link_label").text.strip()] = item_count.find(
2599 | class_="profile_count_link_total").text.strip()
2600 | return userdata_dic
2601 | except Exception as e:
2602 | raise e
2603 |
2604 |
2605 | def getosuuser(user_id, cookies, proxy):
2606 | try:
2607 | response = requests.get('https://osu.ppy.sh/users/%s' % user_id, cookies=cookies, timeout=(3, 7), proxies=proxy)
2608 | soup = BeautifulSoup(response.text, 'lxml')
2609 | user_data = json.loads(soup.find(attrs={'id': 'json-user', 'type': 'application/json'}).text)
2610 | userdata_dic = user_data
2611 | for key in user_data['statistics']:
2612 | userdata_dic[key] = user_data['statistics'][key]
2613 | userdata_dic.pop('statistics')
2614 |
2615 | userdata_dic.pop('follower_count')
2616 | userdata_dic.pop('rank')
2617 | userdata_dic.pop('global_rank')
2618 | userdata_dic.pop('ranked_score')
2619 | userdata_dic.pop('country_rank')
2620 | userdata_dic.pop('rank_history')
2621 | userdata_dic.pop('rankHistory')
2622 | userdata_dic.pop('last_visit')
2623 |
2624 | # 比赛结果
2625 | userdata_dic["user_gamedic"] = {}
2626 | gamelist = json.loads(soup.find(attrs={'id': 'json-extras', 'type': 'application/json'}).text)['recentActivity']
2627 | for gameitem in gamelist:
2628 | game_id = gameitem['id']
2629 | # python3.6无法识别+00:00格式,只能识别+0000格式
2630 | try:
2631 | game_timestamp = phrasetimestamp(gameitem['createdAt'], "%Y-%m-%dT%H:%M:%S%z")
2632 | except:
2633 | game_timestamp = phrasetimestamp(gameitem['createdAt'].replace(':', ''), "%Y-%m-%dT%H%M%S%z")
2634 | game_type = gameitem['type']
2635 | try:
2636 | game_result = "%s - %s(%s) - %s(https://osu.ppy.sh/%s)" % (
2637 | gameitem['mode'], gameitem['scoreRank'], gameitem['rank'], gameitem['beatmap']['title'],
2638 | gameitem['beatmap']['url'])
2639 | except:
2640 | game_result = ''
2641 | userdata_dic["user_gamedic"][game_id] = {"game_timestamp": game_timestamp, "game_type": game_type,
2642 | "game_result": game_result}
2643 | return userdata_dic
2644 | except Exception as e:
2645 | raise e
2646 |
2647 |
2648 | # 检测推送力度
2649 | def getpushcolordic(text, dic):
2650 | pushcolor_dic = {}
2651 | for word in dic.keys():
2652 | if text.count(word) > 0:
2653 | for color in dic[word]:
2654 | if color in pushcolor_dic:
2655 | pushcolor_dic[color] += int(dic[word][color])
2656 | else:
2657 | pushcolor_dic[color] = int(dic[word][color])
2658 | return pushcolor_dic
2659 |
2660 |
2661 | # 求和推送力度,注意传入subdics必须为tuple类型
2662 | def addpushcolordic(*adddics, **kwargs):
2663 | pushcolor_dic = {}
2664 | for adddic in adddics:
2665 | for color in adddic.keys():
2666 | if color in pushcolor_dic:
2667 | pushcolor_dic[color] += adddic[color]
2668 | else:
2669 | pushcolor_dic[color] = adddic[color]
2670 | if "subdics" in kwargs:
2671 | for subdic in kwargs["subdics"]:
2672 | for color in subdic.keys():
2673 | if color in pushcolor_dic:
2674 | pushcolor_dic[color] -= subdic[color]
2675 | else:
2676 | pushcolor_dic[color] = -subdic[color]
2677 | return pushcolor_dic
2678 |
2679 |
2680 | # 查询或修改暂停力度
2681 | def checkpause(pause_json, type, id, pausepower=None):
2682 | is_inpause = None
2683 | for i in range(len(pause_json)):
2684 | if pause_json[i]['type'] == type and pause_json[i]['id'] == id:
2685 | is_inpause = i
2686 |
2687 | if pausepower is not None:
2688 | # 修改
2689 | if is_inpause is not None:
2690 | pause_json[is_inpause]['pausepower'] = pausepower
2691 | return pause_json
2692 | else:
2693 | pause_json.append({'type': type, 'id': id, 'pausepower': pausepower})
2694 | return pause_json
2695 | else:
2696 | # 查询
2697 | if is_inpause is not None:
2698 | return pause_json[is_inpause]['pausepower']
2699 | else:
2700 | return None
2701 |
2702 |
2703 | # 判断是否推送
2704 | def pushall(pushtext, pushcolor_dic, push_list):
2705 | with open('./pause.json', 'r', encoding='utf-8') as f:
2706 | pause_json = json.load(f)
2707 | for push in push_list:
2708 | pausepower = checkpause(pause_json, push['type'], push['id'])
2709 | if pausepower is None:
2710 | pausepower = 0
2711 | for color in push["color_dic"]:
2712 | if color in pushcolor_dic:
2713 | if int(pushcolor_dic[color]) - int(pausepower) >= int(push["color_dic"][color]):
2714 | push_thread = threading.Thread(args=(pushtext, push), target=pushtoall)
2715 | push_thread.start()
2716 | break
2717 |
2718 |
2719 | # 推送
2720 | def pushtoall(pushtext, push):
2721 | if 'proxy' not in push:
2722 | push['proxy'] = ''
2723 | # 不论windows还是linux都是127.0.0.1
2724 | if push['type'] == 'qq_user':
2725 | if 'ip' not in push:
2726 | push['ip'] = '127.0.0.1'
2727 | url = 'http://%s:%s/send_private_msg?user_id=%s&message=%s' % (
2728 | push['ip'], push['port'], push['id'], quote(str(pushtext)))
2729 | pushtourl('GET', url, push['proxy'])
2730 | elif push['type'] == 'qq_group':
2731 | if 'ip' not in push:
2732 | push['ip'] = '127.0.0.1'
2733 | url = 'http://%s:%s/send_group_msg?group_id=%s&message=%s' % (
2734 | push['ip'], push['port'], push['id'], quote(str(pushtext)))
2735 | pushtourl('GET', url, push['proxy'])
2736 | elif push['type'] == 'miaotixing':
2737 | # 带文字推送可能导致语音和短信提醒失效
2738 | url = 'https://miaotixing.com/trigger?id=%s&text=%s' % (push['id'], quote(str(pushtext)))
2739 | pushtourl('POST', url, push['proxy'])
2740 | elif push['type'] == 'miaotixing_simple':
2741 | url = 'https://miaotixing.com/trigger?id=%s' % push['id']
2742 | pushtourl('POST', url, push['proxy'])
2743 | elif push['type'] == 'discord':
2744 | url = push['id']
2745 | headers = {"Content-Type": "application/json"}
2746 | data = {"content": pushtext}
2747 | pushtourl('POST', url, push['proxy'], headers, json.dumps(data))
2748 | elif push['type'] == 'telegram':
2749 | url = 'https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s' % (
2750 | push['bot_id'], push['id'], quote(str(pushtext)))
2751 | pushtourl('POST', url, push['proxy'])
2752 |
2753 |
2754 | # 推送到url
2755 | def pushtourl(method, url, proxy, headers=None, data=None):
2756 | if data is None:
2757 | data = {}
2758 | if headers is None:
2759 | headers = {}
2760 | for retry in range(1, 5):
2761 | status_code = 'fail'
2762 | try:
2763 | if method == 'GET':
2764 | response = requests.get(url, headers=headers, timeout=(10, 30), proxies=proxy)
2765 | elif method == 'POST':
2766 | response = requests.post(url, headers=headers, data=data, timeout=(10, 30), proxies=proxy)
2767 | status_code = response.status_code
2768 | except:
2769 | time.sleep(5)
2770 | finally:
2771 | printlog('[Info] pushtourl:第%s次-结果%s (%s proxy:%s)' % (retry, status_code, url, proxy))
2772 | if status_code == 200 or status_code == 204:
2773 | break
2774 |
2775 |
2776 | def phrasetimestamp(text, timeformat):
2777 | return datetime.datetime.strptime(text, timeformat).timestamp()
2778 |
2779 |
2780 | def getutctimestamp():
2781 | return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).timestamp()
2782 |
2783 |
2784 | # log文件时间时区由hs默认值指定
2785 | def formattime(ts=None, hs=8):
2786 | if ts:
2787 | return datetime.datetime.utcfromtimestamp(float(ts)).replace(tzinfo=datetime.timezone.utc).astimezone(
2788 | tz=datetime.timezone(datetime.timedelta(hours=hs))).strftime("%Y-%m-%d %H:%M:%S %Z")
2789 | else:
2790 | return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone(
2791 | tz=datetime.timezone(datetime.timedelta(hours=hs))).strftime("%Y-%m-%d %H:%M:%S %Z")
2792 |
2793 |
2794 | def printlog(text):
2795 | print("[%s] %s" % (formattime(), text))
2796 |
2797 |
2798 | def writelog(logpath, text):
2799 | with open(logpath, 'a', encoding='utf-8') as log:
2800 | log.write("[%s] %s\n" % (formattime(), text))
2801 |
2802 |
2803 | '''
2804 | def waittime(timestamp):
2805 | return second_to_time(int(float(timestamp)) - formattime())
2806 |
2807 |
2808 | def second_to_time(seconds):
2809 | d = int(seconds / 86400)
2810 | seconds = seconds - d * 86400
2811 | h = int(seconds / 3600)
2812 | seconds = seconds - h * 3600
2813 | m = int(seconds / 60)
2814 | s = seconds - m * 60
2815 |
2816 | if d == 0:
2817 | if h == 0:
2818 | return "%s分%s秒" % (m, s)
2819 | else:
2820 | return "%s小时%s分%s秒" % (h, m, s)
2821 | else:
2822 | return "%s天%s小时%s分%s秒" % (d, h, m, s)
2823 | '''
2824 |
2825 |
2826 | def createmonitor(monitor_name, config):
2827 | monitor_class = config["submonitor_dic"][monitor_name]["class"]
2828 | monitor_target = config["submonitor_dic"][monitor_name]["target"]
2829 | monitor_target_name = config["submonitor_dic"][monitor_name]["target_name"]
2830 | monitor_config = config[config["submonitor_dic"][monitor_name]["config_name"]]
2831 | monitor_config_mod = {}
2832 | for key in config["submonitor_dic"][monitor_name].keys():
2833 | if key != "class" and key != "target" and key != "target_name" and key != "config_name":
2834 | monitor_config_mod[key] = config["submonitor_dic"][monitor_name][key]
2835 | monitor_thread = globals()[monitor_class](monitor_name, monitor_target, monitor_target_name, monitor_config,
2836 | **monitor_config_mod)
2837 | monitor_thread.start()
2838 | return monitor_thread
2839 |
2840 |
2841 | if __name__ == '__main__':
2842 | os.chdir(os.path.dirname(os.path.abspath(__file__)))
2843 |
2844 | if not os.path.exists('./log'):
2845 | os.makedirs('./log')
2846 | if not os.path.isfile('./pause.json'):
2847 | with open('./pause.json', 'w', encoding='utf-8') as f:
2848 | json.dump([], f)
2849 |
2850 | # 读取配置文件
2851 | config_name = input('默认为spider,不用输入json后缀名\n请输入配置文件名称:')
2852 | while True:
2853 | config_path = './%s.json' % (str(config_name))
2854 | if not config_name:
2855 | config_path = './spider.json'
2856 | break
2857 | if os.path.exists(config_path):
2858 | break
2859 | else:
2860 | config_name = input('该配置文件不存在,请重新输入:')
2861 | with open(config_path, 'r', encoding='utf-8') as f:
2862 | config = json.load(f)
2863 |
2864 | # 启动并监视主监视器
2865 | monitor = Monitor("主线程", "main", "main", config)
2866 | monitor.Daemon = True
2867 | monitor.start()
2868 | while True:
2869 | time.sleep(30)
2870 | monitor.checksubmonitor()
2871 |
--------------------------------------------------------------------------------