├── 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 | --------------------------------------------------------------------------------