From afc1303f1190fe41641fc0e9b3a5d6072a5c0f58 Mon Sep 17 00:00:00 2001 From: Muihd <1083092844@qq.com> Date: Wed, 30 Apr 2025 22:53:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=20python=20=E5=92=8C=20node?= =?UTF-8?q?=20Demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Demos/NodeJS/entities.js | 256 +++++++++++++++++++++++++++ Demos/NodeJS/main.js | 312 +++++++++++++++++++++++++++++++++ Demos/NodeJS/package-lock.json | 100 +++++++++++ Demos/NodeJS/package.json | 13 ++ Demos/NodeJS/parsers.js | 215 +++++++++++++++++++++++ Demos/Python/LogWindow.py | 291 ------------------------------ Demos/Python/entities.py | 167 ++++++++++++++++++ Demos/Python/main.py | 245 ++++++++++++++++++++++++++ Demos/Python/parsers.py | 158 +++++++++++++++++ Demos/Python/requirements.txt | 2 + Demos/Python/ws.py | 89 ---------- 11 files changed, 1468 insertions(+), 380 deletions(-) create mode 100644 Demos/NodeJS/entities.js create mode 100644 Demos/NodeJS/main.js create mode 100644 Demos/NodeJS/package-lock.json create mode 100644 Demos/NodeJS/package.json create mode 100644 Demos/NodeJS/parsers.js delete mode 100644 Demos/Python/LogWindow.py create mode 100644 Demos/Python/entities.py create mode 100644 Demos/Python/main.py create mode 100644 Demos/Python/parsers.py create mode 100644 Demos/Python/requirements.txt delete mode 100644 Demos/Python/ws.py diff --git a/Demos/NodeJS/entities.js b/Demos/NodeJS/entities.js new file mode 100644 index 0000000..7c7ddc6 --- /dev/null +++ b/Demos/NodeJS/entities.js @@ -0,0 +1,256 @@ +/** + * 消息类型枚举 + */ +const PackMsgType = { + 无: 0, + 弹幕消息: 1, + 点赞消息: 2, + 进直播间: 3, + 关注消息: 4, + 礼物消息: 5, + 直播间统计: 6, + 粉丝团消息: 7, + 直播间分享: 8, + 下播: 9 +}; + +/** + * 性别枚举 + */ +const Gender = { + 未知: 0, + 男: 1, + 女: 2, + + /** + * 将性别枚举转换为中文字符串 + * @param {number} gender - 性别枚举值 + * @returns {string} 性别的中文表示 + */ + toString(gender) { + switch(gender) { + case this.男: + return "男"; + case this.女: + return "女"; + default: + return "妖"; + } + } +}; + +/** + * 粉丝团消息类型 + */ +const FansclubType = { + 无: 0, + 粉丝团升级: 1, + 加入粉丝团: 2 +}; + +/** + * 直播间分享目标 + */ +const ShareType = { + 未知: 0, + 微信: 1, + 朋友圈: 2, + 微博: 3, + QQ空间: 4, + QQ: 5, + 抖音好友: 112 +}; + +/** + * 观众的进入方式 + */ +const EnterType = { + 正常进入: 0, + 通过分享进入: 6 +}; + +/** + * 粉丝团信息类 + */ +class FansClubInfo { + constructor(data = {}) { + this.ClubName = data.ClubName || ""; + this.Level = data.Level || 0; + } +} + +/** + * 用户信息类 + */ +class MsgUser { + constructor(data = {}) { + this.Id = data.Id || 0; + this.IsAdmin = data.IsAdmin || false; + this.IsAnchor = data.IsAnchor || false; + this.ShortId = data.ShortId || 0; + this.DisplayId = data.DisplayId || ""; + this.Nickname = data.Nickname || "匿名用户"; + this.Level = data.Level || 0; + this.PayLevel = data.PayLevel || 0; + this.Gender = data.Gender || Gender.未知; + this.HeadImgUrl = data.HeadImgUrl || ""; + this.SecUid = data.SecUid || ""; + this.FansClub = null; + this.FollowerCount = data.FollowerCount || 0; + this.FollowStatus = data.FollowStatus || 0; + this.FollowingCount = data.FollowingCount || 0; + } + + /** + * 返回性别的中文表示 + * @returns {string} 性别的中文表示 + */ + genderToString() { + return Gender.toString(this.Gender); + } +} + +/** + * 直播间主播信息类 + */ +class RoomAnchorInfo { + constructor(data = {}) { + this.UserId = data.UserId || ""; + this.SecUid = data.SecUid || ""; + this.Nickname = data.Nickname || ""; + this.HeadUrl = data.HeadUrl || ""; + this.FollowStatus = data.FollowStatus || 0; + } +} + +/** + * 基础消息类 + */ +class Msg { + constructor(data = {}) { + this.MsgId = data.MsgId || 0; + this.User = new MsgUser(); + this.Owner = null; + this.Content = data.Content || ""; + this.RoomId = data.RoomId || ""; + this.WebRoomId = data.WebRoomId || ""; + this.Appid = data.Appid || ""; + } +} + +/** + * 点赞消息类 + */ +class LikeMsg extends Msg { + constructor(data = {}) { + super(data); + this.Count = data.Count || 0; + this.Total = data.Total || 0; + } +} + +/** + * 礼物消息类 + */ +class GiftMsg extends Msg { + constructor(data = {}) { + super(data); + this.GiftId = data.GiftId || 0; + this.GiftName = data.GiftName || ""; + this.GroupId = data.GroupId || 0; + this.GiftCount = data.GiftCount || 0; + this.RepeatCount = data.RepeatCount || 0; + this.DiamondCount = data.DiamondCount || 0; // 抖币价格 + this.Combo = data.Combo || false; // 该礼物是否可连击 + this.ImgUrl = data.ImgUrl || ""; // 礼物图片地址 + this.ToUser = null; // 送礼目标(连麦直播间有用) + this.GiftValue = data.GiftValue || 0; // 在原Python代码中使用的礼物价值字段 + } +} + +/** + * 进入直播间消息类 + */ +class MemberMessage extends Msg { + constructor(data = {}) { + super(data); + this.CurrentCount = data.CurrentCount || 0; + this.EnterTipType = data.EnterTipType || 0; + } +} + +/** + * 直播间统计消息类 + */ +class UserSeqMsg extends Msg { + constructor(data = {}) { + super(data); + this.OnlineUserCount = data.OnlineUserCount || 0; + this.TotalUserCount = data.TotalUserCount || 0; + this.TotalUserCountStr = data.TotalUserCountStr || "0"; + this.OnlineUserCountStr = data.OnlineUserCountStr || "0"; + } +} + +/** + * 粉丝团消息类 + */ +class FansclubMsg extends Msg { + constructor(data = {}) { + super(data); + this.Type = data.Type || 0; // 粉丝团消息类型,升级1,加入2 + this.Level = data.Level || 0; // 粉丝团等级 + this.FansClubName = data.FansClubName || ""; // 粉丝团名称 + } +} + +/** + * 直播间分享类 + */ +class ShareMessage extends Msg { + constructor(data = {}) { + super(data); + this.ShareType = data.ShareType || ShareType.未知; + } +} + +/** + * 直播间统计数据类 + */ +class LiveStats { + constructor() { + this.total_likes = 0; + this.total_users = 0; + this.male_users = 0; + this.female_users = 0; + } + + /** + * 重置统计数据 + */ + reset() { + this.total_likes = 0; + this.total_users = 0; + this.male_users = 0; + this.female_users = 0; + } +} + +module.exports = { + PackMsgType, + Gender, + FansclubType, + ShareType, + EnterType, + FansClubInfo, + MsgUser, + RoomAnchorInfo, + Msg, + LikeMsg, + GiftMsg, + MemberMessage, + UserSeqMsg, + FansclubMsg, + ShareMessage, + LiveStats +}; \ No newline at end of file diff --git a/Demos/NodeJS/main.js b/Demos/NodeJS/main.js new file mode 100644 index 0000000..b62902c --- /dev/null +++ b/Demos/NodeJS/main.js @@ -0,0 +1,312 @@ +/** + * 抖音直播间消息处理脚本 + * 消息类型说明: + * 1[普通弹幕]|2[点赞消息]|3[进入直播间]|4[关注消息]|5[礼物消息]|6[统计消息]|7[粉丝团消息]|8[直播间分享]|9[下播] + * + * 全局统计变量: + * Live_Total - 本场点赞数 + * Live_user - 本场总观看人数 + * Live_man - 本场男生人数 + * Live_woman - 本场女生人数 + */ + +const WebSocket = require('ws'); +const chalk = require('chalk'); +const { PackMsgType, Gender, LiveStats } = require('./entities'); +const { MessageParser } = require('./parsers'); + +// 颜色定义 +const COLOR_MAP = { + [PackMsgType.弹幕消息]: chalk.cyan, // 青色 + [PackMsgType.点赞消息]: chalk.yellow, // 黄色 + [PackMsgType.进直播间]: chalk.green, // 绿色 + [PackMsgType.关注消息]: chalk.magenta, // 洋红色 + [PackMsgType.礼物消息]: chalk.red, // 红色 + [PackMsgType.直播间统计]: chalk.blue, // 蓝色 + [PackMsgType.粉丝团消息]: chalk.blue.bright, // 浅蓝色 + [PackMsgType.直播间分享]: chalk.magenta.bright, // 浅洋红色 + [PackMsgType.下播]: chalk.red, // 红色 +}; + +// 初始化统计对象 +const liveStats = new LiveStats(); + +/** + * 返回当前时间戳,格式:HH:MM:SS + * @returns {string} 格式化的时间戳 + */ +function getTimestamp() { + const now = new Date(); + return now.toTimeString().split(' ')[0]; +} + +/** + * 打印带颜色的消息 + * @param {number} msgType - 消息类型 + * @param {string} content - 消息内容 + * @param {string} [author="未知"] - 消息来源作者 + */ +function printColoredMessage(msgType, content, author = "未知") { + const colorFunc = COLOR_MAP[msgType] || chalk.white; + const timestamp = getTimestamp(); + const authorStr = `[${author}]`; // 使用作者信息 + console.log(colorFunc(`${timestamp} ${authorStr} ${content}`)); +} + +/** + * 统一获取消息所属的主播/房间信息 + * @param {Object} msg - 解析后的消息对象 + * @returns {string} 主播昵称或房间ID + */ +function getMsgOwner(msg) { + if (msg && msg.Owner && msg.Owner.Nickname) { + return msg.Owner.Nickname; + } else if (msg && msg.WebRoomId) { + return msg.WebRoomId; + } else if (msg && msg.RoomId) { + return msg.RoomId; + } + return "未知"; +} + +/** + * 处理用户发送的弹幕消息 + * @param {Object} dataDict - 弹幕消息数据 + */ +function handleDanmaku(dataDict) { + const msg = MessageParser.parseDanmaku(dataDict); + const genderStr = msg.User.genderToString(); + const content = `[弹幕消息] [${genderStr}] ${msg.User.Nickname}: ${msg.Content}`; + const author = getMsgOwner(msg); + printColoredMessage(PackMsgType.弹幕消息, content, author); +} + +/** + * 处理用户点赞消息并更新全局统计 + * @param {Object} dataDict - 点赞消息数据 + */ +function handleDianzanku(dataDict) { + const likeMsg = MessageParser.parseLike(dataDict); + liveStats.total_likes = likeMsg.Total; + const genderStr = likeMsg.User.genderToString(); + const content = `[点赞消息] [${genderStr}] ${likeMsg.User.Nickname} 为主播点了${likeMsg.Count}个赞, 总点赞${likeMsg.Total}`; + const author = getMsgOwner(likeMsg); + printColoredMessage(PackMsgType.点赞消息, content, author); +} + +/** + * 处理用户进入直播间的消息,并根据性别更新统计 + * @param {Object} dataDict - 进入直播间消息数据 + */ +function handleUserentry(dataDict) { + const memberMsg = MessageParser.parseMember(dataDict); + + // 根据性别更新全局计数 + if (memberMsg.User.Gender === Gender.男) { + liveStats.male_users += 1; + } else if (memberMsg.User.Gender === Gender.女) { + liveStats.female_users += 1; + } + + const genderStr = memberMsg.User.genderToString(); + const content = `[进直播间] [${genderStr}] ${memberMsg.User.Nickname} $来了 直播间人数:${memberMsg.CurrentCount}`; + const author = getMsgOwner(memberMsg); + printColoredMessage(PackMsgType.进直播间, content, author); +} + +/** + * 处理用户关注主播的消息 + * @param {Object} dataDict - 关注消息数据 + */ +function handleFollow(dataDict) { + try { + const userNickname = dataDict.User?.Nickname || "匿名用户"; + const gender = dataDict.User?.Gender || 0; + const genderStr = Gender.toString(gender); + const content = `[关注消息] [${genderStr}] ${userNickname} 关注了主播`; + const author = getMsgOwner(dataDict); + printColoredMessage(PackMsgType.关注消息, content, author); + } catch (e) { + console.log(`处理关注消息出错: ${e.message}`); + } +} + +/** + * 处理用户赠送礼物的消息 + * @param {Object} dataDict - 礼物消息数据 + */ +function handleGift(dataDict) { + try { + const userNickname = dataDict.User?.Nickname || "匿名用户"; + const gender = dataDict.User?.Gender || 0; + const genderStr = Gender.toString(gender); + const giftName = dataDict.GiftName || "未知礼物"; + const giftCount = dataDict.GiftCount || 1; + const giftValue = dataDict.GiftValue || 0; + const content = `[礼物消息] [${genderStr}] ${userNickname} 送出了 ${giftCount}个${giftName}, 价值${giftValue}抖币`; + const author = getMsgOwner(dataDict); + printColoredMessage(PackMsgType.礼物消息, content, author); + } catch (e) { + console.log(`处理礼物消息出错: ${e.message}`); + } +} + +/** + * 处理直播间统计信息 + * @param {Object} dataDict - 统计消息数据 + */ +function handleStatistics(dataDict) { + const statsMsg = MessageParser.parseStatistics(dataDict); + liveStats.total_users = parseInt(statsMsg.TotalUserCount); + const content = `[直播间统计] 当前直播间人数 ${statsMsg.OnlineUserCountStr}, 累计直播间人数 ${statsMsg.TotalUserCountStr}万`; + const author = getMsgOwner(statsMsg); + printColoredMessage(PackMsgType.直播间统计, content, author); +} + +/** + * 处理粉丝团相关消息 + * @param {Object} dataDict - 粉丝团消息数据 + */ +function handleFansclub(dataDict) { + try { + const userNickname = dataDict.User?.Nickname || "匿名用户"; + const gender = dataDict.User?.Gender || 0; + const genderStr = Gender.toString(gender); + const clubName = dataDict.FansClubName || ""; + const content = `[粉丝团消息] [${genderStr}] ${userNickname} 加入了${clubName}粉丝团`; + const author = getMsgOwner(dataDict); + printColoredMessage(PackMsgType.粉丝团消息, content, author); + } catch (e) { + console.log(`处理粉丝团消息出错: ${e.message}`); + } +} + +/** + * 处理用户分享直播间的消息 + * @param {Object} dataDict - 分享消息数据 + */ +function handleShare(dataDict) { + try { + const userNickname = dataDict.User?.Nickname || "匿名用户"; + const gender = dataDict.User?.Gender || 0; + const genderStr = Gender.toString(gender); + const content = `[直播间分享] [${genderStr}] ${userNickname} 分享了直播间`; + const author = getMsgOwner(dataDict); + printColoredMessage(PackMsgType.直播间分享, content, author); + } catch (e) { + console.log(`处理分享消息出错: ${e.message}`); + } +} + +/** + * 处理直播结束事件,显示并重置统计数据 + */ +function handleLiveExit() { + const content = `[下播] 直播结束:累计观看 ${liveStats.total_users} 人,累计点赞 ${liveStats.total_likes},` + + `男生 ${liveStats.male_users} 女生 ${liveStats.female_users}`; + printColoredMessage(PackMsgType.下播, content, "系统消息"); + liveStats.reset(); + console.log(`直播统计数据已清空~`); +} + +/** + * 处理从WebSocket接收到的单条消息 + * @param {string} message - WebSocket接收到的消息 + */ +function processMessage(message) { + try { + const data = JSON.parse(message); + + if (!("Type" in data)) { + console.log("⚠️ 收到无效消息:缺少Type字段"); + return; + } + + const msgType = data.Type; + + // 检查是否需要处理Data字段 + if (msgType === PackMsgType.下播) { + handleLiveExit(); + return; + } + + if (!("Data" in data)) { + console.log(`⚠️ 消息类型 ${Object.keys(PackMsgType).find(key => PackMsgType[key] === msgType)} 缺少Data字段`); + return; + } + + try { + const dataDict = JSON.parse(data.Data); + + // 消息处理映射表 + const handlers = { + [PackMsgType.弹幕消息]: handleDanmaku, + [PackMsgType.点赞消息]: handleDianzanku, + [PackMsgType.进直播间]: handleUserentry, + [PackMsgType.关注消息]: handleFollow, + [PackMsgType.礼物消息]: handleGift, + [PackMsgType.直播间统计]: handleStatistics, + [PackMsgType.粉丝团消息]: handleFansclub, + [PackMsgType.直播间分享]: handleShare, + }; + + // 分发消息到相应处理器 + if (handlers[msgType]) { + handlers[msgType](dataDict); + } + + } catch (e) { + if (e instanceof SyntaxError) { + console.log(`⚠️ 无法解析Data字段内容:${data.Data.substring(0, 100)}...`); + } else { + throw e; + } + } + + } catch (e) { + if (e instanceof SyntaxError) { + console.log(`⚠️ 无效的JSON格式: ${message.substring(0, 100)}...`); + } else { + console.log(`❌ 消息处理错误: ${e.message}`); + } + } +} + +/** + * 连接到WebSocket服务器并处理接收到的消息 + */ +function connectAndPrint() { + const uri = "ws://127.0.0.1:8888"; + let reconnectInterval = 5000; // 重连间隔,单位毫秒 + + console.log(`正在连接到服务器 ${uri}...`); + + function connect() { + const ws = new WebSocket(uri); + + ws.on('open', function open() { + console.log(`✅ 已成功连接到服务器 ${uri}`); + }); + + ws.on('message', function incoming(message) { + processMessage(message.toString()); + }); + + ws.on('close', function close() { + console.log(`⚠️ 与服务器的连接已关闭`); + console.log(`正在尝试重新连接...`); + setTimeout(connect, reconnectInterval); + }); + + ws.on('error', function error(err) { + console.log(`❌ 发生错误: ${err.message}`); + // 错误处理由 close 事件处理重连 + }); + } + + connect(); +} + +// 程序入口 +console.log("启动抖音直播消息监听..."); +connectAndPrint(); \ No newline at end of file diff --git a/Demos/NodeJS/package-lock.json b/Demos/NodeJS/package-lock.json new file mode 100644 index 0000000..d24d213 --- /dev/null +++ b/Demos/NodeJS/package-lock.json @@ -0,0 +1,100 @@ +{ + "name": "dy-barrage-grab-nodejs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dy-barrage-grab-nodejs", + "version": "1.0.0", + "dependencies": { + "chalk": "^4.1.2", + "ws": "^8.13.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/Demos/NodeJS/package.json b/Demos/NodeJS/package.json new file mode 100644 index 0000000..dd3f606 --- /dev/null +++ b/Demos/NodeJS/package.json @@ -0,0 +1,13 @@ +{ + "name": "dy-barrage-grab-nodejs", + "version": "1.0.0", + "description": "抖音直播弹幕获取工具 - Node.js版本", + "main": "main.js", + "scripts": { + "start": "node main.js" + }, + "dependencies": { + "ws": "^8.13.0", + "chalk": "^4.1.2" + } +} \ No newline at end of file diff --git a/Demos/NodeJS/parsers.js b/Demos/NodeJS/parsers.js new file mode 100644 index 0000000..8f7739d --- /dev/null +++ b/Demos/NodeJS/parsers.js @@ -0,0 +1,215 @@ +const { + MsgUser, FansClubInfo, Msg, LikeMsg, MemberMessage, UserSeqMsg, + Gender, GiftMsg, FansclubMsg, ShareMessage, RoomAnchorInfo, ShareType +} = require('./entities'); + +/** + * 消息解析器类 + */ +class MessageParser { + /** + * 解析用户数据 + * @param {Object} userData - 用户数据对象 + * @returns {MsgUser} - 解析后的用户对象 + */ + static parseUser(userData) { + const user = new MsgUser(); + if (!userData) { + return user; + } + + user.Id = userData.Id || 0; + user.IsAdmin = userData.IsAdmin || false; + user.IsAnchor = userData.IsAnchor || false; + user.ShortId = userData.ShortId || 0; + user.DisplayId = userData.DisplayId || ""; + user.Nickname = userData.Nickname || "匿名用户"; + user.Level = userData.Level || 0; + user.PayLevel = userData.PayLevel || 0; + user.Gender = userData.Gender || 0; + user.HeadImgUrl = userData.HeadImgUrl || ""; + user.SecUid = userData.SecUid || ""; + user.FollowerCount = userData.FollowerCount || 0; + user.FollowStatus = userData.FollowStatus || 0; + user.FollowingCount = userData.FollowingCount || 0; + + // 解析粉丝团信息 + if (userData.FansClub) { + const fansClubData = userData.FansClub; + user.FansClub = new FansClubInfo({ + ClubName: fansClubData.ClubName || "", + Level: fansClubData.Level || 0 + }); + } + + return user; + } + + /** + * 解析主播信息 + * @param {Object} ownerData - 主播数据对象 + * @returns {RoomAnchorInfo|null} - 解析后的主播信息对象或null + */ + static parseAnchorInfo(ownerData) { + if (!ownerData) { + return null; + } + + const owner = new RoomAnchorInfo({ + UserId: ownerData.UserId || "", + SecUid: ownerData.SecUid || "", + Nickname: ownerData.Nickname || "", + HeadUrl: ownerData.HeadUrl || "", + FollowStatus: ownerData.FollowStatus || 0 + }); + + return owner; + } + + /** + * 解析通用消息字段 + * @param {Object} data - 消息数据对象 + * @param {Msg} msg - 消息对象 + * @returns {Msg} - 解析后的消息对象 + */ + static parseCommonMsgFields(data, msg) { + msg.MsgId = data.MsgId || 0; + msg.Content = data.Content || ""; + msg.RoomId = data.RoomId || ""; + msg.WebRoomId = data.WebRoomId || ""; + msg.Appid = data.Appid || ""; + + // 解析用户数据 + if (data.User) { + msg.User = MessageParser.parseUser(data.User); + } + + // 解析主播数据,优先使用正确拼写的"Owner" + if (data.Owner) { + msg.Owner = MessageParser.parseAnchorInfo(data.Owner); + } + // 如果没有"Owner"字段才考虑拼写错误的"Onwer"字段,并且确保不是字符串 + else if (data.Onwer && typeof data.Onwer !== 'string') { + msg.Owner = MessageParser.parseAnchorInfo(data.Onwer); + } + + return msg; + } + + /** + * 解析弹幕消息 + * @param {Object} data - 弹幕消息数据 + * @returns {Msg} - 解析后的弹幕消息对象 + */ + static parseDanmaku(data) { + const msg = new Msg(); + return MessageParser.parseCommonMsgFields(data, msg); + } + + /** + * 解析点赞消息 + * @param {Object} data - 点赞消息数据 + * @returns {LikeMsg} - 解析后的点赞消息对象 + */ + static parseLike(data) { + const likeMsg = new LikeMsg(); + MessageParser.parseCommonMsgFields(data, likeMsg); + likeMsg.Count = data.Count || 0; + likeMsg.Total = data.Total || 0; + return likeMsg; + } + + /** + * 解析进入直播间消息 + * @param {Object} data - 进入直播间消息数据 + * @returns {MemberMessage} - 解析后的进入直播间消息对象 + */ + static parseMember(data) { + const memberMsg = new MemberMessage(); + MessageParser.parseCommonMsgFields(data, memberMsg); + memberMsg.CurrentCount = data.CurrentCount || 0; + memberMsg.EnterTipType = data.EnterTipType || 0; + return memberMsg; + } + + /** + * 解析礼物消息 + * @param {Object} data - 礼物消息数据 + * @returns {GiftMsg} - 解析后的礼物消息对象 + */ + static parseGift(data) { + const giftMsg = new GiftMsg(); + MessageParser.parseCommonMsgFields(data, giftMsg); + giftMsg.GiftId = data.GiftId || 0; + giftMsg.GiftName = data.GiftName || ""; + giftMsg.GroupId = data.GroupId || 0; + giftMsg.GiftCount = data.GiftCount || 0; + giftMsg.RepeatCount = data.RepeatCount || 0; + giftMsg.DiamondCount = data.DiamondCount || 0; + giftMsg.GiftValue = data.GiftValue || 0; // 兼容原有代码 + giftMsg.Combo = data.Combo || false; + giftMsg.ImgUrl = data.ImgUrl || ""; + + // 解析接收礼物的用户 + if (data.ToUser) { + giftMsg.ToUser = MessageParser.parseUser(data.ToUser); + } + + return giftMsg; + } + + /** + * 解析直播间统计消息 + * @param {Object} data - 直播间统计消息数据 + * @returns {UserSeqMsg} - 解析后的直播间统计消息对象 + */ + static parseStatistics(data) { + const statsMsg = new UserSeqMsg(); + MessageParser.parseCommonMsgFields(data, statsMsg); + statsMsg.OnlineUserCount = data.OnlineUserCount || 0; + statsMsg.TotalUserCount = data.TotalUserCount || 0; + statsMsg.TotalUserCountStr = data.TotalUserCountStr || "0"; + statsMsg.OnlineUserCountStr = data.OnlineUserCountStr || "0"; + return statsMsg; + } + + /** + * 解析粉丝团消息 + * @param {Object} data - 粉丝团消息数据 + * @returns {FansclubMsg} - 解析后的粉丝团消息对象 + */ + static parseFansclub(data) { + const fansclubMsg = new FansclubMsg(); + MessageParser.parseCommonMsgFields(data, fansclubMsg); + fansclubMsg.Type = data.Type || 0; + fansclubMsg.Level = data.Level || 0; + fansclubMsg.FansClubName = data.FansClubName || ""; + return fansclubMsg; + } + + /** + * 解析直播间分享消息 + * @param {Object} data - 直播间分享消息数据 + * @returns {ShareMessage} - 解析后的直播间分享消息对象 + */ + static parseShare(data) { + const shareMsg = new ShareMessage(); + MessageParser.parseCommonMsgFields(data, shareMsg); + shareMsg.ShareType = data.ShareType || ShareType.未知; + return shareMsg; + } + + /** + * 解析关注消息 + * @param {Object} data - 关注消息数据 + * @returns {Msg} - 解析后的关注消息对象 + */ + static parseFollow(data) { + const followMsg = new Msg(); + return MessageParser.parseCommonMsgFields(data, followMsg); + } +} + +module.exports = { + MessageParser +}; \ No newline at end of file diff --git a/Demos/Python/LogWindow.py b/Demos/Python/LogWindow.py deleted file mode 100644 index 22e911d..0000000 --- a/Demos/Python/LogWindow.py +++ /dev/null @@ -1,291 +0,0 @@ -# ruff: noqa: F401,F403,F405,E402,F541,E722 -import asyncio -import websockets -import json -import os -import ctypes -import sys -import time -import winreg -import tkinter as tk -from tkinter import ttk -import customtkinter -import subprocess -import random -import threading -import atexit -import psutil -import queue - -# 创建一个队列 -gui_queue = queue.Queue() - -# 创建一个线程安全的标志 -stop_flag = threading.Event() - - -def is_admin(): - try: - return ctypes.windll.shell32.IsUserAnAdmin() - except: - return False - - -def disable_proxy(): - internet_settings = winreg.OpenKey( - winreg.HKEY_CURRENT_USER, - r"Software\Microsoft\Windows\CurrentVersion\Internet Settings", - 0, - winreg.KEY_ALL_ACCESS, - ) - - # 设置代理使能为0(禁用) - winreg.SetValueEx(internet_settings, "ProxyEnable", 0, winreg.REG_DWORD, 0) - # 清空代理服务器信息 - winreg.SetValueEx(internet_settings, "ProxyServer", 0, winreg.REG_SZ, "") - - winreg.CloseKey(internet_settings) - print("Proxy disabled.") - - -def initGame(): - print("Init game.") - pass - - -class App(customtkinter.CTk): - def __init__(self): - super().__init__() - - self.title("dy-barrage-grab-log-window") - self.geometry("800x600") - self.grid_columnconfigure((0, 1), weight=1) - self.grid_rowconfigure(0, weight=1) - self.LogFrame = LogFrame(self) - self.LogFrame.grid(row=0, column=0, sticky="nsew") - - -class LogFrame(customtkinter.CTkFrame): - def __init__(self, master): - super().__init__(master) - - # 创建一个Frame来包含treeview和滚动条 - self.tree_frame = ttk.Frame(self) - self.tree_frame.grid(row=0, column=0, sticky="nsew", padx=50, pady=50) - - # 创建一个滚动条 - self.scrollbar = customtkinter.CTkScrollbar(self.tree_frame) - self.scrollbar.grid(row=0, column=1, sticky="ns") - - self.treeview = ttk.Treeview( - self.tree_frame, - columns=("Type", "User ID", "User Nickname", "Content"), - show="headings", - height=20, - yscrollcommand=self.scrollbar.set, # 将滚动条与treeview关联 - ) - self.treeview.grid(row=0, column=0, sticky="nsew") - - # 设置滚动条的命令为移动treeview - self.scrollbar.configure(command=self.treeview.yview) - - # 设置列宽 - self.treeview.column("Type", width=50) - self.treeview.column("User ID", width=100) - self.treeview.column("User Nickname", width=100) - self.treeview.column("Content", width=450) - - self.treeview.heading("Type", text="Type") - self.treeview.heading("User ID", text="User ID") - self.treeview.heading("User Nickname", text="User Nickname") - self.treeview.heading("Content", text="Content") - - # 设置不同类型的颜色 - self.treeview.tag_configure("type1", foreground="black") - self.treeview.tag_configure("type2", foreground="cyan") - self.treeview.tag_configure("type3", foreground="gold") - self.treeview.tag_configure("type4", foreground="blue") - self.treeview.tag_configure("type5", foreground="red") - self.treeview.tag_configure("type6", foreground="purple") - self.treeview.tag_configure("type7", foreground="green") - self.treeview.tag_configure("type8", foreground="orange") - self.treeview.tag_configure("type9", foreground="gray") - # 添加更多类型的颜色配置... - - def display_message(self, message): - parsed_message = json.loads(message) - message_type = parsed_message.get("Type") - - data_str = parsed_message.get("Data", "{}") - try: - data = json.loads(data_str) - except json.JSONDecodeError: - print(f"Cannot parse 'Data' as JSON: {data_str}") - return - user = data.get("User", {}) - if user is None: - # print(f"'User' is None in 'Data': {data_str}") - user = {"Id": "无", "Nickname": "无"} - user_id = user.get("Id", "") - user_nickname = user.get("Nickname", "") - content = data.get("Content", "") - # 根据类型设置颜色 - color_tag = f"type{message_type}" - message_type_str = [ - "无", - "弹幕", - "点赞", - "进房", - "关注", - "礼物", - "统计", - "粉团", - "分享", - "下播", - ] - self.treeview.insert( - "", - "end", - values=( - message_type_str[message_type], - user_id, - user_nickname, - content, - ), - tags=(color_tag,), - ) - - -# async def send_ping(websocket): -# while True: -# try: -# if websocket.open: -# await websocket.ping() -# await asyncio.sleep(5) -# else: -# break -# except websockets.exceptions.ConnectionClosedOK: -# # 连接已关闭,尝试重新连接 -# websocket = await websockets.connect("ws://127.0.0.1:8888") - - -async def receive_messages(): - while True: - try: - async with websockets.connect( - "ws://127.0.0.1:8888", ping_timeout=None, ping_interval=None - ) as websocket: - while True: - message = await websocket.recv() - if message is not None: - try: - json.loads(message) - app.LogFrame.display_message(message) - except json.JSONDecodeError: - print( - f"Received a message that could not be parsed as JSON: {message}" - ) - except ( - websockets.ConnectionClosed, - websockets.exceptions.ConnectionClosedError, - websockets.exceptions.ConnectionClosedOK, - ) as e: - print(f"Connection closed, retrying...{e}") - continue - except Exception as e: - print(f"An error occurred on ws server: {e}") - continue - - -def main(): - global listener_process - global app - # Check if the process is already running - for proc in psutil.process_iter(): - if proc.name() == "WssBarrageServer.exe": - print("The process is already running.") - proc.kill() - time.sleep(1) - - # Start a new process - # 将WssBarrageServer.exe放至同目录下dy-barrage-grab文件夹 - # 获取当前脚本的绝对路径 - script_dir = os.path.dirname(os.path.realpath(__file__)) - # 构建WssBarrageServer.exe的绝对路径 - exe_path = os.path.join(script_dir, "dy-barrage-grab", "WssBarrageServer.exe") - - # 使用绝对路径启动进程 - listener_process = subprocess.Popen(exe_path) - # 创建一个新的事件循环 - loop = asyncio.new_event_loop() - # 设置这个事件循环为当前线程的事件循环 - asyncio.set_event_loop(loop) - app = App() - - # 点击关闭按钮时调用的函数 - def on_close(): - print("Closing...") - # 设置停止标志 - stop_flag.set() - # 关闭WssBarrageServer - listener_process.terminate() - # 关闭代理 - disable_proxy() - # 退出程序 - os._exit(0) - - # 设置关闭按钮的回调函数 - app.protocol("WM_DELETE_WINDOW", on_close) - # 在事件循环中创建新的异步任务 - loop.create_task(receive_messages()) - - def update(): - # 更新Tkinter的界面 - app.update() - # 在事件循环中定期调用update方法 - loop.call_soon(update) - - # 检查队列中是否有任务 - while not gui_queue.empty(): - # 获取任务并执行 - task = gui_queue.get() - task() - - # 在事件循环中定期调用update方法 - loop.call_soon(update) - try: - # 运行事件循环 - loop.run_forever() - except Exception as e: - print(f"An error occurred in loop: {e}") - finally: - # 关闭事件循环 - loop.close() - - -if __name__ == "__main__": - if is_admin(): - # 如果已经是管理员权限,那么直接运行你的代码 - print("Running as administrator.") - pass - else: - # 如果不是管理员权限,那么以管理员权限重新启动程序 - print("Running as non-administrator.") - ctypes.windll.shell32.ShellExecuteW( - None, "runas", sys.executable, " ".join(sys.argv), None, 1 - ) - # try: - main() -# except Exception as e: -# print(f"An error occurred: {e}") -# finally: -# # 这里结束进程 -# listener_process.terminate() - - -# 注册一个函数,在程序结束时调用 -def on_exit(): - listener_process.terminate() - - -atexit.register(on_exit) diff --git a/Demos/Python/entities.py b/Demos/Python/entities.py new file mode 100644 index 0000000..909242a --- /dev/null +++ b/Demos/Python/entities.py @@ -0,0 +1,167 @@ +from enum import Enum, IntEnum +from typing import Optional, Dict, Any, List, Union +from dataclasses import dataclass, field + +# 枚举类型定义 +class PackMsgType(IntEnum): + """消息类型枚举""" + 无 = 0 + 弹幕消息 = 1 + 点赞消息 = 2 + 进直播间 = 3 + 关注消息 = 4 + 礼物消息 = 5 + 直播间统计 = 6 + 粉丝团消息 = 7 + 直播间分享 = 8 + 下播 = 9 + +class Gender(IntEnum): + """性别枚举""" + 未知 = 0 + 男 = 1 + 女 = 2 + + def to_string(self) -> str: + """将性别枚举转换为中文字符串""" + if self == Gender.男: + return "男" + elif self == Gender.女: + return "女" + else: + return "妖" + +class FansclubType(IntEnum): + """粉丝团消息类型""" + 无 = 0 + 粉丝团升级 = 1 + 加入粉丝团 = 2 + +class ShareType(IntEnum): + """直播间分享目标""" + 未知 = 0 + 微信 = 1 + 朋友圈 = 2 + 微博 = 3 + QQ空间 = 4 + QQ = 5 + 抖音好友 = 112 + +class EnterType(IntEnum): + """观众的进入方式""" + 正常进入 = 0 + 通过分享进入 = 6 + +# 数据类定义 +@dataclass +class FansClubInfo: + """粉丝团信息""" + ClubName: str = "" + Level: int = 0 + +@dataclass +class MsgUser: + """用户信息""" + Id: int = 0 + IsAdmin: bool = False + IsAnchor: bool = False + ShortId: int = 0 + DisplayId: str = "" + Nickname: str = "匿名用户" + Level: int = 0 + PayLevel: int = 0 + Gender: Gender = Gender.未知 + HeadImgUrl: str = "" + SecUid: str = "" + FansClub: Optional[FansClubInfo] = None + FollowerCount: int = 0 + FollowStatus: int = 0 + FollowingCount: int = 0 + + def gender_to_string(self) -> str: + """返回性别的中文表示""" + return Gender(self.Gender).to_string() + +@dataclass +class RoomAnchorInfo: + """直播间主播信息""" + UserId: str = "" + SecUid: str = "" + Nickname: str = "" + HeadUrl: str = "" + FollowStatus: int = 0 + +@dataclass +class Msg: + """基础消息类""" + MsgId: int = 0 + User: MsgUser = field(default_factory=MsgUser) + Owner: Optional[RoomAnchorInfo] = None # 修正字段名称 + Content: str = "" + RoomId: str = "" + WebRoomId: str = "" + RoomTitle: str = "" + IsAnonymous: bool = False + Appid: str = "" + +@dataclass +class LikeMsg(Msg): + """点赞消息""" + Count: int = 0 + Total: int = 0 + +@dataclass +class GiftMsg(Msg): + """礼物消息""" + GiftId: int = 0 + GiftName: str = "" + GroupId: int = 0 + GiftCount: int = 0 + RepeatCount: int = 0 + DiamondCount: int = 0 # 抖币价格 + Combo: bool = False # 该礼物是否可连击 + ImgUrl: str = "" # 礼物图片地址 + ToUser: Optional[MsgUser] = None # 送礼目标(连麦直播间有用) + GiftValue: int = 0 # 在原Python代码中使用的礼物价值字段 + +@dataclass +class MemberMessage(Msg): + """进入直播间消息""" + CurrentCount: int = 0 + EnterTipType: int = 0 + +@dataclass +class UserSeqMsg(Msg): + """直播间统计消息""" + OnlineUserCount: int = 0 + TotalUserCount: int = 0 + TotalUserCountStr: str = "0" + OnlineUserCountStr: str = "0" + +@dataclass +class FansclubMsg(Msg): + """粉丝团消息""" + Type: int = 0 # 粉丝团消息类型,升级1,加入2 + Level: int = 0 # 粉丝团等级 + FansClubName: str = "" # 粉丝团名称 + +@dataclass +class ShareMessage(Msg): + """直播间分享""" + ShareType: ShareType = ShareType.未知 + +# 全局统计数据类 +@dataclass +class LiveStats: + """直播间统计数据""" + total_likes: int = 0 + total_users: int = 0 + male_users: int = 0 + female_users: int = 0 + + def reset(self) -> None: + """重置统计数据""" + self.total_likes = 0 + self.total_users = 0 + self.male_users = 0 + self.female_users = 0 diff --git a/Demos/Python/main.py b/Demos/Python/main.py new file mode 100644 index 0000000..0ba21d5 --- /dev/null +++ b/Demos/Python/main.py @@ -0,0 +1,245 @@ +import asyncio +import websockets +import json +import time +from datetime import datetime +from typing import Dict, Any +from colorama import init, Fore, Style +from entities import ( + PackMsgType, Gender, LiveStats, LikeMsg, MemberMessage, UserSeqMsg, + GiftMsg, FansclubMsg, ShareMessage, ShareType +) +from parsers import MessageParser + +''' +抖音直播间消息处理脚本 +消息类型说明: +1[普通弹幕]|2[点赞消息]|3[进入直播间]|4[关注消息]|5[礼物消息]|6[统计消息]|7[粉丝团消息]|8[直播间分享]|9[下播] + +全局统计变量: +Live_Total - 本场点赞数 +Live_user - 本场总观看人数 +Live_man - 本场男生人数 +Live_woman - 本场女生人数 +''' + +# 初始化彩色输出 +init(autoreset=True) + +# 颜色定义 +COLOR_MAP = { + PackMsgType.弹幕消息: Fore.CYAN, # 青色 + PackMsgType.点赞消息: Fore.YELLOW, # 黄色 + PackMsgType.进直播间: Fore.GREEN, # 绿色 + PackMsgType.关注消息: Fore.MAGENTA, # 洋红色 + PackMsgType.礼物消息: Fore.RED, # 红色 + PackMsgType.直播间统计: Fore.BLUE, # 蓝色 + PackMsgType.粉丝团消息: Fore.LIGHTBLUE_EX, # 浅蓝色 + PackMsgType.直播间分享: Fore.LIGHTMAGENTA_EX, # 浅洋红色 + PackMsgType.下播: Fore.RED, # 红色 +} + +# 初始化统计对象 +live_stats = LiveStats() + +def get_timestamp() -> str: + """返回当前时间戳,格式:HH:MM:SS""" + return datetime.now().strftime("%H:%M:%S") + +def print_colored_message(msg_type: PackMsgType, content: str, author: str = "未知") -> None: + """打印带颜色的消息""" + color = COLOR_MAP.get(msg_type, Fore.WHITE) + timestamp = get_timestamp() + elapsed_time = "["+author+"]" + print(f"{color}{timestamp} {elapsed_time} {content}{Style.RESET_ALL}") + +def get_msg_owner(msg) -> str: + """统一获取消息所属的主播/房间信息""" + if hasattr(msg, 'Owner') and msg.Owner: + return msg.Owner.Nickname + elif hasattr(msg, 'WebRoomId') and msg.WebRoomId: + return msg.WebRoomId + elif hasattr(msg, 'RoomId') and msg.RoomId: + return msg.RoomId + return "未知" + +# 处理函数:弹幕消息 +def handle_danmaku(data_dict: Dict[str, Any]) -> None: + """处理用户发送的弹幕消息""" + msg = MessageParser.parse_danmaku(data_dict) + gender_str = msg.User.gender_to_string() + content = f"[弹幕消息] [{gender_str}] {msg.User.Nickname}: {msg.Content}" + author = get_msg_owner(msg) + print_colored_message(PackMsgType.弹幕消息, content, author) + +# 处理函数:点赞消息 +def handle_dianzanku(data_dict: Dict[str, Any]) -> None: + """处理用户点赞消息并更新全局统计""" + like_msg = MessageParser.parse_like(data_dict) + live_stats.total_likes = like_msg.Total + gender_str = like_msg.User.gender_to_string() + content = f"[点赞消息] [{gender_str}] {like_msg.User.Nickname} 为主播点了{like_msg.Count}个赞, 总点赞{like_msg.Total}" + author = get_msg_owner(like_msg) + print_colored_message(PackMsgType.点赞消息, content, author) + +# 处理函数:进入直播间 +def handle_userentry(data_dict: Dict[str, Any]) -> None: + """处理用户进入直播间的消息,并根据性别更新统计""" + member_msg = MessageParser.parse_member(data_dict) + + # 根据性别更新全局计数 + if member_msg.User.Gender == Gender.男: + live_stats.male_users += 1 + elif member_msg.User.Gender == Gender.女: + live_stats.female_users += 1 + + gender_str = member_msg.User.gender_to_string() + content = f"[进直播间] [{gender_str}] {member_msg.User.Nickname} $来了 直播间人数:{member_msg.CurrentCount}" + author = get_msg_owner(member_msg) + print_colored_message(PackMsgType.进直播间, content, author) + +# 处理函数:关注消息 +def handle_follow(data_dict: Dict[str, Any]) -> None: + """处理用户关注主播的消息""" + follow_msg = MessageParser.parse_follow(data_dict) + gender_str = follow_msg.User.gender_to_string() + content = f"[关注消息] [{gender_str}] {follow_msg.User.Nickname} 关注了主播" + author = get_msg_owner(follow_msg) + print_colored_message(PackMsgType.关注消息, content, author) + +# 处理函数:礼物消息 +def handle_gift(data_dict: Dict[str, Any]) -> None: + """处理用户赠送礼物的消息""" + gift_msg = MessageParser.parse_gift(data_dict) + gender_str = gift_msg.User.gender_to_string() + + # 使用DiamondCount或GiftValue作为礼物价值 + gift_value = gift_msg.DiamondCount or gift_msg.GiftValue + + content = f"[礼物消息] [{gender_str}] {gift_msg.User.Nickname} 送出了 {gift_msg.GiftCount}个{gift_msg.GiftName}, 价值{gift_value}抖币" + author = get_msg_owner(gift_msg) + print_colored_message(PackMsgType.礼物消息, content, author) + +# 处理函数:统计消息 +def handle_statistics(data_dict: Dict[str, Any]) -> None: + """处理直播间统计信息""" + stats_msg = MessageParser.parse_statistics(data_dict) + live_stats.total_users = int(stats_msg.TotalUserCount) + content = f"[直播间统计] 当前直播间人数 {stats_msg.OnlineUserCountStr}, 累计直播间人数 {stats_msg.TotalUserCountStr}万" + author = get_msg_owner(stats_msg) + print_colored_message(PackMsgType.直播间统计, content, author) + +# 处理函数:粉丝团消息 +def handle_fansclub(data_dict: Dict[str, Any]) -> None: + """处理粉丝团相关消息""" + fansclub_msg = MessageParser.parse_fansclub(data_dict) + gender_str = fansclub_msg.User.gender_to_string() + + if fansclub_msg.Type == 1: + action = "升级了" + content = f"[粉丝团消息] [{gender_str}] {fansclub_msg.User.Nickname} {action}{fansclub_msg.FansClubName}粉丝团到{fansclub_msg.Level}级" + else: + action = "加入了" + content = f"[粉丝团消息] [{gender_str}] {fansclub_msg.User.Nickname} {action}{fansclub_msg.FansClubName}粉丝团" + + author = get_msg_owner(fansclub_msg) + print_colored_message(PackMsgType.粉丝团消息, content, author) + +# 处理函数:直播间分享 +def handle_share(data_dict: Dict[str, Any]) -> None: + """处理用户分享直播间的消息""" + share_msg = MessageParser.parse_share(data_dict) + gender_str = share_msg.User.gender_to_string() + + # 获取分享目标类型 + share_type_str = share_msg.ShareType.name if hasattr(share_msg.ShareType, 'name') else "未知平台" + + content = f"[直播间分享] [{gender_str}] {share_msg.User.Nickname} 分享了直播间到{share_type_str}" + author = get_msg_owner(share_msg) + print_colored_message(PackMsgType.直播间分享, content, author) + +# 处理函数:直播结束 +def handle_live_exit() -> None: + """处理直播结束事件,显示并重置统计数据""" + content = f"[下播] 直播结束:累计观看 {live_stats.total_users} 人,累计点赞 {live_stats.total_likes},"\ + f"男生 {live_stats.male_users} 女生 {live_stats.female_users}" + print_colored_message(PackMsgType.下播, content, "系统消息") + live_stats.reset() + print(f"直播统计数据已清空~") + +async def process_message(message: str) -> None: + """处理从WebSocket接收到的单条消息""" + try: + data = json.loads(message) + + if "Type" not in data: + print("⚠️ 收到无效消息:缺少Type字段") + return + + msg_type = PackMsgType(data['Type']) + + # 检查是否需要处理Data字段 + if msg_type == PackMsgType.下播: + handle_live_exit() + return + + if "Data" not in data: + print(f"⚠️ 消息类型 {msg_type.name} 缺少Data字段") + return + + try: + data_dict = json.loads(data['Data']) + + # 消息处理映射表 + handlers = { + PackMsgType.弹幕消息: handle_danmaku, + PackMsgType.点赞消息: handle_dianzanku, + PackMsgType.进直播间: handle_userentry, + PackMsgType.关注消息: handle_follow, + PackMsgType.礼物消息: handle_gift, + PackMsgType.直播间统计: handle_statistics, + PackMsgType.粉丝团消息: handle_fansclub, + PackMsgType.直播间分享: handle_share, + } + + # 分发消息到相应处理器 + if msg_type in handlers: + handlers[msg_type](data_dict) + + except json.JSONDecodeError: + print(f"⚠️ 无法解析Data字段内容:{data['Data'][:100]}...") + + except json.JSONDecodeError: + print(f"⚠️ 无效的JSON格式: {message[:100]}...") + except Exception as e: + print(f"❌ 消息处理错误: {str(e)}") + +async def connect_and_print() -> None: + """连接到WebSocket服务器并处理接收到的消息""" + uri = "ws://127.0.0.1:8888" + + while True: + try: + print(f"正在连接到服务器 {uri}...") + async with websockets.connect(uri, ping_interval=None) as websocket: + print(f"✅ 已成功连接到服务器 {uri}") + + while True: + message = await websocket.recv() + await process_message(message) + + except websockets.exceptions.ConnectionClosed as e: + print(f"⚠️ 与服务器的连接已关闭: {e}") + except ConnectionRefusedError: + print(f"❌ 无法连接到服务器 {uri},5秒后重试...") + except Exception as e: + print(f"❌ 发生错误: {str(e)}") + + # 连接断开后等待重连 + await asyncio.sleep(5) + print("正在尝试重新连接...") + +# 程序入口 +if __name__ == "__main__": + print("启动抖音直播消息监听...") + asyncio.run(connect_and_print()) diff --git a/Demos/Python/parsers.py b/Demos/Python/parsers.py new file mode 100644 index 0000000..0a77b86 --- /dev/null +++ b/Demos/Python/parsers.py @@ -0,0 +1,158 @@ +from typing import Dict, Any, Optional +from entities import ( + MsgUser, FansClubInfo, Msg, LikeMsg, MemberMessage, UserSeqMsg, + Gender, GiftMsg, FansclubMsg, ShareMessage, RoomAnchorInfo, ShareType +) + +# 消息解析器 +class MessageParser: + @staticmethod + def parse_user(user_data: Dict[str, Any]) -> MsgUser: + """解析用户数据""" + user = MsgUser() + if not user_data: + return user + + user.Id = user_data.get("Id", 0) + user.IsAdmin = user_data.get("IsAdmin", False) + user.IsAnchor = user_data.get("IsAnchor", False) + user.ShortId = user_data.get("ShortId", 0) + user.DisplayId = user_data.get("DisplayId", "") + user.Nickname = user_data.get("Nickname", "匿名用户") + user.Level = user_data.get("Level", 0) + user.PayLevel = user_data.get("PayLevel", 0) + user.Gender = Gender(user_data.get("Gender", 0)) + user.HeadImgUrl = user_data.get("HeadImgUrl", "") + user.SecUid = user_data.get("SecUid", "") + user.FollowerCount = user_data.get("FollowerCount", 0) + user.FollowStatus = user_data.get("FollowStatus", 0) + user.FollowingCount = user_data.get("FollowingCount", 0) + + # 解析粉丝团信息 + if "FansClub" in user_data and user_data["FansClub"]: + fans_club_data = user_data["FansClub"] + user.FansClub = FansClubInfo( + ClubName=fans_club_data.get("ClubName", ""), + Level=fans_club_data.get("Level", 0) + ) + + return user + + @staticmethod + def parse_anchor_info(owner_data: Dict[str, Any]) -> Optional[RoomAnchorInfo]: + """解析主播信息""" + if not owner_data: + return None + + owner = RoomAnchorInfo() + owner.UserId = owner_data.get("UserId", "") + owner.SecUid = owner_data.get("SecUid", "") + owner.Nickname = owner_data.get("Nickname", "") + owner.HeadUrl = owner_data.get("HeadUrl", "") + owner.FollowStatus = owner_data.get("FollowStatus", 0) + + return owner + + @staticmethod + def parse_common_msg_fields(data: Dict[str, Any], msg: Msg) -> Msg: + """解析通用消息字段""" + msg.MsgId = data.get("MsgId", 0) + msg.Content = data.get("Content", "") + msg.RoomId = data.get("RoomId", "") + msg.WebRoomId = data.get("WebRoomId", "") + msg.RoomTitle = data.get("RoomTitle", "") + msg.IsAnonymous = data.get("IsAnonymous", False) + msg.Appid = data.get("Appid", "") + + # 解析用户数据 + if "User" in data and data["User"]: + msg.User = MessageParser.parse_user(data["User"]) + + # 解析主播数据,优先使用正确拼写的"Owner" + if "Owner" in data and data["Owner"]: + msg.Owner = MessageParser.parse_anchor_info(data["Owner"]) + # 如果没有"Owner"字段才考虑拼写错误的"Onwer"字段,并且确保不是字符串 + elif "Onwer" in data and data["Onwer"] and not isinstance(data["Onwer"], str): + msg.Owner = MessageParser.parse_anchor_info(data["Onwer"]) + + return msg + + @staticmethod + def parse_danmaku(data: Dict[str, Any]) -> Msg: + """解析弹幕消息""" + msg = Msg() + return MessageParser.parse_common_msg_fields(data, msg) + + @staticmethod + def parse_like(data: Dict[str, Any]) -> LikeMsg: + """解析点赞消息""" + like_msg = LikeMsg() + MessageParser.parse_common_msg_fields(data, like_msg) + like_msg.Count = data.get("Count", 0) + like_msg.Total = data.get("Total", 0) + return like_msg + + @staticmethod + def parse_member(data: Dict[str, Any]) -> MemberMessage: + """解析进入直播间消息""" + member_msg = MemberMessage() + MessageParser.parse_common_msg_fields(data, member_msg) + member_msg.CurrentCount = data.get("CurrentCount", 0) + member_msg.EnterTipType = data.get("EnterTipType", 0) + return member_msg + + @staticmethod + def parse_gift(data: Dict[str, Any]) -> GiftMsg: + """解析礼物消息""" + gift_msg = GiftMsg() + MessageParser.parse_common_msg_fields(data, gift_msg) + gift_msg.GiftId = data.get("GiftId", 0) + gift_msg.GiftName = data.get("GiftName", "") + gift_msg.GroupId = data.get("GroupId", 0) + gift_msg.GiftCount = data.get("GiftCount", 0) + gift_msg.RepeatCount = data.get("RepeatCount", 0) + gift_msg.DiamondCount = data.get("DiamondCount", 0) + gift_msg.GiftValue = data.get("GiftValue", 0) # 兼容原有代码 + gift_msg.Combo = data.get("Combo", False) + gift_msg.ImgUrl = data.get("ImgUrl", "") + + # 解析接收礼物的用户 + if "ToUser" in data and data["ToUser"]: + gift_msg.ToUser = MessageParser.parse_user(data["ToUser"]) + + return gift_msg + + @staticmethod + def parse_statistics(data: Dict[str, Any]) -> UserSeqMsg: + """解析直播间统计消息""" + stats_msg = UserSeqMsg() + MessageParser.parse_common_msg_fields(data, stats_msg) + stats_msg.OnlineUserCount = data.get("OnlineUserCount", 0) + stats_msg.TotalUserCount = data.get("TotalUserCount", 0) + stats_msg.TotalUserCountStr = data.get("TotalUserCountStr", "0") + stats_msg.OnlineUserCountStr = data.get("OnlineUserCountStr", "0") + return stats_msg + + @staticmethod + def parse_fansclub(data: Dict[str, Any]) -> FansclubMsg: + """解析粉丝团消息""" + fansclub_msg = FansclubMsg() + MessageParser.parse_common_msg_fields(data, fansclub_msg) + fansclub_msg.Type = data.get("Type", 0) + fansclub_msg.Level = data.get("Level", 0) + fansclub_msg.FansClubName = data.get("FansClubName", "") + return fansclub_msg + + @staticmethod + def parse_share(data: Dict[str, Any]) -> ShareMessage: + """解析直播间分享消息""" + share_msg = ShareMessage() + MessageParser.parse_common_msg_fields(data, share_msg) + share_msg.ShareType = ShareType(data.get("ShareType", 0)) + return share_msg + + @staticmethod + def parse_follow(data: Dict[str, Any]) -> Msg: + """解析关注消息""" + follow_msg = Msg() + return MessageParser.parse_common_msg_fields(data, follow_msg) diff --git a/Demos/Python/requirements.txt b/Demos/Python/requirements.txt new file mode 100644 index 0000000..84252b8 --- /dev/null +++ b/Demos/Python/requirements.txt @@ -0,0 +1,2 @@ +websockets>=10.0 +colorama>=0.4.4 diff --git a/Demos/Python/ws.py b/Demos/Python/ws.py deleted file mode 100644 index 647f440..0000000 --- a/Demos/Python/ws.py +++ /dev/null @@ -1,89 +0,0 @@ -import asyncio -import json -from jsonpath import jsonpath -import websockets - -# 1用户发言#2用户点赞#3用户入房#4关注主播#5用户礼物#6人数统计 -''' -str(fid[0:2])=="403": #字符串提取方便自定义命令 -''' - - -def msg(data): - global close - load_json_data = json.loads(data.get("Data")) - 用户名 = jsonpath(load_json_data, '$.User.Nickname') - 用户等级 = jsonpath(load_json_data, '$.User.PayLevel') - 粉丝团 = jsonpath(load_json_data, '$.User.FansClub.ClubName') - 用户发言 = jsonpath(load_json_data, '$.Content') - if 用户发言[0] == "#关闭": - close = False - else: - print(用户名[0]) - print(用户等级[0]) - - -def praise(data): # type2 - load_json_data = json.loads(data.get("Data")) - 用户点赞 = jsonpath(load_json_data, '$.Content') - print("感谢" + 用户点赞[0]) - - -def welcome(data): # type3 - load_json_data = json.loads(data.get("Data")) - 用户名 = jsonpath(load_json_data, '$.User.Nickname') - 粉丝团 = jsonpath(load_json_data, '$.User.FansClub.ClubName') - if 粉丝团[0] == "Reset": - print("欢迎粉丝:" + 用户名[0]) - else: - print("欢迎:" + 用户名[0]) - - -def thank(data): # type5 - load_json_data = json.loads(data.get("Data")) - 粉丝团 = jsonpath(load_json_data, '$.User.FansClub.ClubName') - 用户送礼 = jsonpath(load_json_data, '$.Content') - # txt1 = txt.replace("主播", "黎公子") #字符串替换功能方便拓展功能 - if 粉丝团[0] == "Reset": - print("感谢老板:" + 用户送礼[0]) - else: - print("感谢:" + 用户送礼[0]) - - -def check(data): # type6 - load_json_data = json.loads(data.get("Data")) - 房间统计 = jsonpath(load_json_data, '$.Content') - print(房间统计[0]) - - -def check_json(json_data): - Token = json_data.get("Type") # 标签类型 - if Token == 1: # 1用户发言 - msg(json_data) - elif Token == 2: # 2用户点赞 - praise(json_data) - elif Token == 3: # 3用户入房 - welcome(json_data) - elif Token == 4: # 感谢关注 - print(str(json_data)) - elif Token == 5: # 5用户礼物 - thank(json_data) - elif Token == 6: # 6人数统计 - check(json_data) - else: - print(json_data) - - -async def main(): - global close - async with websockets.connect("ws://127.0.0.1:8888/", ping_interval=None) as ws: - await ws.send("token") - close = True - while close is True: - result = await ws.recv() - check_json(json.loads(result)) - await ws.close() - - -close = True -asyncio.run(main()) \ No newline at end of file