3875 lines
154 KiB
Python
3875 lines
154 KiB
Python
import json
|
||
import os
|
||
import subprocess
|
||
from typing import Dict
|
||
from tqdm import tqdm
|
||
import re
|
||
import requests
|
||
import time
|
||
import random
|
||
import base64
|
||
import shutil
|
||
import hashlib
|
||
|
||
def convert_browser_cookies(browser_cookies: list) -> Dict:
|
||
"""转换浏览器导出的cookies格式为简单的键值对"""
|
||
cookies = {}
|
||
|
||
for cookie in browser_cookies:
|
||
if isinstance(cookie, dict) and 'name' in cookie and 'value' in cookie:
|
||
cookies[cookie['name']] = cookie['value']
|
||
|
||
return cookies
|
||
|
||
def load_cookies_from_file() -> Dict:
|
||
"""从文件加载cookies"""
|
||
cookie_files = ['cookies.txt', 'bilibili_cookies.json'] # 直接从当前目录读取
|
||
|
||
for cookie_file in cookie_files:
|
||
if os.path.exists(cookie_file):
|
||
try:
|
||
with open(cookie_file, 'r', encoding='utf-8') as f:
|
||
content = f.read().strip()
|
||
cookies = {}
|
||
|
||
# 尝试作为JSON解析
|
||
try:
|
||
json_data = json.loads(content)
|
||
if isinstance(json_data, list):
|
||
cookies = convert_browser_cookies(json_data)
|
||
elif isinstance(json_data, dict):
|
||
cookies = json_data
|
||
except json.JSONDecodeError:
|
||
# 如果不是JSON,尝试作为Netscape格式或普通cookie字符串解析
|
||
if content.startswith('# Netscape HTTP Cookie File'):
|
||
# Netscape格式
|
||
for line in content.split('\n'):
|
||
line = line.strip()
|
||
if not line or line.startswith('#'):
|
||
continue
|
||
try:
|
||
fields = line.split('\t')
|
||
if len(fields) >= 7:
|
||
name, value = fields[5:7]
|
||
cookies[name] = value
|
||
except:
|
||
continue
|
||
else:
|
||
# 尝试作为普通cookie字符串解析
|
||
cookies = parse_cookie_string(content)
|
||
|
||
if cookies:
|
||
# 检查必要的cookie是否存在
|
||
required_cookies = ['SESSDATA', 'bili_jct', 'DedeUserID']
|
||
missing_cookies = [cookie for cookie in required_cookies if cookie not in cookies]
|
||
|
||
if missing_cookies:
|
||
print(f"警告: 缺少必要的cookies: {', '.join(missing_cookies)}")
|
||
print("这些cookie是必需的,否则某些功能可能无法正常工作")
|
||
print("请确保你的cookies.txt中包含这些必要的cookie")
|
||
return {}
|
||
|
||
print(f"已从 {cookie_file} 加载cookies:")
|
||
for name, value in cookies.items():
|
||
print(f"找到cookie: {name}")
|
||
return cookies
|
||
else:
|
||
print(f"警告: 无法从 {cookie_file} 加载有效的cookies")
|
||
|
||
except Exception as e:
|
||
print(f"警告: 读取 {cookie_file} 时出错: {str(e)}")
|
||
return {}
|
||
|
||
def parse_cookie_string(cookie_str: str) -> Dict:
|
||
"""解析浏览器直接复制的cookie字符串"""
|
||
cookies = {}
|
||
|
||
try:
|
||
# 处理URL编码
|
||
def decode_value(value):
|
||
# 处理特殊的URL编码字符
|
||
replacements = {
|
||
'%2C': ',',
|
||
'%2F': '/',
|
||
'%3A': ':',
|
||
'%2B': '+',
|
||
'%3D': '=',
|
||
'%3B': ';'
|
||
}
|
||
for encoded, decoded in replacements.items():
|
||
value = value.replace(encoded, decoded)
|
||
return value
|
||
|
||
# 分割cookie字符串
|
||
cookie_pairs = cookie_str.split(';')
|
||
for pair in cookie_pairs:
|
||
if '=' in pair:
|
||
name, value = pair.strip().split('=', 1)
|
||
name = name.strip()
|
||
value = decode_value(value.strip())
|
||
cookies[name] = value
|
||
|
||
# 如果cookies非空,打印找到的cookie数量
|
||
if cookies:
|
||
print(f"字符串中解析出 {len(cookies)} 个cookies")
|
||
|
||
except Exception as e:
|
||
print(f"解析cookie字符串时出错: {str(e)}")
|
||
return {}
|
||
|
||
return cookies
|
||
|
||
def get_cookies_from_user() -> Dict:
|
||
"""获取用户输入的cookies"""
|
||
print("请输入B站cookies (支持JSON格式、浏览器导出格式或直接复制的cookie字符串):")
|
||
cookies_str = input().strip()
|
||
|
||
# 首先尝试解析JSON
|
||
try:
|
||
cookies_data = json.loads(cookies_str)
|
||
if isinstance(cookies_data, list):
|
||
cookies = convert_browser_cookies(cookies_data)
|
||
else:
|
||
cookies = cookies_data
|
||
except json.JSONDecodeError:
|
||
# 如果不是JSON,尝试解析为cookie字符串
|
||
cookies = parse_cookie_string(cookies_str)
|
||
|
||
# 检查是否获取到cookies
|
||
if cookies:
|
||
print("成功获取到以下cookies:")
|
||
for name in cookies.keys():
|
||
print(f"- {name}")
|
||
return cookies
|
||
else:
|
||
print("未能获取到有效的cookies")
|
||
return {}
|
||
|
||
def save_cookies(cookies: Dict):
|
||
"""保存cookies为Netscape格式"""
|
||
with open('cookies.txt', 'w', encoding='utf-8') as f: # 直接保存到当前目录
|
||
f.write("# Netscape HTTP Cookie File\n")
|
||
f.write("# https://curl.haxx.se/rfc/cookie_spec.html\n")
|
||
f.write("# This is a generated file! Do not edit.\n\n")
|
||
|
||
# 为所有cookie写入Netscape格式的行
|
||
for name, value in cookies.items():
|
||
# 某些特殊cookie需要特殊处理
|
||
if name in ['SESSDATA']:
|
||
f.write(f"#HttpOnly_.bilibili.com\tTRUE\t/\tTRUE\t1735689600\t{name}\t{value}\n")
|
||
else:
|
||
f.write(f".bilibili.com\tTRUE\t/\tFALSE\t1735689600\t{name}\t{value}\n")
|
||
|
||
print(f"cookies已保存到cookies.txt,包含以下字段:{', '.join(cookies.keys())}")
|
||
|
||
def find_ffmpeg() -> str:
|
||
"""查找ffmpeg可执行文件的路径"""
|
||
try:
|
||
# 直接检查系统是否能执行ffmpeg命令
|
||
result = subprocess.run(['ffmpeg', '-version'],
|
||
capture_output=True,
|
||
text=True)
|
||
if result.returncode == 0:
|
||
print("找到系统ffmpeg")
|
||
return 'ffmpeg' # 直接返回命令名称
|
||
except Exception:
|
||
pass
|
||
|
||
# 如果直接执行失败,尝试完整路径
|
||
ffmpeg_path = r'C:\Program Files\ffmpeg\bin\ffmpeg.exe'
|
||
if os.path.isfile(ffmpeg_path):
|
||
print(f"找到ffmpeg: {ffmpeg_path}")
|
||
return ffmpeg_path
|
||
|
||
print("警告:未找到ffmpeg,请确保ffmpeg已正确安装添加到系统PATH中")
|
||
return ''
|
||
|
||
def convert_to_mp3(input_file: str) -> bool:
|
||
"""将音频文件转换为MP3格式"""
|
||
if not os.path.exists(input_file):
|
||
print(f"错误:找不到文件 {input_file}")
|
||
return False
|
||
|
||
output_file = os.path.splitext(input_file)[0] + '.mp3'
|
||
|
||
try:
|
||
# 构建ffmpeg命令
|
||
cmd = [
|
||
'ffmpeg',
|
||
'-i', input_file,
|
||
'-acodec', 'libmp3lame',
|
||
'-q:a', '0', # ���高质量
|
||
'-threads', '4', # 使���4个线程
|
||
'-progress', 'pipe:1', # 输出进度信息到stdout
|
||
'-nostats', # 不输出额外统计信息
|
||
output_file
|
||
]
|
||
|
||
print(f"正在转换为MP3: {os.path.basename(input_file)}")
|
||
|
||
# 创�����进度条
|
||
with tqdm(total=100,
|
||
desc="转换进度",
|
||
ncols=80,
|
||
bar_format='{desc}: {percentage:3.1f}%|{bar}| [{elapsed}<{remaining}]') as pbar:
|
||
|
||
process = subprocess.Popen(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
universal_newlines=True
|
||
)
|
||
|
||
# 读取并更新进
|
||
last_percent = 0
|
||
while True:
|
||
line = process.stdout.readline()
|
||
if not line and process.poll() is not None:
|
||
break
|
||
|
||
# 从输出中提取时间信息
|
||
if line.startswith('out_time_ms='):
|
||
time_ms = int(line.split('=')[1])
|
||
# 假设每秒处理速度相同,使用已处理时间估算进度
|
||
percent = min(100, max(0, int(time_ms / 10000000))) # 假设平均时长100秒
|
||
|
||
# 更进度条
|
||
if percent > last_percent:
|
||
pbar.update(percent - last_percent)
|
||
last_percent = percent
|
||
|
||
# 确保进度条到达100%
|
||
if last_percent < 100:
|
||
pbar.update(100 - last_percent)
|
||
|
||
# 检查转换结果
|
||
if process.returncode == 0:
|
||
print(f"\n转换完成:{os.path.basename(output_file)}")
|
||
# 询问是否删除原始文件
|
||
choice = input("是否删除原始音频文件?(y/n) [y]: ").lower()
|
||
if not choice or choice == 'y': # 直接回车或输入y都删除
|
||
os.remove(input_file)
|
||
print("原始文件已删除")
|
||
return True
|
||
else:
|
||
error = process.stderr.read()
|
||
print(f"转换失败!错误信息:\n{error}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"转换过程中发生错误: {str(e)}")
|
||
return False
|
||
|
||
def download_audio(url_or_bvid: str, cookies: Dict):
|
||
"""下载音频"""
|
||
save_cookies(cookies)
|
||
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
# 获取视频BV号并显���信息
|
||
bvid = extract_bvid(url_or_bvid)
|
||
if bvid:
|
||
pages = get_video_info(bvid, headers, show_info=False) # 添加参数控制是否显示信息
|
||
if len(pages) > 1:
|
||
choice = input("\n是��显示分P列表?(y/n) [n]: ").lower()
|
||
if choice == 'y':
|
||
print("\n分P列表:")
|
||
# 显示视频基本信息
|
||
get_video_info(bvid, headers, show_info=True) # 此时显示完整信息
|
||
# 显示分P信息
|
||
for page in pages:
|
||
duration_min = page['duration'] // 60
|
||
duration_sec = page['duration'] % 60
|
||
print(f"{page['page']}. {page['part']} ({duration_min}分{duration_sec}秒)")
|
||
|
||
print("\n请输入要下载的分P编号(用空格分隔,例如:1 3 5)")
|
||
print("支持范围选择(例如:1-5 7 9-11)")
|
||
choice = input("请选择 [1]: ").strip()
|
||
|
||
# 如果直接回车,默认下载第一P
|
||
if not choice:
|
||
choice = "1"
|
||
|
||
# 解析选��的分P
|
||
selected_pages = set()
|
||
for part in choice.split():
|
||
if '-' in part:
|
||
start, end = map(int, part.split('-'))
|
||
selected_pages.update(range(start, end + 1))
|
||
else:
|
||
selected_pages.add(int(part))
|
||
|
||
# 过滤无效的分P编号
|
||
valid_pages = sorted([p for p in selected_pages if 1 <= p <= len(pages)])
|
||
|
||
if valid_pages:
|
||
print(f"\n将下载以下分P: {', '.join(map(str, valid_pages))}")
|
||
for page_num in valid_pages:
|
||
page = pages[page_num - 1]
|
||
page_url = f"https://www.bilibili.com/video/{bvid}?p={page_num}"
|
||
print(f"\n开始下载P{page_num}: {page['part']}")
|
||
download_single_audio(page_url, cookies)
|
||
else:
|
||
print("未选择有效的分P编号,将下载P1")
|
||
download_single_audio(url_or_bvid, cookies)
|
||
else:
|
||
# 单P视频直接下载
|
||
download_single_audio(url_or_bvid, cookies)
|
||
else:
|
||
print("请输入有效的B站频链接或BV号!")
|
||
except Exception as e:
|
||
print(f"下载过程出错: {str(e)}")
|
||
|
||
def get_user_info(cookies: Dict) -> None:
|
||
"""获取并显示用户信息"""
|
||
print("\n" + "="*50)
|
||
print("# 用户信息")
|
||
print("="*50)
|
||
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
# 获取用户导航信息
|
||
nav_url = 'https://api.bilibili.com/x/web-interface/nav'
|
||
nav_resp = requests.get(nav_url, cookies=cookies, headers=headers)
|
||
nav_data = nav_resp.json()
|
||
|
||
if nav_data['code'] == 0:
|
||
data = nav_data['data']
|
||
print(f"用户名: {data['uname']}")
|
||
print(f"用户ID: {data['mid']}")
|
||
print(f"邮箱验证: {'已验证' if data['email_verified'] else '未验证'}")
|
||
print(f"手机验证: {'已验证' if data['mobile_verified'] else '未验证'}")
|
||
print(f"硬币数: {data['money']}")
|
||
print(f"B币余额: {data['wallet']['bcoin_balance']}")
|
||
print(f"道德值: {data['moral']}")
|
||
|
||
# 等级信息
|
||
level_info = data['level_info']
|
||
print(f"\n等级信息:")
|
||
print(f"当前等级: {level_info['current_level']}")
|
||
print(f"当前经验: {level_info['current_exp']}")
|
||
print(f"下一级所需经验: {level_info['next_exp']}")
|
||
|
||
# 会员信息
|
||
vip = data['vip']
|
||
print(f"\n会员信息:")
|
||
vip_type_map = {0: "无", 1: "月度大会员", 2: "年度大会员"}
|
||
print(f"会员类型: {vip_type_map.get(vip['type'], '未知')}")
|
||
if vip['status'] == 1:
|
||
due_date = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(vip['due_date']/1000))
|
||
print(f"到期时间: {due_date}")
|
||
|
||
# 认证信息
|
||
if data['official']['type'] != -1:
|
||
print(f"\n认证信息:")
|
||
print(f"认证类型: {'个人认证' if data['official']['type'] == 0 else '机构认证'}")
|
||
if data['official']['title']:
|
||
print(f"认证信息: {data['official']['title']}")
|
||
if data['official']['desc']:
|
||
print(f"认证描述: {data['official']['desc']}")
|
||
|
||
# 获取关注和粉丝数
|
||
stat_url = f'https://api.bilibili.com/x/relation/stat?vmid={data["mid"]}'
|
||
stat_resp = requests.get(stat_url, headers=headers)
|
||
stat_data = stat_resp.json()
|
||
|
||
if stat_data['code'] == 0:
|
||
stat_info = stat_data['data']
|
||
print(f"\n社交信息:")
|
||
print(f"关注数: {stat_info['following']}")
|
||
print(f"粉丝数: {stat_info['follower']}")
|
||
|
||
# 获取今日投币经验
|
||
coin_exp_url = 'https://api.bilibili.com/x/web-interface/coin/today/exp'
|
||
coin_exp_resp = requests.get(coin_exp_url, cookies=cookies, headers=headers)
|
||
coin_exp_data = coin_exp_resp.json()
|
||
|
||
if coin_exp_data['code'] == 0:
|
||
today_exp = coin_exp_data['data']
|
||
max_coin_exp = 50
|
||
print(f"\n投币信息:")
|
||
print(f"今日投币经验: {today_exp}/{max_coin_exp}")
|
||
remaining_coins = (max_coin_exp - today_exp) // 10
|
||
if remaining_coins > 0:
|
||
print(f"今日还可投币: {remaining_coins}个")
|
||
|
||
else:
|
||
print(f"获取用户信息失败:{nav_data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取用户信���时出错: {str(e)}")
|
||
|
||
print("="*50 + "\n")
|
||
|
||
def extract_bvid_from_file(filename: str) -> list:
|
||
"""从文件中取URL并提取BV号"""
|
||
bvids = []
|
||
try:
|
||
with open(filename, 'r', encoding='utf-8') as f:
|
||
for line in f:
|
||
url = line.strip()
|
||
if url:
|
||
# 使用正则表达式匹配BV号
|
||
match = re.search(r'BV[a-zA-Z0-9]{10}', url)
|
||
if match:
|
||
bvid = match.group()
|
||
bvids.append(bvid)
|
||
except Exception as e:
|
||
print(f"读取文件出错: {str(e)}")
|
||
return bvids
|
||
|
||
def batch_coin(cookies: Dict) -> None:
|
||
"""批量投币功能"""
|
||
print("\n" + "="*50)
|
||
print("# 批量投币")
|
||
print("="*50)
|
||
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
# 先检查今日投币情况
|
||
coin_exp_url = 'https://api.bilibili.com/x/web-interface/coin/today/exp'
|
||
coin_exp_resp = requests.get(coin_exp_url, cookies=cookies, headers=headers)
|
||
coin_exp_data = coin_exp_resp.json()
|
||
|
||
if coin_exp_data['code'] == 0:
|
||
today_exp = coin_exp_data['data']
|
||
max_coin_exp = 50
|
||
remaining_coins = (max_coin_exp - today_exp) // 10
|
||
|
||
if remaining_coins <= 0:
|
||
print("今日已完成投币任务!")
|
||
return
|
||
|
||
print(f"今日还可投币: {remaining_coins}个")
|
||
|
||
# 检查bvid.txt文件是否存在
|
||
if not os.path.exists('bvid.txt'):
|
||
print("错误:找不到bvid.txt文件!")
|
||
print("请创建bvid.txt文件并在其中每行写入一个视频链接")
|
||
return
|
||
|
||
# 从文件中读取��取BV号
|
||
bvids = extract_bvid_from_file('bvid.txt')
|
||
|
||
if not bvids:
|
||
print("错误:无法从文件中提取有效的BV号!")
|
||
return
|
||
|
||
print(f"从文件中提取到 {len(bvids)} 个BV号:")
|
||
# 获取每个视频的信息
|
||
for bvid in bvids:
|
||
get_video_info(bvid, headers)
|
||
|
||
# 询问是否继续投币
|
||
choice = input("\n是否开始批量投币?(y/n) [y]: ").lower()
|
||
if not choice or choice == 'y': # 直接回车或输入y都继续
|
||
# 获取csrf令牌
|
||
csrf = cookies.get('bili_jct')
|
||
if not csrf:
|
||
print("错误:无法获取csrf令牌!")
|
||
return
|
||
|
||
# 投API
|
||
coin_url = 'https://api.bilibili.com/x/web-interface/coin/add'
|
||
|
||
# 创建进度条
|
||
with tqdm(total=min(remaining_coins, len(bvids)), desc="投币进度") as pbar:
|
||
for bvid in bvids:
|
||
if remaining_coins <= 0:
|
||
print("\n今日投币任务完成!")
|
||
break
|
||
|
||
# 构造请求数据
|
||
data = {
|
||
'bvid': bvid,
|
||
'multiply': 1, # 投1个币
|
||
'select_like': 1, # 同时点赞
|
||
'csrf': csrf
|
||
}
|
||
|
||
try:
|
||
# 发送投币请求
|
||
response = requests.post(coin_url, data=data, cookies=cookies, headers=headers)
|
||
result = response.json()
|
||
|
||
if result['code'] == 0:
|
||
print(f"\n成功给视频 {bvid} 投币")
|
||
remaining_coins -= 1
|
||
pbar.update(1)
|
||
else:
|
||
print(f"\n给视频 {bvid} 投币失败:{result['message']}")
|
||
print("停止投币操作")
|
||
break # 投币失败就停止
|
||
|
||
# 添加延时,避免请求过快
|
||
time.sleep(2)
|
||
|
||
except Exception as e:
|
||
print(f"\n投币过程出错: {str(e)}")
|
||
print("停止投币操作")
|
||
break # 出错也停止
|
||
|
||
print("\n批量投币完成!")
|
||
|
||
# 获取当前硬币数
|
||
nav_url = 'https://api.bilibili.com/x/web-interface/nav'
|
||
nav_resp = requests.get(nav_url, cookies=cookies, headers=headers)
|
||
nav_data = nav_resp.json()
|
||
|
||
if nav_data['code'] == 0:
|
||
current_coins = nav_data['data']['money']
|
||
print(f"当前剩余硬币数: {current_coins}")
|
||
|
||
except Exception as e:
|
||
print(f"批量投币时出错: {str(e)}")
|
||
|
||
print("="*50 + "\n")
|
||
|
||
def show_menu() -> str:
|
||
"""显示功能菜单"""
|
||
print("\n" + "="*50)
|
||
print("B站工具箱")
|
||
print("="*50)
|
||
print("1. 批量点赞视频 (默认)")
|
||
print("2. 批量投币")
|
||
print("3. 下载视频音频")
|
||
print("4. 获取视频BV号")
|
||
print("5. 查看用户信息")
|
||
print("6. bv转av")
|
||
print("7. 获取视频弹幕")
|
||
print("8. 获取视频热评")
|
||
print("9. 查看IP地理位置")
|
||
print("10. 直播")
|
||
print("11. 弹幕点赞")
|
||
print("12. 老粉计划")
|
||
print("13. 举报视频")
|
||
print("14. 搜索视频") # 新增选项
|
||
print("15. 退出程序") # 退出选项改为15
|
||
print("="*50)
|
||
|
||
choice = input("请选择功能 (1-15) [1]: ").strip()
|
||
if not choice:
|
||
return '1'
|
||
if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15']:
|
||
return choice
|
||
print("无效的选择,使用默认选项1")
|
||
return '1'
|
||
|
||
def show_live_menu() -> str:
|
||
"""显示直播功能子菜单"""
|
||
print("\n" + "="*50)
|
||
print("直播功能")
|
||
print("="*50)
|
||
print("1. 获取直播间信息")
|
||
print("2. 获取用户������播状态")
|
||
print("3. 获取主播信息")
|
||
print("4. 获取多个直播间信息")
|
||
print("5. 批量查询直播状态")
|
||
print("6. 获取直播间历史弹幕")
|
||
print("7. 获取直播间播放信息")
|
||
print("8. 获取直播间主播详情")
|
||
print("9. ���回主菜单")
|
||
print("="*50)
|
||
|
||
choice = input("请选择功能 (1-9) [1]: ").strip()
|
||
if not choice:
|
||
return '1'
|
||
if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9']:
|
||
return choice
|
||
print("无效的选择,使用默认选项1")
|
||
return '1'
|
||
|
||
def extract_bvid(url_or_bvid: str) -> str:
|
||
"""从URL或直接输入的BV��中提取BV"""
|
||
# 如果直接输入的是BV号
|
||
if url_or_bvid.startswith('BV'):
|
||
return url_or_bvid
|
||
|
||
# 否则从URL中提取
|
||
bv_pattern = r'BV[a-zA-Z0-9]{10}'
|
||
match = re.search(bv_pattern, url_or_bvid)
|
||
if match:
|
||
return match.group()
|
||
return ''
|
||
|
||
def get_video_bvids() -> None:
|
||
"""获取视频BV号并保存到文件"""
|
||
bvids = []
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
print("请输入B站视频URL�����BV号,每行一���,输入q��束:")
|
||
while True:
|
||
input_text = input().strip()
|
||
if input_text.lower() == 'q':
|
||
break
|
||
|
||
# 如果输入的是BV号
|
||
if input_text.startswith('BV'):
|
||
bvid = input_text
|
||
bvids.append(bvid)
|
||
get_video_info(bvid, headers)
|
||
continue
|
||
|
||
# 如果输入的是URL
|
||
if not input_text.startswith(('https://www.bilibili.com', 'https://b23.tv')):
|
||
print("请输入有效的B站视频链或BV号!")
|
||
continue
|
||
|
||
bvid = extract_bvid(input_text)
|
||
if bvid:
|
||
bvids.append(bvid)
|
||
get_video_info(bvid, headers)
|
||
else:
|
||
print("无法从URL中提���BV号!")
|
||
|
||
if bvids:
|
||
with open('bvid.txt', 'w', encoding='utf-8') as f:
|
||
f.write('\n'.join(bvids))
|
||
print(f"已保存 {len(bvids)} 个BV号到 bvid.txt")
|
||
|
||
def get_video_info(bvid: str, headers: dict, show_info: bool = True) -> list:
|
||
"""获取并显示视频信息"""
|
||
try:
|
||
video_url = f'https://api.bilibili.com/x/web-interface/view?bvid={bvid}'
|
||
response = requests.get(video_url, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
info = data['data']
|
||
if show_info: # 只在需要时显示信息
|
||
print(f"\n视频信息 - {bvid}:")
|
||
print(f"标题: {info['title']}")
|
||
print(f"UP主: {info['owner']['name']}")
|
||
print(f"播放量: {info['stat']['view']}")
|
||
print(f"点赞数: {info['stat']['like']}")
|
||
print(f"投币数: {info['stat']['coin']}")
|
||
print(f"收藏数: {info['stat']['favorite']}")
|
||
print(f"分享数: {info['stat']['share']}")
|
||
# 获取并显示评论数
|
||
comment_count = get_comment_count(bvid)
|
||
print(f"评论数: {comment_count}")
|
||
print(f"发布时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(info['pubdate']))}")
|
||
print(f"视频简介: {info['desc'][:100]}..." if len(info['desc']) > 100 else f"视频简介: {info['desc']}")
|
||
print("-" * 50)
|
||
|
||
# ���回分P信息,供下载功能使用
|
||
return info.get('pages', [])
|
||
except Exception as e:
|
||
print(f"获取视频 {bvid} 信息失败: {str(e)}")
|
||
return []
|
||
|
||
def batch_like(cookies: Dict) -> None:
|
||
"""批量点赞功能"""
|
||
print("\n" + "="*50)
|
||
print("# 批量点赞")
|
||
print("="*50)
|
||
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
# 检查bvid.txt文件是��存在
|
||
if not os.path.exists('bvid.txt'):
|
||
print("错误:找不到bvid.txt文件!")
|
||
print("请创建bvid.txt文件并在其中每行写入一个视频链接")
|
||
return
|
||
|
||
# 从文件中读取并提取BV号
|
||
bvids = extract_bvid_from_file('bvid.txt')
|
||
|
||
if not bvids:
|
||
print("错误:无法从文件中提取有效的BV号!")
|
||
return
|
||
|
||
print(f"从文件中提取到 {len(bvids)} 个BV号:")
|
||
# 获取每个视频的信息
|
||
for bvid in bvids:
|
||
get_video_info(bvid, headers)
|
||
|
||
# 询问是否继续点赞
|
||
choice = input("\n是否开始批量点赞?(y/n) [y]: ").lower()
|
||
if not choice or choice == 'y':
|
||
# 获csrf令牌
|
||
csrf = cookies.get('bili_jct')
|
||
if not csrf:
|
||
print("错误:无法获取csrf令牌!")
|
||
return
|
||
|
||
# 点赞API
|
||
like_url = 'https://api.bilibili.com/x/web-interface/archive/like'
|
||
|
||
# 创建进度条
|
||
with tqdm(total=len(bvids), desc="点赞进度") as pbar:
|
||
for bvid in bvids:
|
||
# 构造请求数据
|
||
data = {
|
||
'bvid': bvid,
|
||
'like': 1, # 1表示点赞,2表示取消��赞
|
||
'csrf': csrf
|
||
}
|
||
|
||
try:
|
||
# 发送点赞请求
|
||
response = requests.post(like_url, data=data, cookies=cookies, headers=headers)
|
||
result = response.json()
|
||
|
||
if result['code'] == 0:
|
||
print(f"\n成功给视频 {bvid} 点赞")
|
||
pbar.update(1)
|
||
else:
|
||
print(f"\n给频 {bvid} 点赞失败{result['message']}")
|
||
print("��止点赞操作")
|
||
break # 点赞失败就停止
|
||
|
||
# 添加延时,避免请求过快
|
||
time.sleep(2)
|
||
|
||
except Exception as e:
|
||
print(f"\n点赞过程出错: {str(e)}")
|
||
print("停止点赞操作")
|
||
break # 出错也停止
|
||
|
||
print("\n批量点赞完成!")
|
||
|
||
except Exception as e:
|
||
print(f"批量点赞时出错: {str(e)}")
|
||
|
||
print("="*50 + "\n")
|
||
|
||
def download_single_audio(url: str, cookies: Dict):
|
||
"""��载单个音频"""
|
||
try:
|
||
cmd = [
|
||
'yt-dlp',
|
||
'--cookies', 'cookies.txt', # 直接使用当前目����的cookies.txt
|
||
'-f', 'ba',
|
||
'--no-playlist',
|
||
'--no-check-certificates',
|
||
'--progress',
|
||
'--newline',
|
||
'-o', os.path.join("音频", "%(title)s.%(ext)s"), # 指定下载路径
|
||
url
|
||
]
|
||
|
||
print("开始下载音频...")
|
||
|
||
process = subprocess.Popen(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
encoding='utf-8',
|
||
errors='replace',
|
||
bufsize=1,
|
||
universal_newlines=True
|
||
)
|
||
|
||
downloaded_file = None
|
||
pbar = None
|
||
total_size = None
|
||
|
||
# 用于解析下载进度的正则表达式
|
||
progress_pattern = re.compile(r'\[download\]\s+(\d+\.\d+)%\s+of\s+(\d+\.\d+)([KMG])iB\s+at\s+.*?ETA\s+(.*)')
|
||
|
||
while True:
|
||
output = process.stdout.readline()
|
||
if output == '' and process.poll() is not None:
|
||
break
|
||
|
||
if output:
|
||
output = output.strip()
|
||
|
||
# 获取文件名
|
||
if '[download] Destination:' in output:
|
||
downloaded_file = output.split('[download] Destination:', 1)[1].strip()
|
||
elif 'has already been downloaded' in output:
|
||
downloaded_file = output.split('[download] ', 1)[1].split(' has already', 1)[0].strip()
|
||
|
||
# 解析进度信息
|
||
match = progress_pattern.search(output)
|
||
if match:
|
||
percentage, size, unit, eta = match.groups()
|
||
|
||
# 计算总大小(转换为字节)
|
||
if total_size is None:
|
||
multiplier = {'K': 1024, 'M': 1024**2, 'G': 1024**3}
|
||
total_size = int(float(size) * multiplier[unit])
|
||
# 创建进度条
|
||
pbar = tqdm(
|
||
total=total_size,
|
||
unit='B',
|
||
unit_scale=True,
|
||
desc="下载进度",
|
||
ncols=80,
|
||
bar_format='{desc}: {percentage:3.1f}%|{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]'
|
||
)
|
||
|
||
# 更新进度条
|
||
if pbar:
|
||
current = int(total_size * float(percentage) / 100)
|
||
pbar.n = current
|
||
pbar.refresh()
|
||
|
||
# 其他输出信息
|
||
elif not output.startswith('[download]'):
|
||
print(output)
|
||
|
||
# 关闭进度条
|
||
if pbar:
|
||
pbar.close()
|
||
|
||
# 查下载结果
|
||
if process.returncode == 0:
|
||
print("\n下载完成!")
|
||
|
||
if downloaded_file and os.path.exists(downloaded_file):
|
||
print(f"找到音频文件: {downloaded_file}")
|
||
if input("是否转换为MP3格式?(y/n) [y]: ").lower() in ['', 'y']:
|
||
if find_ffmpeg():
|
||
convert_to_mp3(downloaded_file)
|
||
else:
|
||
print("未找到ffmpeg,无法转换为MP3格式")
|
||
else:
|
||
print(f"警告:无法找到下载的文件")
|
||
m4a_files = [f for f in os.listdir('.') if f.endswith('.m4a')]
|
||
if m4a_files:
|
||
print(f"在当前目录找到以下.m4a文件:")
|
||
for i, file in enumerate(m4a_files, 1):
|
||
print(f"{i}. {file}")
|
||
choice = input("请选择要转换的文件编号(输入q取消)[1]: ")
|
||
if not choice:
|
||
choice = '1'
|
||
if choice.isdigit() and 1 <= int(choice) <= len(m4a_files):
|
||
if find_ffmpeg():
|
||
convert_to_mp3(m4a_files[int(choice)-1])
|
||
else:
|
||
print("未找到ffmpeg,无法转换为MP3格式")
|
||
else:
|
||
error = process.stderr.read()
|
||
print(f"下载失败!错误���息:\n{error}")
|
||
|
||
except FileNotFoundError:
|
||
print("错误:请先安装 yt-dlp")
|
||
print("可以使用以下命令安装:")
|
||
print("pip install yt-dlp")
|
||
except Exception as e:
|
||
print(f"发生误: {str(e)}")
|
||
|
||
def create_required_directories():
|
||
"""创建程序所需的文件夹"""
|
||
required_dirs = [
|
||
"弹幕", # 存放视频弹幕文件
|
||
os.path.join("弹幕", "直播弹幕"), # 存放直播弹幕文件
|
||
"评论", # 存放评论文件
|
||
"音频", # 存放下载的音频文件
|
||
"举报", # 存放举报相关文件
|
||
os.path.join("举报", "图片"), # 存放举报附件图片
|
||
os.path.join("举报", "记录"), # 存放举报记录
|
||
]
|
||
|
||
for directory in required_dirs:
|
||
if not os.path.exists(directory):
|
||
try:
|
||
os.makedirs(directory)
|
||
print(f"创建文件夹: {directory}")
|
||
except Exception as e:
|
||
print(f"创建文件夹 {directory} 时出错: {str(e)}")
|
||
|
||
def main():
|
||
# 创建必要的文件夹
|
||
create_required_directories()
|
||
|
||
# 检查cookies.txt是否存在
|
||
if not os.path.exists('cookies.txt'): # 直接���查当前目录
|
||
print("错误:找不到cookies.txt文件!")
|
||
print("请确保cookies.txt文件存在且包含有效的cookies信息")
|
||
return
|
||
|
||
# 加载cookies
|
||
cookies = load_cookies_from_file()
|
||
if not cookies:
|
||
print("错误:无法从cookies.txt加载有效的cookies")
|
||
return
|
||
|
||
# 获取并显示用户信息
|
||
get_user_info(cookies)
|
||
|
||
# 创建LiveRoom实例
|
||
live_room = LiveRoom()
|
||
|
||
while True:
|
||
choice = show_menu()
|
||
|
||
if choice == '1':
|
||
# 批量点赞
|
||
batch_like(cookies)
|
||
|
||
elif choice == '2':
|
||
# 批量投币
|
||
batch_coin(cookies)
|
||
|
||
elif choice == '3':
|
||
# 下载视频音频
|
||
while True:
|
||
print("\n请输入B站视频URL或BV号 (输入q返回主菜单):")
|
||
input_text = input().strip()
|
||
|
||
if input_text.lower() == 'q':
|
||
break
|
||
|
||
# 如果输入的是BV号
|
||
if input_text.startswith('BV'):
|
||
download_audio(input_text, cookies)
|
||
continue
|
||
|
||
# 如果输入的是URL
|
||
if not input_text.startswith(('https://www.bilibili.com', 'https://b23.tv')):
|
||
print("请输入有效的B站视频链接或BV号!")
|
||
continue
|
||
|
||
download_audio(input_text, cookies)
|
||
|
||
elif choice == '4':
|
||
# ���取视频BV号
|
||
get_video_bvids()
|
||
|
||
elif choice == '5':
|
||
# 查看用户信息
|
||
get_user_info(cookies)
|
||
|
||
elif choice == '6':
|
||
# 新增的功能:bv转av
|
||
bvid = input("请输入BV号: ").strip()
|
||
if bvid.startswith('BV'):
|
||
av_number = bv_to_av(bvid)
|
||
print(f"对应AV号是: {av_number}")
|
||
else:
|
||
print("请输入有效的BV号!")
|
||
|
||
elif choice == '7':
|
||
# 获取视频弹幕
|
||
input_text = input("请输入视频URL或BV号: ").strip()
|
||
|
||
# 如果输入的��BV号
|
||
if input_text.startswith('BV'):
|
||
bvid = input_text
|
||
# 如果输入的是URL
|
||
elif input_text.startswith(('https://www.bilibili.com', 'https://b23.tv')):
|
||
bvid = extract_bvid(input_text)
|
||
else:
|
||
print("请输入有效的B站视频链接或BV号!")
|
||
continue
|
||
|
||
if bvid:
|
||
try:
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
video_url = f'https://api.bilibili.com/x/web-interface/view?bvid={bvid}'
|
||
response = requests.get(video_url, headers=headers)
|
||
data = response.json()
|
||
if data['code'] == 0:
|
||
cid = data['data']['cid']
|
||
segment = input("请输入要获取第几个6分钟段的弹幕 (1-n) [1]: ").strip()
|
||
if not segment:
|
||
segment = "1"
|
||
if segment.isdigit():
|
||
get_danmaku(cid, bvid, int(segment), cookies) # 传入cookies参数
|
||
else:
|
||
print("请输入有效的数字!")
|
||
else:
|
||
print(f"获取视频信息失败:{data['message']}")
|
||
except Exception as e:
|
||
print(f"获取弹幕时出错: {str(e)}")
|
||
else:
|
||
print("无法��别BV号")
|
||
|
||
elif choice == '8':
|
||
# 获取视频热评
|
||
input_text = input("请输入视频URL或BV号: ").strip()
|
||
|
||
# 如果输入的BV号
|
||
if input_text.startswith('BV'):
|
||
bvid = input_text
|
||
# 如果输入的是URL
|
||
elif input_text.startswith(('https://www.bilibili.com', 'https://b23.tv')):
|
||
bvid = extract_bvid(input_text)
|
||
else:
|
||
print("请输入有效的B站视频链接或BV号!")
|
||
continue
|
||
|
||
if bvid:
|
||
# 获取每页评论数
|
||
ps = input("请输入每页显示的评论数 (1-49) [20]: ").strip()
|
||
if not ps:
|
||
ps = "20"
|
||
if ps.isdigit() and 1 <= int(ps) <= 49:
|
||
get_hot_comments(bvid, int(ps))
|
||
else:
|
||
print("请输入有效的数字!")
|
||
else:
|
||
print("无法识别BV号!")
|
||
|
||
elif choice == '9':
|
||
# 查看IP地理位置
|
||
get_ip_location()
|
||
|
||
elif choice == '10':
|
||
# 进入直播子菜单
|
||
while True:
|
||
live_choice = show_live_menu()
|
||
|
||
if live_choice == '1':
|
||
room_input = input("请输入直播间号或直播间链接: ").strip()
|
||
room_id = extract_room_id(room_input) if 'live.bilibili.com' in room_input else room_input
|
||
if room_id.isdigit():
|
||
live_room.get_room_info(room_id)
|
||
else:
|
||
print("请输入有效的直播间号或链接!")
|
||
input("\n按回车继续...") # 添加这一行
|
||
continue # 继续显示直播菜单
|
||
|
||
elif live_choice == '2':
|
||
uid_input = input("请输入用户UID或个人空间链接: ").strip()
|
||
if 'space.bilibili.com' in uid_input:
|
||
uid = extract_uid(uid_input)
|
||
else:
|
||
uid = uid_input
|
||
if uid.isdigit():
|
||
live_room.get_user_live_status(uid)
|
||
else:
|
||
print("请输入有效的用户UID或个人空间链接!")
|
||
input("\n按回车继续...") # 添加这一行
|
||
continue # 继续显示直播菜单
|
||
|
||
elif live_choice == '3':
|
||
uid_input = input("请输入主播UID或直播间链接: ").strip()
|
||
if 'live.bilibili.com' in uid_input:
|
||
uid = extract_uid(uid_input)
|
||
else:
|
||
uid = uid_input
|
||
if uid.isdigit():
|
||
live_room.get_anchor_info(uid)
|
||
else:
|
||
print("请输入有效的主播UID或直播间链接!")
|
||
input("\n按回车继续...") # 添加这一行
|
||
continue # 继���显示直播菜单
|
||
|
||
elif live_choice == '4':
|
||
room_input = input("请输入直播间号或直播间链接: ").strip()
|
||
live_room.get_room_base_info([room_input])
|
||
input("\n按回车继续...") # 添加这一行
|
||
continue # 继续显示直播菜单
|
||
|
||
elif live_choice == '5':
|
||
live_room.get_batch_live_status([room_input])
|
||
input("\n按回车继续...") # 添加���一行
|
||
continue # 继续��示直播菜单
|
||
|
||
elif live_choice == '6':
|
||
room_input = input("请输入直播间号或直播间链接: ").strip()
|
||
live_room.get_history_danmaku(room_input)
|
||
input("\n按回车继续...") # 添加这一行
|
||
continue # 继续显示直播菜单
|
||
|
||
elif live_choice == '7':
|
||
room_input = input("请输入直播间号或直播间链接: ").strip()
|
||
live_room.get_play_info(room_input)
|
||
input("\n按回车继续...") # 添加这一行
|
||
continue # 继续显示直播菜单
|
||
|
||
elif live_choice == '8':
|
||
room_input = input("请输入直播间号或直播间链接: ").strip()
|
||
live_room.get_room_anchor_info(room_input)
|
||
input("\n按回车继续...") # 添加这一行
|
||
continue # 继续显示直播菜单
|
||
|
||
elif live_choice == '9':
|
||
break # 返回主菜单
|
||
|
||
elif choice == '11':
|
||
# 弹幕点赞功能
|
||
input_text = input("请输入视频URL或BV号: ").strip()
|
||
|
||
# 如果输入的是BV号
|
||
if input_text.startswith('BV'):
|
||
bvid = input_text
|
||
# 如果输入的是URL
|
||
elif input_text.startswith(('https://www.bilibili.com', 'https://b23.tv')):
|
||
bvid = extract_bvid(input_text)
|
||
else:
|
||
print("请输入有效的B站视频链接或BV号!")
|
||
continue
|
||
|
||
if bvid:
|
||
try:
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
video_url = f'https://api.bilibili.com/x/web-interface/view?bvid={bvid}'
|
||
response = requests.get(video_url, headers=headers)
|
||
data = response.json()
|
||
if data['code'] == 0:
|
||
cid = data['data']['cid']
|
||
segment = input("请输入要获取第几个6分钟段的弹幕 (1-n) [1]: ").strip()
|
||
if not segment:
|
||
segment = "1"
|
||
if segment.isdigit():
|
||
get_danmaku(cid, bvid, int(segment), cookies) # 传入cookies参数
|
||
else:
|
||
print("请输入有效的数字!")
|
||
else:
|
||
print(f"获取视频信息失败:{data['message']}")
|
||
except Exception as e:
|
||
print(f"获取弹幕时出错: {str(e)}")
|
||
else:
|
||
print("无法识别BV号")
|
||
|
||
elif choice == '12': # 原来的退出选项改为12
|
||
print("\n=== 老粉计划 ===")
|
||
print("1. 加入老粉计划")
|
||
print("2. 发送老粉留言")
|
||
print("3. 返回主菜单")
|
||
|
||
fan_choice = input("\n请选择功能 (1-3) [1]: ").strip()
|
||
if not fan_choice:
|
||
fan_choice = "1"
|
||
|
||
if fan_choice == "1":
|
||
up_input = input("\n请输入UP主UID或空间链接: ").strip()
|
||
if 'space.bilibili.com' in up_input:
|
||
up_mid = extract_uid(up_input)
|
||
else:
|
||
up_mid = up_input
|
||
|
||
if up_mid.isdigit():
|
||
join_old_fan_plan(up_mid, cookies)
|
||
else:
|
||
print("请输入有效的UP主UID或空间链接!")
|
||
|
||
elif fan_choice == "2":
|
||
up_input = input("\n请输入UP主UID或空间链接: ").strip()
|
||
if 'space.bilibili.com' in up_input:
|
||
up_mid = extract_uid(up_input)
|
||
else:
|
||
up_mid = up_input
|
||
|
||
if up_mid.isdigit():
|
||
message = input("请输入要发送的留言: ").strip()
|
||
if message:
|
||
send_old_fan_message(up_mid, message, cookies)
|
||
else:
|
||
print("留言内容不能为空!")
|
||
else:
|
||
print("请输入有效的UP主UID或空间链接!")
|
||
|
||
input("\n按回车继续...") # 添加这行防止直接退出
|
||
continue # 继续显示主菜单
|
||
|
||
elif choice == '13':
|
||
# 举报视频
|
||
input_text = input("请输入要举报的视频URL或BV号: ").strip()
|
||
|
||
# 如果输入的是BV号
|
||
if input_text.startswith('BV'):
|
||
bvid = input_text
|
||
# 如果输入的是URL
|
||
elif input_text.startswith(('https://www.bilibili.com', 'https://b23.tv')):
|
||
bvid = extract_bvid(input_text)
|
||
else:
|
||
print("请输入有效的B站视频链接或BV号!")
|
||
continue
|
||
|
||
if bvid:
|
||
show_report_menu(bvid, cookies)
|
||
else:
|
||
print("无法识别BV号!")
|
||
|
||
elif choice == '14':
|
||
show_search_menu(cookies) # 传入cookies
|
||
|
||
elif choice == '15':
|
||
print("\n感谢使用!再见!")
|
||
break
|
||
|
||
def bv_to_av(bvid: str) -> str:
|
||
"""将BV号转换为AV号"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
# 使用B站API获取视信息
|
||
video_url = f'https://api.bilibili.com/x/web-interface/view?bvid={bvid}'
|
||
response = requests.get(video_url, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
# 从返回数据中获取aid(AV号)
|
||
av_number = data['data']['aid']
|
||
return str(av_number) # 直接返回数字部分
|
||
else:
|
||
print(f"获取AV号失败:{data['message']}")
|
||
return ""
|
||
|
||
except Exception as e:
|
||
print(f"转换过程出错: {str(e)}")
|
||
return ""
|
||
|
||
def get_color_name(hex_color: str) -> str:
|
||
"""将十六进制颜色代码转换为颜色名称"""
|
||
# 常见B站弹幕颜色映射
|
||
color_map = {
|
||
'#ffffff': '白色',
|
||
'#000000': '黑色',
|
||
'#ff0000': '红色',
|
||
'#00ff00': '绿色',
|
||
'#0000ff': '蓝色',
|
||
'#ffff00': '黄色',
|
||
'#ff7600': '橙色',
|
||
'#ff00ff': '粉色',
|
||
'#00ffff': '青色',
|
||
'#64c8ff': '浅蓝',
|
||
'#ff8080': '��红',
|
||
'#66ccff': '天蓝',
|
||
'#ff69b4': '粉红',
|
||
'#ff69b4': '热粉红',
|
||
'#98fb98': '浅绿',
|
||
'#ffa500': '橙色',
|
||
'#f0e68c': '卡其色',
|
||
'#87ceeb': '天蓝色',
|
||
'#dda0dd': '梅红色',
|
||
'#ff7f50': '珊瑚色',
|
||
}
|
||
return color_map.get(hex_color.lower(), hex_color)
|
||
|
||
def like_danmaku(dmid: str, oid: str, cookies: Dict) -> bool:
|
||
"""点赞弹幕
|
||
|
||
Args:
|
||
dmid (str): 弹幕ID
|
||
oid (str): 视频cid
|
||
cookies (Dict): cookies信息
|
||
|
||
Returns:
|
||
bool: 是否点赞成功
|
||
"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
# 获取csrf令牌
|
||
csrf = cookies.get('bili_jct')
|
||
if not csrf:
|
||
print("错误:无法获取csrf令牌!")
|
||
return False
|
||
|
||
# 构造请求数据
|
||
data = {
|
||
'dmid': dmid,
|
||
'oid': oid,
|
||
'op': 1, # 1表示点赞,2表示取消点赞
|
||
'platform': 'web_player',
|
||
'csrf': csrf
|
||
}
|
||
|
||
# 发送点赞请求
|
||
url = 'https://api.bilibili.com/x/v2/dm/thumbup/add'
|
||
response = requests.post(url, data=data, cookies=cookies, headers=headers)
|
||
result = response.json()
|
||
|
||
if result['code'] == 0:
|
||
print(f"成功给弹幕 {dmid} 点赞")
|
||
return True
|
||
else:
|
||
error_messages = {
|
||
-101: "账号未登录",
|
||
-111: "csrf校验失败",
|
||
-400: "请求错误",
|
||
36106: "���弹幕已被删除",
|
||
36805: "该视频禁止点赞弹幕",
|
||
65004: "取消赞失败,未点赞过",
|
||
65006: "已赞过该弹幕"
|
||
}
|
||
error_msg = error_messages.get(result['code'], result['message'])
|
||
print(f"点赞弹幕失败:{error_msg}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"点赞弹幕时出错: {str(e)}")
|
||
return False
|
||
|
||
def get_danmaku(cid: str, bvid: str, segment_index: int = 1, cookies: Dict = None) -> None:
|
||
"""获取视频弹幕"""
|
||
try:
|
||
import google.protobuf.text_format as text_format
|
||
from bilibili.community.service.dm.v1 import dm_pb2 as Danmaku
|
||
except ImportError:
|
||
print("错误:缺少必要的库,请先安装:")
|
||
print("pip install protobuf")
|
||
print("同时需要编译proto文件生成dm_pb2.py")
|
||
return
|
||
|
||
url = 'https://api.bilibili.com/x/v2/dm/web/seg.so'
|
||
params = {
|
||
'type': 1,
|
||
'oid': cid,
|
||
'segment_index': segment_index
|
||
}
|
||
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com'
|
||
}
|
||
|
||
try:
|
||
resp = requests.get(url, params=params, headers=headers)
|
||
if resp.status_code == 200:
|
||
danmaku_seg = Danmaku.DmSegMobileReply()
|
||
danmaku_seg.ParseFromString(resp.content)
|
||
|
||
danmaku_list = []
|
||
|
||
print(f"\n获取到 {len(danmaku_seg.elems)} 条弹幕:")
|
||
print("-" * 50)
|
||
|
||
for elem in danmaku_seg.elems:
|
||
seconds = elem.progress / 1000
|
||
minutes = int(seconds / 60)
|
||
seconds = int(seconds % 60)
|
||
|
||
mode_types = {
|
||
1: "普通弹幕",
|
||
2: "普通弹幕",
|
||
3: "普通弹幕",
|
||
4: "底部弹幕",
|
||
5: "顶部弹幕",
|
||
6: "逆向弹幕",
|
||
7: "高级弹幕",
|
||
8: "代码弹幕",
|
||
9: "BAS弹幕"
|
||
}
|
||
mode_type = mode_types.get(elem.mode, "未知类型")
|
||
|
||
hex_color = f"#{elem.color:06x}"
|
||
color_name = get_color_name(hex_color)
|
||
|
||
danmaku_info = (
|
||
f"时间: {minutes:02d}:{seconds:02d}\n"
|
||
f"内容: {elem.content}\n"
|
||
f"类型: {mode_type}\n"
|
||
f"颜色: {color_name} ({hex_color})\n"
|
||
f"字号: {elem.fontsize}\n"
|
||
f"发送时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(elem.ctime))}\n"
|
||
f"弹幕ID: {elem.id}\n" # 添加弹幕ID显示
|
||
f"{'-' * 50}"
|
||
)
|
||
|
||
# 只打印一次弹幕信息
|
||
print(danmaku_info)
|
||
|
||
# 询问是否点赞
|
||
choice = input("是否点赞该弹幕?(y/n) [n]: ").lower()
|
||
if choice == 'y':
|
||
like_danmaku(str(elem.id), cid, cookies)
|
||
|
||
danmaku_list.append(danmaku_info)
|
||
|
||
# 询问是否保存到文件
|
||
if danmaku_list:
|
||
choice = input("\n是否保存弹幕到文本文件?(y/n) [y]: ").lower()
|
||
if not choice or choice == 'y':
|
||
danmaku_dir = "弹幕"
|
||
if not os.path.exists(danmaku_dir):
|
||
os.makedirs(danmaku_dir)
|
||
|
||
filename = f"{bvid} {time.strftime('%Y-%m-%d %H-%M-%S', time.localtime())} 弹幕.txt"
|
||
filepath = os.path.join(danmaku_dir, filename)
|
||
|
||
try:
|
||
with open(filepath, 'w', encoding='utf-8') as f:
|
||
f.write('\n'.join(danmaku_list))
|
||
print(f"弹幕已保存到文件: {filepath}")
|
||
except Exception as e:
|
||
print(f"保存文件时出错: {str(e)}")
|
||
|
||
else:
|
||
print(f"获取弹幕失败: HTTP {resp.status_code}")
|
||
|
||
except Exception as e:
|
||
print(f"获取弹幕时出错: {str(e)}")
|
||
print("���误详细信息:")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def get_comment_count(bvid: str) -> int:
|
||
"""获取视频评论总数"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
# 首先获取视频aid
|
||
video_url = f'https://api.bilibili.com/x/web-interface/view?bvid={bvid}'
|
||
response = requests.get(video_url, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
aid = data['data']['aid']
|
||
|
||
# 然后获取评论总数
|
||
count_url = 'https://api.bilibili.com/x/v2/reply/count'
|
||
params = {
|
||
'type': 1, # 视频评论区
|
||
'oid': aid # 视频aid
|
||
}
|
||
|
||
count_response = requests.get(count_url, params=params, headers=headers)
|
||
count_data = count_response.json()
|
||
|
||
if count_data['code'] == 0:
|
||
return count_data['data']['count']
|
||
else:
|
||
print(f"获取评数失败:{count_data['message']}")
|
||
return 0
|
||
else:
|
||
print(f"获取视频信息失败:{data['message']}")
|
||
return 0
|
||
|
||
except Exception as e:
|
||
print(f"获取评论数时出错: {str(e)}")
|
||
return 0
|
||
|
||
def get_hot_comments(bvid: str, ps: int = 20, pn: int = 1) -> None:
|
||
"""获取视频热门评论"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
# 首先取视频aid
|
||
video_url = f'https://api.bilibili.com/x/web-interface/view?bvid={bvid}'
|
||
response = requests.get(video_url, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
aid = data['data']['aid']
|
||
|
||
# 获取热评
|
||
hot_url = 'https://api.bilibili.com/x/v2/reply/hot'
|
||
params = {
|
||
'type': 1, # 视频评论区
|
||
'oid': aid, # 视频aid
|
||
'ps': ps, # 每页评论数
|
||
'pn': pn # 页码
|
||
}
|
||
|
||
hot_response = requests.get(hot_url, params=params, headers=headers)
|
||
hot_data = hot_response.json()
|
||
|
||
if hot_data['code'] == 0:
|
||
# ��查data字段是否存在
|
||
if 'data' not in hot_data or hot_data['data'] is None:
|
||
print("该视频暂无热评")
|
||
return
|
||
|
||
# 检查replies字段是否存在
|
||
if 'replies' not in hot_data['data'] or not hot_data['data']['replies']:
|
||
print("该视频暂无热���")
|
||
return
|
||
|
||
page_info = hot_data['data'].get('page', {'acount': 0})
|
||
replies = hot_data['data']['replies']
|
||
|
||
print(f"\n获取到 {len(replies)} 条热门评论(总评论数:{page_info.get('acount', 0)}):")
|
||
print("="*50)
|
||
|
||
for reply in replies:
|
||
# 获取评论者信息
|
||
member = reply['member']
|
||
username = member['uname'] # 用户名
|
||
user_level = member['level_info']['current_level'] # 用户等级
|
||
user_id = member['mid'] # 用户ID
|
||
|
||
# 获取评论内容
|
||
content = reply['content']['message']
|
||
# 获取点赞数
|
||
like_count = reply['like']
|
||
# 获取回复数
|
||
reply_count = reply['rcount']
|
||
# 获取发布时间
|
||
ctime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(reply['ctime']))
|
||
|
||
print(f"评论者: {username} (UID: {user_id})")
|
||
print(f"用户等级: {user_level}")
|
||
print(f"点赞数: {like_count}")
|
||
print(f"回复数: {reply_count}")
|
||
print(f"发布时间: {ctime}")
|
||
print(f"评论内容: {content}")
|
||
print("-"*50)
|
||
|
||
# 询问是否保存到文件
|
||
choice = input("\n是否保存热评到文本文件?(y/n) [y]: ").lower()
|
||
if not choice or choice == 'y':
|
||
# 创建评论文件夹(如果不存在)
|
||
comment_dir = "评论"
|
||
if not os.path.exists(comment_dir):
|
||
os.makedirs(comment_dir)
|
||
|
||
# 生成文件名(BV号 + 当前���间 + 评论后���)
|
||
filename = f"{bvid} {time.strftime('%Y-%m-%d %H-%M-%S', time.localtime())} 评论.txt"
|
||
# 完整文件路径
|
||
filepath = os.path.join(comment_dir, filename)
|
||
|
||
try:
|
||
with open(filepath, 'w', encoding='utf-8') as f:
|
||
f.write(f"视频热门评论(总评论数:{page_info.get('acount', 0)})\n")
|
||
f.write("="*50 + "\n")
|
||
for reply in replies:
|
||
member = reply['member']
|
||
f.write(f"评论者: {member['uname']} (UID: {member['mid']})\n")
|
||
f.write(f"用户等级: {member['level_info']['current_level']}\n")
|
||
f.write(f"点赞数: {reply['like']}\n")
|
||
f.write(f"回复数: {reply['rcount']}\n")
|
||
f.write(f"发布时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(reply['ctime']))}\n")
|
||
f.write(f"评论内容: {reply['content']['message']}\n")
|
||
f.write("-"*50 + "\n")
|
||
print(f"热评已保存到文件: {filepath}")
|
||
except Exception as e:
|
||
print(f"保存文件���出��: {str(e)}")
|
||
else:
|
||
print(f"获取热评失败:{hot_data['message']}")
|
||
else:
|
||
print(f"获取视频信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取热评时出错: {str(e)}")
|
||
print("请确保视频存在且有评论权限")
|
||
|
||
def get_ip_location():
|
||
"""获取IP地理位置信息"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
while True:
|
||
print("\n请选择查询方式:")
|
||
print("1. 查询当前IP")
|
||
print("2. 查询指定IP")
|
||
print("3. 返回主菜单")
|
||
|
||
choice = input("请选择 [1]: ").strip()
|
||
if not choice:
|
||
choice = "1"
|
||
|
||
if choice == "1":
|
||
# 查询当前IP
|
||
url = 'https://api.bilibili.com/x/web-interface/zone'
|
||
response = requests.get(url, headers=headers)
|
||
data = response.json()
|
||
|
||
elif choice == "2":
|
||
# 查询指定IP
|
||
ip = input("请输入要查询的IP地址: ").strip()
|
||
url = 'https://api.live.bilibili.com/ip_service/v1/ip_service/get_ip_addr'
|
||
params = {'ip': ip}
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
elif choice == "3":
|
||
break
|
||
else:
|
||
print("无效的选择!")
|
||
continue
|
||
|
||
if data['code'] == 0:
|
||
ip_info = data['data']
|
||
print("\nIP地理位置信息:")
|
||
print("="*50)
|
||
print(f"IP地址: {ip_info['addr']}")
|
||
print(f"国家/地区: {ip_info['country']}")
|
||
if 'province' in ip_info and ip_info['province']:
|
||
print(f"省/州: {ip_info['province']}")
|
||
if 'city' in ip_info and ip_info['city']:
|
||
print(f"城市: {ip_info['city']}")
|
||
print(f"运营商: {ip_info['isp']}")
|
||
if 'longitude' in ip_info and ip_info['longitude']:
|
||
print(f"经度: {ip_info['longitude']}")
|
||
if 'latitude' in ip_info and ip_info['latitude']:
|
||
print(f"纬度: {ip_info['latitude']}")
|
||
print("="*50)
|
||
else:
|
||
print(f"获取IP信息失败:{data.get('message', '') or data.get('msg', '')}")
|
||
|
||
input("\n按回车继续...")
|
||
|
||
except Exception as e:
|
||
print(f"获取IP信息时出错: {str(e)}")
|
||
|
||
def get_live_room_info(room_id: str) -> None:
|
||
"""获取直播间信息"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.live.bilibili.com/room/v1/Room/get_info'
|
||
params = {'room_id': room_id}
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
room_info = data['data']
|
||
print("\n直播间信息:")
|
||
print("="*50)
|
||
print(f"主��UID: {room_info['uid']}")
|
||
print(f"直播间号: {room_info['room_id']}")
|
||
if room_info['short_id'] != 0:
|
||
print(f"直播间短号: {room_info['short_id']}")
|
||
print(f"关注数: {room_info['attention']}")
|
||
print(f"观看人数: {room_info['online']}")
|
||
print(f"标题: {room_info['title']}")
|
||
print(f"分区: {room_info['parent_area_name']} - {room_info['area_name']}")
|
||
|
||
# 显示直播状态
|
||
status_map = {
|
||
0: "未开播",
|
||
1: "直播中",
|
||
2: "轮播中"
|
||
}
|
||
status = status_map.get(room_info['live_status'], "未知状态")
|
||
print(f"直播状态: {status}")
|
||
|
||
if room_info['live_status'] == 1:
|
||
print(f"开播时间: {room_info['live_time']}")
|
||
|
||
if room_info['description']:
|
||
print(f"\n房间简介: {room_info['description']}")
|
||
|
||
# 显示认证信息
|
||
if room_info['new_pendants']['badge']:
|
||
badge = room_info['new_pendants']['badge']
|
||
badge_type = {
|
||
'v_person': '个人认证',
|
||
'v_company': '企业认证'
|
||
}
|
||
print(f"\n认证类型: {badge_type.get(badge['name'], '未知认证')}")
|
||
if badge['desc']:
|
||
print(f"认证说明: {badge['desc']}")
|
||
|
||
# 显示热词
|
||
if room_info['hot_words']:
|
||
print("\n房间热词:")
|
||
for word in room_info['hot_words']:
|
||
print(f"- {word}")
|
||
|
||
print("="*50)
|
||
|
||
else:
|
||
print(f"获取直播间信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取直播间信息时出错: {str(e)}")
|
||
|
||
def get_user_live_status(mid: str) -> None:
|
||
"""获取用户直播间状态"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.live.bilibili.com/room/v1/Room/getRoomInfoOld'
|
||
params = {'mid': mid}
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
live_info = data['data']
|
||
print("\n用户直播间状态:")
|
||
print("="*50)
|
||
|
||
# 显示房间状态
|
||
room_status = "有直播间" if live_info['roomStatus'] == 1 else "无直播间"
|
||
print(f"房间状态: {room_status}")
|
||
|
||
if live_info['roomStatus'] == 1:
|
||
# 显示直播状态
|
||
live_status_map = {
|
||
0: "未开播",
|
||
1: "直播中"
|
||
}
|
||
live_status = live_status_map.get(live_info['live_status'], "未知状态")
|
||
print(f"直播状态: {live_status}")
|
||
|
||
# 显示轮播状态
|
||
round_status = "轮播中" if live_info['roundStatus'] == 1 else "未轮播"
|
||
print(f"轮播状态: {round_status}")
|
||
|
||
print(f"直播间ID: {live_info['roomid']}")
|
||
print(f"直播间标题: {live_info['title']}")
|
||
print(f"观看人数: {live_info['online']}")
|
||
print(f"直播间地址: {live_info['url']}")
|
||
if live_info['cover']:
|
||
print(f"封面地址: {live_info['cover']}")
|
||
|
||
print("="*50)
|
||
|
||
else:
|
||
print(f"获取直播间状态失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取直播间状态时出错: {str(e)}")
|
||
|
||
def get_room_init_info(room_id: str) -> None:
|
||
"""获取直播间初始化信息"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.live.bilibili.com/room/v1/Room/room_init'
|
||
params = {'id': room_id}
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
room_info = data['data']
|
||
print("\n直播间初始化信息:")
|
||
print("="*50)
|
||
print(f"真实房间号: {room_info['room_id']}")
|
||
print(f"短号: {room_info['short_id']}")
|
||
print(f"主播UID: {room_info['uid']}")
|
||
|
||
# 显示直播状态
|
||
status_map = {
|
||
0: "未开播",
|
||
1: "直播中",
|
||
2: "轮播中"
|
||
}
|
||
status = status_map.get(room_info['live_status'], "未知状态")
|
||
print(f"直播状态: {status}")
|
||
|
||
# 显示开播时间
|
||
if room_info['live_time'] > 0:
|
||
live_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(room_info['live_time']))
|
||
print(f"开播时间: {live_time}")
|
||
|
||
# 显示房间类型
|
||
special_type_map = {
|
||
0: "普通直播间",
|
||
1: "付费直播间",
|
||
2: "拜年祭直播间"
|
||
}
|
||
special_type = special_type_map.get(room_info['special_type'], "未知类型")
|
||
print(f"房间类型: {special_type}")
|
||
|
||
# 显示其��状态
|
||
print(f"是否隐藏: {'是' if room_info['is_hidden'] else '否'}")
|
||
print(f"是否锁���: {'是' if room_info['is_locked'] else '否'}")
|
||
print(f"是否竖屏: {'是' if room_info['is_portrait'] else '否'}")
|
||
print(f"是否加密: {'是' if room_info['encrypted'] else '否'}")
|
||
if room_info['encrypted']:
|
||
print(f"密码验证: {'已通过' if room_info['pwd_verified'] else '未通过'}")
|
||
|
||
print("="*50)
|
||
|
||
else:
|
||
print(f"获取直播间信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取直播间信息时出错: {str(e)}")
|
||
|
||
def get_anchor_info(uid: str) -> None:
|
||
"""获取主播信息"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.live.bilibili.com/live_user/v1/Master/info'
|
||
params = {'uid': uid}
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
anchor_data = data['data']
|
||
info = anchor_data['info']
|
||
exp = anchor_data['exp']
|
||
|
||
print("\n主播信息:")
|
||
print("="*50)
|
||
print(f"UID: {info['uid']}")
|
||
print(f"用户名: {info['uname']}")
|
||
print(f"头像: {info['face']}")
|
||
|
||
# 显示性别
|
||
gender_map = {
|
||
-1: "保密",
|
||
0: "女",
|
||
1: "男"
|
||
}
|
||
gender = gender_map.get(info['gender'], "未知")
|
||
print(f"性别: {gender}")
|
||
|
||
# 显示认证信息
|
||
if info['official_verify']['type'] != -1:
|
||
verify_type = "个人认证" if info['official_verify']['type'] == 0 else "机构认证"
|
||
print(f"认证类型: {verify_type}")
|
||
print(f"认证说明: {info['official_verify']['desc']}")
|
||
|
||
# 显示等级信息
|
||
level_info = exp['master_level']
|
||
print(f"\n主播等级: {level_info['level']}")
|
||
print(f"当前经验: {level_info['current'][1]}/{level_info['next'][1]}")
|
||
print(f"升级还需: {level_info['next'][0] - level_info['current'][0]}")
|
||
|
||
# 显示其他信息
|
||
print(f"\n粉丝数: {anchor_data['follower_num']}")
|
||
print(f"直播间ID: {anchor_data['room_id']}")
|
||
if anchor_data['medal_name']:
|
||
print(f"粉丝勋章: {anchor_data['medal_name']}")
|
||
if anchor_data['glory_count'] > 0:
|
||
print(f"主播荣誉数: {anchor_data['glory_count']}")
|
||
if anchor_data['pendant']:
|
||
print(f"头像框: {anchor_data['pendant']}")
|
||
|
||
# 显示公告
|
||
if anchor_data['room_news']['content']:
|
||
print("\n主播公告:")
|
||
print(f"内容: {anchor_data['room_news']['content']}")
|
||
print(f"时��: {anchor_data['room_news']['ctime_text']}")
|
||
|
||
print("="*50)
|
||
|
||
else:
|
||
print(f"获取主播信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取主播信息时出错: {str(e)}")
|
||
|
||
def get_room_base_info(room_ids: list) -> None:
|
||
"""获取多个直播间基本信息"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.live.bilibili.com/xlive/web-room/v1/index/getRoomBaseInfo'
|
||
params = {
|
||
'req_biz': 'web_room_componet',
|
||
}
|
||
# 添加多个房间号
|
||
for room_id in room_ids:
|
||
params[f'room_ids'] = room_id
|
||
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
rooms_info = data['data']['by_room_ids']
|
||
|
||
for room_id, room_info in rooms_info.items():
|
||
print(f"\n直播间 {room_id} 基本信息:")
|
||
print("="*50)
|
||
print(f"主播: {room_info['uname']} (UID: {room_info['uid']})")
|
||
print(f"真实房间号: {room_info['room_id']}")
|
||
if room_info['short_id'] != 0:
|
||
print(f"短号: {room_info['short_id']}")
|
||
print(f"标题: {room_info['title']}")
|
||
print(f"分区: {room_info['parent_area_name']} - {room_info['area_name']}")
|
||
|
||
# 显示直播状态
|
||
status_map = {
|
||
0: "未开播",
|
||
1: "直播中",
|
||
2: "轮播中"
|
||
}
|
||
status = status_map.get(room_info['live_status'], "未知��态")
|
||
print(f"直播状态: {status}")
|
||
|
||
print(f"关注数: {room_info['attention']}")
|
||
print(f"在线人数: {room_info['online']}")
|
||
|
||
if room_info['live_time'] != "0000-00-00 00:00:00":
|
||
print(f"开播时间: {room_info['live_time']}")
|
||
|
||
if room_info['description']:
|
||
print(f"\n房间简介: {room_info['description']}")
|
||
|
||
if room_info['tags']:
|
||
print(f"标签: {room_info['tags']}")
|
||
|
||
if room_info['cover']:
|
||
print(f"封面: {room_info['cover']}")
|
||
|
||
if room_info['background']:
|
||
print(f"背景: {room_info['background']}")
|
||
|
||
print(f"直播间地址: {room_info['live_url']}")
|
||
print("="*50)
|
||
|
||
else:
|
||
print(f"获取直播间信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取直播间信息时�����错: {str(e)}")
|
||
|
||
def get_batch_live_status(uids: list) -> None:
|
||
"""批量查询直播间状态"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.live.bilibili.com/room/v1/Room/get_status_info_by_uids'
|
||
# 构造请求参数
|
||
params = {}
|
||
for i, uid in enumerate(uids):
|
||
params[f'uids[]'] = uid
|
||
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
rooms_info = data['data']
|
||
|
||
for uid, room_info in rooms_info.items():
|
||
print(f"\n主播 {room_info['uname']} (UID: {uid}) 的直播状态:")
|
||
print("="*50)
|
||
print(f"直播间标题: {room_info['title']}")
|
||
print(f"房间号: {room_info['room_id']}")
|
||
if room_info['short_id'] != 0:
|
||
print(f"短号: {room_info['short_id']}")
|
||
|
||
# 显示直播状态
|
||
status_map = {
|
||
0: "未开播",
|
||
1: "正在直播",
|
||
2: "轮播中"
|
||
}
|
||
status = status_map.get(room_info['live_status'], "未知状态")
|
||
print(f"直播状态: {status}")
|
||
|
||
print(f"在线人数: {room_info['online']}")
|
||
|
||
if room_info['live_time'] > 0:
|
||
live_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(room_info['live_time']))
|
||
print(f"开���时间: {live_time}")
|
||
|
||
print(f"分区: {room_info['area_v2_parent_name']} - {room_info['area_v2_name']}")
|
||
|
||
if room_info['tag_name']:
|
||
print(f"标签: {room_info['tag_name']}")
|
||
|
||
if room_info['tags']:
|
||
print(f"定义标签: {room_info['tags']}")
|
||
|
||
if room_info['cover_from_user']:
|
||
print(f"封面: {room_info['cover_from_user']}")
|
||
|
||
# 显示直播类型
|
||
broadcast_type = "手机直播" if room_info['broadcast_type'] == 1 else "普通直播"
|
||
print(f"直播类型: {broadcast_type}")
|
||
|
||
print("="*50)
|
||
|
||
else:
|
||
print(f"获取直播间状态失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取直播间状态时出错: {str(e)}")
|
||
|
||
def get_live_history_danmaku(room_id: str) -> None:
|
||
"""获取直播间最近历史弹幕"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.live.bilibili.com/xlive/web-room/v1/dM/gethistory'
|
||
params = {'roomid': room_id}
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
admin_msgs = data['data']['admin'] # 管理员的弹幕
|
||
room_msgs = data['data']['room'] # 普通用户的弹幕
|
||
|
||
print("\n直播间历史弹幕:")
|
||
print("="*50)
|
||
|
||
if admin_msgs:
|
||
print("\n管理员弹幕:")
|
||
print("-"*30)
|
||
for msg in admin_msgs:
|
||
print(f"时间: {msg['timeline']}")
|
||
print(f"用户: {msg['nickname']} (UID: {msg['uid']})")
|
||
if msg['medal']: # 如果有粉丝勋章
|
||
print(f"勋章: {msg['medal'][1]} {msg['medal'][0]}级")
|
||
print(f"内容: {msg['text']}")
|
||
print("-"*30)
|
||
|
||
if room_msgs:
|
||
print("\n普通用户弹幕:")
|
||
print("-"*30)
|
||
for msg in room_msgs:
|
||
print(f"时间: {msg['timeline']}")
|
||
print(f"���户: {msg['nickname']} (UID: {msg['uid']})")
|
||
if msg['medal']: # 如果有粉丝勋章
|
||
print(f"勋章: {msg['medal'][1]} {msg['medal'][0]}级")
|
||
print(f"内容: {msg['text']}")
|
||
print("-"*30)
|
||
|
||
# 询问是否保存到文件
|
||
choice = input("\n是否保存历史弹幕到文本文件?(y/n) [y]: ").lower()
|
||
if not choice or choice == 'y':
|
||
# 创建直播弹幕文件夹(如果不存在)
|
||
danmaku_dir = os.path.join("弹幕", "直播弹幕")
|
||
if not os.path.exists(danmaku_dir):
|
||
os.makedirs(danmaku_dir)
|
||
|
||
# 生成文件名(房间号 + 当前时间 + 历史弹幕后缀)
|
||
filename = f"{room_id} {time.strftime('%Y-%m-%d %H-%M-%S', time.localtime())} �����史弹幕.txt"
|
||
filepath = os.path.join(danmaku_dir, filename)
|
||
|
||
try:
|
||
with open(filepath, 'w', encoding='utf-8') as f:
|
||
if admin_msgs:
|
||
f.write("管理员弹幕:\n")
|
||
f.write("-"*30 + "\n")
|
||
for msg in admin_msgs:
|
||
f.write(f"时间: {msg['timeline']}\n")
|
||
f.write(f"用户: {msg['nickname']} (UID: {msg['uid']})\n")
|
||
if msg['medal']:
|
||
f.write(f"勋章: {msg['medal'][1]} {msg['medal'][0]}级\n")
|
||
f.write(f"内容: {msg['text']}\n")
|
||
f.write("-"*30 + "\n")
|
||
f.flush() # 立即写入文件
|
||
|
||
time.sleep(10)
|
||
|
||
print(f"\n监��完成,共收集到 {len(seen_msgs)} 条弹幕")
|
||
print(f"弹幕已保存到文件: {filepath}")
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n用户停止监听")
|
||
print(f"已收集到 {len(seen_msgs)} 条弹幕")
|
||
print(f"弹幕已保存到文件: {filepath}")
|
||
|
||
else:
|
||
print(f"获取历史弹幕失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取历史弹幕时出错: {str(e)}")
|
||
|
||
def get_room_play_info(room_id: str) -> None:
|
||
"""获取直播间播放信息"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
|
||
# 清晰度代码映射
|
||
quality_map = {
|
||
30000: "杜比",
|
||
20000: "4K",
|
||
10000: "原画",
|
||
400: "蓝光",
|
||
250: "超清",
|
||
150: "高清",
|
||
80: "流畅"
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo'
|
||
params = {
|
||
'room_id': room_id,
|
||
'protocol': '0,1', # http_stream,http_hls
|
||
'format': '0,1,2', # flv,ts,fmp4
|
||
'codec': '0,1', # AVC,HEVC
|
||
'qn': '10000', # 原画质量
|
||
'platform': 'web',
|
||
'ptype': '8',
|
||
'dolby': '5',
|
||
'panorama': '1'
|
||
}
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
room_info = data['data']
|
||
print("\n直播间播放信息:")
|
||
print("="*50)
|
||
print(f"房间号: {room_info['room_id']}")
|
||
if room_info['short_id'] != 0:
|
||
print(f"短号: {room_info['short_id']}")
|
||
print(f"主UID: {room_info['uid']}")
|
||
|
||
# 显示直播状态
|
||
status_map = {
|
||
0: "未开播",
|
||
1: "直播中",
|
||
2: "轮播中"
|
||
}
|
||
status = status_map.get(room_info['live_status'], "未知状态")
|
||
print(f"直播状态: {status}")
|
||
|
||
if room_info['live_status'] == 1:
|
||
live_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(room_info['live_time']))
|
||
print(f"开播时间: {live_time}")
|
||
|
||
# 显示房间状态
|
||
print(f"是否隐藏: {'是' if room_info['is_hidden'] else '否'}")
|
||
print(f"是否锁定: {'是' if room_info['is_locked'] else '否'}")
|
||
print(f"是否竖屏: {'是' if room_info['is_portrait'] else '否'}")
|
||
print(f"是否加密: {'是' if room_info['encrypted'] else '否'}")
|
||
if room_info['encrypted']:
|
||
print(f"密码验证: {'已通过' if room_info['pwd_verified'] else '未通过'}")
|
||
|
||
# 显示清晰度信息
|
||
if 'playurl_info' in room_info and 'playurl' in room_info['playurl_info']:
|
||
playurl = room_info['playurl_info']['playurl']
|
||
if 'g_qn_desc' in playurl:
|
||
print("\n可用清晰度:")
|
||
for qn in playurl['g_qn_desc']:
|
||
quality_name = quality_map.get(qn['qn'], qn['desc'])
|
||
desc = f"{quality_name}"
|
||
if qn['hdr_desc']:
|
||
desc += f" ({qn['hdr_desc']})"
|
||
print(f"- {desc} (qn: {qn['qn']})")
|
||
|
||
# 显示直播流信息
|
||
if 'stream' in playurl:
|
||
print("\n直播流信息:")
|
||
for stream in playurl['stream']:
|
||
print(f"\n协议: {stream['protocol_name']}")
|
||
for format_info in stream['format']:
|
||
print(f"格式: {format_info['format_name']}")
|
||
for codec in format_info['codec']:
|
||
print(f"编码: {codec['codec_name']}")
|
||
current_quality = quality_map.get(codec['current_qn'], f"未知({codec['current_qn']})")
|
||
print(f"当前清晰度: {current_quality}")
|
||
accept_qualities = [quality_map.get(qn, f"未知({qn})") for qn in codec['accept_qn']]
|
||
print(f"可用清晰度: {', '.join(accept_qualities)}")
|
||
if codec['url_info']:
|
||
print(f"播放地址: {codec['url_info'][0]['host']}{codec['base_url']}")
|
||
|
||
print("="*50)
|
||
|
||
else:
|
||
print(f"获取直播间播放信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取直播间播放信息时出错: {str(e)}")
|
||
|
||
def get_room_anchor_info(room_id: str) -> None:
|
||
"""获取直播间主播详细信息"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room'
|
||
params = {'roomid': room_id}
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
if 'data' in data and data['data']:
|
||
anchor_data = data['data']
|
||
info = anchor_data['info']
|
||
level = anchor_data['level']
|
||
|
||
print("\n主播详细信息:")
|
||
print("="*50)
|
||
|
||
# 基本信息
|
||
print("基本信息:")
|
||
print(f"UID: {info['uid']}")
|
||
print(f"用户名: {info['uname']}")
|
||
print(f"头像: {info['face']}")
|
||
print(f"排名: {info['rank']}")
|
||
print(f"平台用户等级: {info['platform_user_level']}")
|
||
|
||
# 认证状态
|
||
print(f"\n认证状态:")
|
||
print(f"手机认证: {'已认证' if info['mobile_verify'] == 1 else '未认证'}")
|
||
print(f"身份认证: {'已认证' if info['identification'] == 1 else '未认证'}")
|
||
|
||
# 官方认证信息
|
||
if info['official_verify']['type'] != -1:
|
||
verify_type = "认证" if info['official_verify']['type'] == 0 else "机构认证"
|
||
print(f"认证类型: {verify_type}")
|
||
print(f"认证说���: {info['official_verify']['desc']}")
|
||
|
||
# 性别
|
||
gender_map = {
|
||
-1: "保密",
|
||
0: "女",
|
||
1: "男"
|
||
}
|
||
print(f"性别: {gender_map.get(info['gender'], '未知')}")
|
||
|
||
# 等级信息
|
||
print("\n等级信息:")
|
||
master_level = level['master_level']
|
||
print(f"主播等级: {master_level['level']}")
|
||
print(f"用户等级: {level['user_level']}")
|
||
print(f"���播积分: {master_level['anchor_score']}")
|
||
if master_level['upgrade_score'] > 0:
|
||
print(f"升级还需: {master_level['upgrade_score']}")
|
||
|
||
# 消费信息
|
||
print("\n消费信息:")
|
||
print(f"消费金额: {level['cost']}")
|
||
print(f"充值金额: {level['rcost']}")
|
||
|
||
# VIP信息
|
||
print("\nVIP状态:")
|
||
if level['vip'] == 1:
|
||
print(f"VIP到期时间: {level['vip_time']}")
|
||
else:
|
||
print("不是VIP")
|
||
if level['svip'] == 1:
|
||
print(f"SVIP到期时间: {level['svip_time']}")
|
||
else:
|
||
print("不是SVIP")
|
||
|
||
# SAN值
|
||
print(f"\nSAN值: {anchor_data['san']}/12")
|
||
|
||
print("="*50)
|
||
else:
|
||
print("未找到主播信息")
|
||
else:
|
||
print(f"获取主播信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取主播信息时出错: {str(e)}")
|
||
|
||
class LiveRoom:
|
||
"""B站直播间相关功能类"""
|
||
|
||
def __init__(self):
|
||
self.headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
# 清晰度代码映射
|
||
self.quality_map = {
|
||
30000: "杜比",
|
||
20000: "4K",
|
||
10000: "原画",
|
||
400: "蓝光",
|
||
250: "超清",
|
||
150: "高清",
|
||
80: "流畅"
|
||
}
|
||
|
||
def _process_room_id(self, room_input: str) -> str:
|
||
"""处理直播间号输入,支持直接输入房�����号或链接"""
|
||
if 'live.bilibili.com' in room_input:
|
||
return extract_room_id(room_input)
|
||
return room_input
|
||
|
||
def get_room_info(self, room_input: str) -> None:
|
||
"""获取直播间信息"""
|
||
room_id = self._process_room_id(room_input)
|
||
if not room_id.isdigit():
|
||
print("请输入有效的直播间号或链接!")
|
||
return
|
||
try:
|
||
url = 'https://api.live.bilibili.com/room/v1/Room/get_info'
|
||
params = {'room_id': room_id}
|
||
response = requests.get(url, params=params, headers=self.headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
room_info = data['data']
|
||
print("\n直播间信息:")
|
||
print("="*50)
|
||
print(f"主播UID: {room_info['uid']}")
|
||
print(f"直播间号: {room_info['room_id']}")
|
||
if room_info['short_id'] != 0:
|
||
print(f"直播间短号: {room_info['short_id']}")
|
||
print(f"关注数: {room_info['attention']}")
|
||
print(f"观看人数: {room_info['online']}")
|
||
print(f"标题: {room_info['title']}")
|
||
print(f"分区: {room_info['parent_area_name']} - {room_info['area_name']}")
|
||
|
||
# 显示直播状态
|
||
status_map = {
|
||
0: "未开播",
|
||
1: "直播中",
|
||
2: "轮播中"
|
||
}
|
||
status = status_map.get(room_info['live_status'], "未知状态")
|
||
print(f"直播状态: {status}")
|
||
|
||
if room_info['live_status'] == 1:
|
||
print(f"开播时间: {room_info['live_time']}")
|
||
|
||
if room_info['description']:
|
||
print(f"\n房间简介: {room_info['description']}")
|
||
|
||
# 显示认证信息
|
||
if room_info['new_pendants']['badge']:
|
||
badge = room_info['new_pendants']['badge']
|
||
badge_type = {
|
||
'v_person': '个人认证',
|
||
'v_company': '企业认证'
|
||
}
|
||
print(f"\n认证类型: {badge_type.get(badge['name'], '未知认证')}")
|
||
if badge['desc']:
|
||
print(f"认证说明: {badge['desc']}")
|
||
|
||
# 显示热词
|
||
if room_info['hot_words']:
|
||
print("\n房间热词:")
|
||
for word in room_info['hot_words']:
|
||
print(f"- {word}")
|
||
|
||
print("="*50)
|
||
|
||
else:
|
||
print(f"获取直播间信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取直播间信息时出错: {str(e)}")
|
||
|
||
def get_user_live_status(self, mid: str) -> None:
|
||
"""获取用户直播间状态"""
|
||
# 原get_user_live_status函数的内容
|
||
|
||
def get_anchor_info(self, uid: str) -> None:
|
||
"""获取主播信息"""
|
||
try:
|
||
url = 'https://api.live.bilibili.com/live_user/v1/Master/info'
|
||
params = {'uid': uid}
|
||
response = requests.get(url, params=params, headers=self.headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
anchor_data = data['data']
|
||
info = anchor_data['info']
|
||
exp = anchor_data['exp']
|
||
|
||
print("\n主播信息:")
|
||
print("="*50)
|
||
print(f"UID: {info['uid']}")
|
||
print(f"用户名: {info['uname']}")
|
||
print(f"头像: {info['face']}")
|
||
|
||
# 显示性别
|
||
gender_map = {
|
||
-1: "保密",
|
||
0: "女",
|
||
1: "男"
|
||
}
|
||
gender = gender_map.get(info['gender'], "未知")
|
||
print(f"性别: {gender}")
|
||
|
||
# 显示认证信息
|
||
if info['official_verify']['type'] != -1:
|
||
verify_type = "个人认证" if info['official_verify']['type'] == 0 else "机构认证"
|
||
print(f"认证类型: {verify_type}")
|
||
print(f"认证说明: {info['official_verify']['desc']}")
|
||
|
||
# 显示等级信息
|
||
level_info = exp['master_level']
|
||
print(f"\n主播等级: {level_info['level']}")
|
||
print(f"当前经验: {level_info['current'][1]}/{level_info['next'][1]}")
|
||
print(f"升级还需: {level_info['next'][0] - level_info['current'][0]}")
|
||
|
||
# ���示其他信息
|
||
print(f"\n粉丝数: {anchor_data['follower_num']}")
|
||
print(f"直播间ID: {anchor_data['room_id']}")
|
||
if anchor_data['medal_name']:
|
||
print(f"粉丝勋章: {anchor_data['medal_name']}")
|
||
if anchor_data['glory_count'] > 0:
|
||
print(f"主播荣誉数: {anchor_data['glory_count']}")
|
||
if anchor_data['pendant']:
|
||
print(f"头像框: {anchor_data['pendant']}")
|
||
|
||
# 显示公告
|
||
if anchor_data['room_news']['content']:
|
||
print("\n主播公告:")
|
||
print(f"内容: {anchor_data['room_news']['content']}")
|
||
print(f"时间: {anchor_data['room_news']['ctime_text']}")
|
||
|
||
print("="*50)
|
||
|
||
else:
|
||
print(f"获取主播信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取��播��息时出错: {str(e)}")
|
||
|
||
def get_room_base_info(self, room_ids: list) -> None:
|
||
"""获取多个直播间基本信息"""
|
||
# 原get_room_base_info函数的内容
|
||
|
||
def get_batch_live_status(self, uids: list) -> None:
|
||
"""批量查询直播间状态"""
|
||
# 原get_batch_live_status函数的���容
|
||
|
||
def get_history_danmaku(self, room_input: str) -> None:
|
||
"""获取直播间历史弹幕"""
|
||
room_id = self._process_room_id(room_input)
|
||
if not room_id.isdigit():
|
||
print("请输入有效的直播间号或链接!")
|
||
return
|
||
|
||
try:
|
||
print("\n请选择获取方式:")
|
||
print("1. 获取最近弹幕")
|
||
print("2. 持续监听弹幕")
|
||
choice = input("请选择 [1]: ").strip()
|
||
|
||
if not choice:
|
||
choice = "1"
|
||
|
||
if choice == "1":
|
||
self._get_recent_danmaku(room_id)
|
||
elif choice == "2":
|
||
self._monitor_danmaku(room_id)
|
||
|
||
except Exception as e:
|
||
print(f"获取历史弹幕时出错: {str(e)}")
|
||
|
||
def _get_recent_danmaku(self, room_id: str) -> None:
|
||
"""获取最近的弹幕"""
|
||
try:
|
||
url = 'https://api.live.bilibili.com/xlive/web-room/v1/dM/gethistory'
|
||
params = {'roomid': room_id}
|
||
response = requests.get(url, params=params, headers=self.headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
self._save_danmaku_to_file(room_id, data['data'])
|
||
else:
|
||
print(f"获取历史弹幕失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取最近弹幕时出错: {str(e)}")
|
||
|
||
def _monitor_danmaku(self, room_id: str) -> None:
|
||
"""持续监听直播间弹幕"""
|
||
try:
|
||
duration = input("请输入监听时长(分钟)[30]: ").strip()
|
||
duration = int(duration) if duration.isdigit() else 30
|
||
interval = input("请输入获取间隔(秒)[10]: ").strip()
|
||
interval = int(interval) if interval.isdigit() else 10
|
||
|
||
print(f"\n开始监听直播间 {room_id} 的弹幕")
|
||
print(f"将持续监听 {duration} 分钟,每 {interval} 秒获取一次")
|
||
print("按Ctrl+C可以随时停止")
|
||
|
||
# 创建文件
|
||
danmaku_dir = os.path.join("弹幕", "直播弹幕")
|
||
if not os.path.exists(danmaku_dir):
|
||
os.makedirs(danmaku_dir)
|
||
|
||
filename = f"{room_id} {time.strftime('%Y-%m-%d %H-%M-%S', time.localtime())} 监听弹幕.txt"
|
||
filepath = os.path.join(danmaku_dir, filename)
|
||
|
||
seen_msgs = set() # 用于去重
|
||
start_time = time.time()
|
||
|
||
with open(filepath, 'w', encoding='utf-8') as f:
|
||
try:
|
||
while time.time() - start_time < duration * 60:
|
||
new_msgs = self._fetch_danmaku(room_id)
|
||
for msg in new_msgs:
|
||
msg_id = f"{msg['timeline']}_{msg['nickname']}_{msg['text']}"
|
||
if msg_id not in seen_msgs:
|
||
seen_msgs.add(msg_id)
|
||
# 写入文件
|
||
f.write(f"时间: {msg['timeline']}\n")
|
||
f.write(f"用户: {msg['nickname']} (UID: {msg['uid']})\n")
|
||
if msg['medal']:
|
||
f.write(f"勋章: {msg['medal'][1]} {msg['medal'][0]}级\n")
|
||
f.write(f"内容: {msg['text']}\n")
|
||
f.write("-"*30 + "\n")
|
||
f.flush() # 立即写入文件
|
||
|
||
time.sleep(interval)
|
||
|
||
print(f"\n监完成,共收集到 {len(seen_msgs)} 条弹幕")
|
||
print(f"弹幕已保存到文件: {filepath}")
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n用户停止监听")
|
||
print(f"已收集到 {len(seen_msgs)} 条弹幕")
|
||
print(f"弹幕已保存到文件: {filepath}")
|
||
|
||
except Exception as e:
|
||
print(f"监听弹幕时出错: {str(e)}")
|
||
|
||
def _save_danmaku_to_file(self, room_id: str, data: dict) -> None:
|
||
"""保存弹幕到文件"""
|
||
admin_msgs = data['admin']
|
||
room_msgs = data['room']
|
||
|
||
danmaku_dir = os.path.join("弹幕", "直播弹幕")
|
||
if not os.path.exists(danmaku_dir):
|
||
os.makedirs(danmaku_dir)
|
||
|
||
filename = f"{room_id} {time.strftime('%Y-%m-%d %H-%M-%S', time.localtime())} 历史弹幕.txt"
|
||
filepath = os.path.join(danmaku_dir, filename)
|
||
|
||
with open(filepath, 'w', encoding='utf-8') as f:
|
||
if admin_msgs:
|
||
f.write("管理员弹幕:\n")
|
||
f.write("-"*30 + "\n")
|
||
for msg in admin_msgs:
|
||
f.write(f"时间: {msg['timeline']}\n")
|
||
f.write(f"用户: {msg['nickname']} (UID: {msg['uid']})\n")
|
||
if msg['medal']:
|
||
f.write(f"勋章: {msg['medal'][1]} {msg['medal'][0]}级\n")
|
||
f.write(f"内容: {msg['text']}\n")
|
||
f.write("-"*30 + "\n")
|
||
|
||
if room_msgs:
|
||
f.write("\n普通用户弹幕:\n")
|
||
f.write("-"*30 + "\n")
|
||
for msg in room_msgs:
|
||
f.write(f"时间: {msg['timeline']}\n")
|
||
f.write(f"用户: {msg['nickname']} (UID: {msg['uid']})\n")
|
||
if msg['medal']:
|
||
f.write(f"勋章: {msg['medal'][1]} {msg['medal'][0]}级\n")
|
||
f.write(f"内容: {msg['text']}\n")
|
||
f.write("-"*30 + "\n")
|
||
|
||
total_msgs = len(admin_msgs) + len(room_msgs)
|
||
print(f"\n共获取到 {total_msgs} 条弹幕")
|
||
print(f"弹幕已保存到文件: {filepath}")
|
||
|
||
def _fetch_danmaku(self, room_id: str) -> list:
|
||
"""获取一次弹幕数据"""
|
||
url = 'https://api.live.bilibili.com/xlive/web-room/v1/dM/gethistory'
|
||
params = {'roomid': room_id}
|
||
response = requests.get(url, params=params, headers=self.headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
msgs = []
|
||
if 'admin' in data['data']:
|
||
msgs.extend(data['data']['admin'])
|
||
if 'room' in data['data']:
|
||
msgs.extend(data['data']['room'])
|
||
return msgs
|
||
return []
|
||
|
||
def get_play_info(self, room_input: str) -> None:
|
||
"""获取直播间播放信息"""
|
||
room_id = self._process_room_id(room_input)
|
||
if not room_id.isdigit():
|
||
print("请输入有效的直播间号或链接!")
|
||
return
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
|
||
# 清晰度代码映射
|
||
quality_map = {
|
||
30000: "杜比",
|
||
20000: "4K",
|
||
10000: "���画",
|
||
400: "蓝光",
|
||
250: "超清",
|
||
150: "高清",
|
||
80: "流畅"
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo'
|
||
params = {
|
||
'room_id': room_id,
|
||
'protocol': '0,1', # http_stream,http_hls
|
||
'format': '0,1,2', # flv,ts,fmp4
|
||
'codec': '0,1', # AVC,HEVC
|
||
'qn': '10000', # 原画质量
|
||
'platform': 'web',
|
||
'ptype': '8',
|
||
'dolby': '5',
|
||
'panorama': '1'
|
||
}
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
room_info = data['data']
|
||
print("\n直播间播放信息:")
|
||
print("="*50)
|
||
print(f"房间号: {room_info['room_id']}")
|
||
if room_info['short_id'] != 0:
|
||
print(f"短号: {room_info['short_id']}")
|
||
print(f"主UID: {room_info['uid']}")
|
||
|
||
# 显示直播状态
|
||
status_map = {
|
||
0: "未开播",
|
||
1: "直播中",
|
||
2: "轮播中"
|
||
}
|
||
status = status_map.get(room_info['live_status'], "未知状态")
|
||
print(f"直播状态: {status}")
|
||
|
||
if room_info['live_status'] == 1:
|
||
live_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(room_info['live_time']))
|
||
print(f"开播时间: {live_time}")
|
||
|
||
# 显示房间状态
|
||
print(f"是否隐藏: {'是' if room_info['is_hidden'] else '否'}")
|
||
print(f"是否锁定: {'是' if room_info['is_locked'] else '否'}")
|
||
print(f"是否竖屏: {'是' if room_info['is_portrait'] else '否'}")
|
||
print(f"是否加密: {'是' if room_info['encrypted'] else '否'}")
|
||
if room_info['encrypted']:
|
||
print(f"密码验证: {'已通过' if room_info['pwd_verified'] else '未通过'}")
|
||
|
||
# 显示清晰度信息
|
||
if 'playurl_info' in room_info and 'playurl' in room_info['playurl_info']:
|
||
playurl = room_info['playurl_info']['playurl']
|
||
if 'g_qn_desc' in playurl:
|
||
print("\n可用清晰度:")
|
||
for qn in playurl['g_qn_desc']:
|
||
quality_name = quality_map.get(qn['qn'], qn['desc'])
|
||
desc = f"{quality_name}"
|
||
if qn['hdr_desc']:
|
||
desc += f" ({qn['hdr_desc']})"
|
||
print(f"- {desc} (qn: {qn['qn']})")
|
||
|
||
# 显示直播流信息
|
||
if 'stream' in playurl:
|
||
print("\n直播流信息:")
|
||
for stream in playurl['stream']:
|
||
print(f"\n协议: {stream['protocol_name']}")
|
||
for format_info in stream['format']:
|
||
print(f"格式: {format_info['format_name']}")
|
||
for codec in format_info['codec']:
|
||
print(f"编码: {codec['codec_name']}")
|
||
current_quality = quality_map.get(codec['current_qn'], f"未知({codec['current_qn']})")
|
||
print(f"当前清晰度: {current_quality}")
|
||
accept_qualities = [quality_map.get(qn, f"未知({qn})") for qn in codec['accept_qn']]
|
||
print(f"可用清晰度: {', '.join(accept_qualities)}")
|
||
if codec['url_info']:
|
||
print(f"播放地址: {codec['url_info'][0]['host']}{codec['base_url']}")
|
||
|
||
print("="*50)
|
||
|
||
else:
|
||
print(f"获取直播间播放信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取直播间播放信息时出错: {str(e)}")
|
||
|
||
def get_room_anchor_info(self, room_input: str) -> None:
|
||
"""获取直播间主播详细信息"""
|
||
room_id = self._process_room_id(room_input)
|
||
if not room_id.isdigit():
|
||
print("请输入有效的直播间号或链接!")
|
||
return
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://live.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room'
|
||
params = {'roomid': room_id}
|
||
response = requests.get(url, params=params, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
if 'data' in data and data['data']:
|
||
anchor_data = data['data']
|
||
info = anchor_data['info']
|
||
level = anchor_data['level']
|
||
|
||
print("\n主播详细信息:")
|
||
print("="*50)
|
||
|
||
# 基本信息
|
||
print("基本信息:")
|
||
print(f"UID: {info['uid']}")
|
||
print(f"���户名: {info['uname']}")
|
||
print(f"头像: {info['face']}")
|
||
print(f"排名: {info['rank']}")
|
||
print(f"平台用户等级: {info['platform_user_level']}")
|
||
|
||
# 认证状态
|
||
print(f"\n认证状态:")
|
||
print(f"手机认证: {'已认证' if info['mobile_verify'] == 1 else '未认证'}")
|
||
print(f"身份认证: {'已认证' if info['identification'] == 1 else '未认证'}")
|
||
|
||
# 官方认证信息
|
||
if info['official_verify']['type'] != -1:
|
||
verify_type = "认证" if info['official_verify']['type'] == 0 else "机构认证"
|
||
print(f"认证类型: {verify_type}")
|
||
print(f"认证说明: {info['official_verify']['desc']}")
|
||
|
||
# 性别
|
||
gender_map = {
|
||
-1: "保密",
|
||
0: "女",
|
||
1: "男"
|
||
}
|
||
print(f"性别: {gender_map.get(info['gender'], '未知')}")
|
||
|
||
# 等级信息
|
||
print("\n等级信息:")
|
||
master_level = level['master_level']
|
||
print(f"主播等级: {master_level['level']}")
|
||
print(f"用户等级: {level['user_level']}")
|
||
print(f"主播积分: {master_level['anchor_score']}")
|
||
if master_level['upgrade_score'] > 0:
|
||
print(f"升级还需: {master_level['upgrade_score']}")
|
||
|
||
# 消费信息
|
||
print("\n消费信息:")
|
||
print(f"消费金额: {level['cost']}")
|
||
print(f"充值金额: {level['rcost']}")
|
||
|
||
# VIP信息
|
||
print("\nVIP状态:")
|
||
if level['vip'] == 1:
|
||
print(f"VIP到期时间: {level['vip_time']}")
|
||
else:
|
||
print("不是VIP")
|
||
if level['svip'] == 1:
|
||
print(f"SVIP到期时间: {level['svip_time']}")
|
||
else:
|
||
print("不是SVIP")
|
||
|
||
# SAN值
|
||
print(f"\nSAN值: {anchor_data['san']}/12")
|
||
|
||
print("="*50)
|
||
else:
|
||
print("未找到主播信息")
|
||
else:
|
||
print(f"获取主播信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"获取主播信息时出错: {str(e)}")
|
||
|
||
def extract_room_id(url: str) -> str:
|
||
"""从直播间URL中提取房间号"""
|
||
# 匹配live.bilibili.com/后面的数字
|
||
pattern = r'live\.bilibili\.com/(\d+)'
|
||
match = re.search(pattern, url)
|
||
if match:
|
||
return match.group(1)
|
||
return ''
|
||
|
||
def extract_uid(url: str) -> str:
|
||
"""从B站个人空间URL中提取UID"""
|
||
# 匹配space.bilibili.com/后面的数字
|
||
pattern = r'space\.bilibili\.com/(\d+)'
|
||
match = re.search(pattern, url)
|
||
if match:
|
||
return match.group(1)
|
||
return ''
|
||
|
||
def send_old_fan_message(up_mid: str, content: str, cookies: Dict) -> bool:
|
||
"""发��老粉计划留言
|
||
|
||
Args:
|
||
up_mid (str): UP主的UID
|
||
content (str): 留言内容
|
||
cookies (Dict): cookies信息
|
||
|
||
Returns:
|
||
bool: 是否发送成功
|
||
"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://space.bilibili.com',
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
}
|
||
|
||
try:
|
||
# 获取csrf令牌
|
||
csrf = cookies.get('bili_jct')
|
||
if not csrf:
|
||
print("错误:无法获取csrf令牌!")
|
||
return False
|
||
|
||
# 构造请求��据
|
||
data = {
|
||
'aid': '',
|
||
'up_mid': up_mid,
|
||
'source': '4',
|
||
'scene': '105',
|
||
'content': content,
|
||
'csrf': csrf
|
||
}
|
||
|
||
# 发送请求
|
||
url = 'https://api.bilibili.com/x/v1/contract/add_message'
|
||
response = requests.post(url, data=data, cookies=cookies, headers=headers)
|
||
result = response.json()
|
||
|
||
if result['code'] == 0:
|
||
print(f"\n{result['data']['success_toast']}")
|
||
return True
|
||
else:
|
||
error_messages = {
|
||
-101: "账号未登录",
|
||
-111: "csrf校验失��",
|
||
-400: "请求错误",
|
||
158005: "您跟up主还不是契约关系,请先加入老粉计划"
|
||
}
|
||
error_msg = error_messages.get(result['code'], result['message'])
|
||
print(f"\n发送留言失败:{error_msg}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"\n发送留言时出错: {str(e)}")
|
||
return False
|
||
|
||
def join_old_fan_plan(up_mid: str, cookies: Dict) -> bool:
|
||
"""加入UP主的老粉计划"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://space.bilibili.com',
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
}
|
||
|
||
try:
|
||
csrf = cookies.get('bili_jct')
|
||
if not csrf:
|
||
print("错误:无法获取csrf令牌!")
|
||
return False
|
||
|
||
data = {
|
||
'aid': '',
|
||
'up_mid': up_mid,
|
||
'source': '4',
|
||
'scene': '105',
|
||
'platform': 'web',
|
||
'mobi_app': 'pc',
|
||
'csrf': csrf
|
||
}
|
||
|
||
url = 'https://api.bilibili.com/x/v1/contract/add_contract'
|
||
response = requests.post(url, data=data, cookies=cookies, headers=headers)
|
||
result = response.json()
|
||
|
||
if result['code'] == 0:
|
||
print("\n成功加入老粉计划!")
|
||
if result['data']['allow_message']:
|
||
print(f"\n{result['data']['input_title']}")
|
||
choice = input("\n是否发送留言?(y/n) [n]: ").lower()
|
||
if choice == 'y':
|
||
message = input(f"请输入要���送的留言 [默认: {result['data']['input_text']}]: ").strip()
|
||
if not message:
|
||
message = result['data']['input_text']
|
||
send_old_fan_message(up_mid, message, cookies)
|
||
return True
|
||
else:
|
||
error_messages = {
|
||
-101: "账号未登录",
|
||
-111: "csrf校验失败",
|
||
-400: "请���错误",
|
||
158001: "不满足��入条件",
|
||
158002: "已经是老粉了",
|
||
158003: "UP主未开通老粉计划",
|
||
158004: "UP主暂未达到开通条件"
|
||
}
|
||
error_msg = error_messages.get(result['code'], result['message'])
|
||
print(f"\n加入老粉计划失败:{error_msg}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"\n加入老粉计划时出错: {str(e)}")
|
||
return False
|
||
|
||
def check_follow_status(up_mid: str, cookies: Dict) -> bool:
|
||
"""检查是否关注了UP主
|
||
|
||
Args:
|
||
up_mid (str): UP主的UID
|
||
cookies (Dict): cookies信息
|
||
|
||
Returns:
|
||
bool: 是否已关注
|
||
"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://space.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
# 获取关注状态
|
||
url = f'https://api.bilibili.com/x/relation/stat?vmid={up_mid}'
|
||
response = requests.get(url, cookies=cookies, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
# 获取UP主信息
|
||
info_url = f'https://api.bilibili.com/x/space/acc/info?mid={up_mid}'
|
||
info_response = requests.get(info_url, headers=headers)
|
||
info_data = info_response.json()
|
||
|
||
if info_data['code'] == 0:
|
||
up_info = info_data['data']
|
||
print(f"\nUP主信息:")
|
||
print(f"名称: {up_info['name']}")
|
||
print(f"等级: LV{up_info['level']}")
|
||
if up_info['official']['title']:
|
||
print(f"认证信息: {up_info['official']['title']}")
|
||
|
||
# 检查关注状态
|
||
relation_url = f'https://api.bilibili.com/x/relation?fid={up_mid}'
|
||
relation_response = requests.get(relation_url, cookies=cookies, headers=headers)
|
||
relation_data = relation_response.json()
|
||
|
||
if relation_data['code'] == 0:
|
||
is_followed = relation_data['data']['attribute'] == 1
|
||
print(f"关���状态: {'已关注' if is_followed else '未关注'}")
|
||
|
||
if not is_followed:
|
||
print("\n提示: 需要先关注UP主才能加入老粉计划")
|
||
choice = input("是否现在关注?(y/n) [y]: ").lower()
|
||
if not choice or choice == 'y':
|
||
return follow_up(up_mid, cookies)
|
||
return is_followed
|
||
|
||
print(f"获取关注状态失败:{data['message']}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"检查关注状态时出错: {str(e)}")
|
||
return False
|
||
|
||
def follow_up(up_mid: str, cookies: Dict) -> bool:
|
||
"""关注UP主
|
||
|
||
Args:
|
||
up_mid (str): UP主的UID
|
||
cookies (Dict): cookies信息
|
||
|
||
Returns:
|
||
bool: 是否关注成功
|
||
"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://space.bilibili.com',
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
}
|
||
|
||
try:
|
||
csrf = cookies.get('bili_jct')
|
||
if not csrf:
|
||
print("错误:无法获取csrf令牌!")
|
||
return False
|
||
|
||
data = {
|
||
'fid': up_mid,
|
||
'act': 1, # 1:关注 2:取消关注
|
||
're_src': 11,
|
||
'csrf': csrf
|
||
}
|
||
|
||
url = 'https://api.bilibili.com/x/relation/modify'
|
||
response = requests.post(url, data=data, cookies=cookies, headers=headers)
|
||
result = response.json()
|
||
|
||
if result['code'] == 0:
|
||
print("\n成功关注UP主!")
|
||
return True
|
||
else:
|
||
print(f"\n关注UP主失败:{result['message']}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"关注UP主时出错: {str(e)}")
|
||
return False
|
||
|
||
def report_video(aid: str, tid: str, desc: str, attach: str, cookies: Dict, video_info: dict = None) -> bool:
|
||
"""无图片举报视频"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
'buid': str(random.randint(100000, 999999)) # 随机生成风控代码
|
||
}
|
||
|
||
try:
|
||
csrf = cookies.get('bili_jct')
|
||
if not csrf:
|
||
print("错误:无法获取csrf令牌!")
|
||
return False
|
||
|
||
data = {
|
||
'csrf': csrf,
|
||
'aid': aid,
|
||
'tid': tid,
|
||
'desc': desc,
|
||
}
|
||
|
||
if attach:
|
||
data['attach'] = attach
|
||
|
||
cookies['Buid'] = headers['buid']
|
||
|
||
url = 'https://api.bilibili.com/x/web-interface/appeal/v2/submit'
|
||
response = requests.post(url, data=data, cookies=cookies, headers=headers)
|
||
result = response.json()
|
||
|
||
if result['code'] == 0:
|
||
print("\n举报成功!")
|
||
# 保存举报记录
|
||
if video_info:
|
||
report_info = {
|
||
'reason': report_types.get(tid, {}).get('name', '未知理由'),
|
||
'desc': desc,
|
||
'attach': attach,
|
||
'is_image': False
|
||
}
|
||
save_report_record(video_info, report_info)
|
||
return True
|
||
else:
|
||
error_messages = {
|
||
-101: "账未登录",
|
||
-111: "csrf校验失败",
|
||
-400: "请求错误",
|
||
-404: "视频不存在",
|
||
78001: "举报理由不合法",
|
||
78002: "举报描述过短",
|
||
78003: "举报描述过长",
|
||
78004: "附件格式不支持",
|
||
78005: "附件数量超限"
|
||
}
|
||
error_msg = error_messages.get(result['code'], result['message'])
|
||
print(f"\n举报失败:{error_msg}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"\n举报时出错: {str(e)}")
|
||
return False
|
||
|
||
def report_video_with_image(aid: str, tid: str, desc: str, image_url: str, cookies: Dict, video_info: dict = None) -> bool:
|
||
"""带图片举报视频"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
'buid': str(random.randint(100000, 999999)) # 随机生成风控代码
|
||
}
|
||
|
||
try:
|
||
csrf = cookies.get('bili_jct')
|
||
if not csrf:
|
||
print("错误:无法获取csrf令牌!")
|
||
return False
|
||
|
||
data = {
|
||
'csrf': csrf,
|
||
'aid': aid,
|
||
'tid': tid,
|
||
'desc': desc,
|
||
'attach': image_url
|
||
}
|
||
|
||
cookies['Buid'] = headers['buid']
|
||
|
||
url = 'https://api.bilibili.com/x/web-interface/appeal/v2/submit'
|
||
response = requests.post(url, data=data, cookies=cookies, headers=headers)
|
||
result = response.json()
|
||
|
||
if result['code'] == 0:
|
||
print("\n举报成功!")
|
||
# 保存举报记录
|
||
if video_info:
|
||
report_info = {
|
||
'reason': report_types.get(tid, {}).get('name', '未知理���'),
|
||
'desc': desc,
|
||
'attach': image_url,
|
||
'is_image': True
|
||
}
|
||
save_report_record(video_info, report_info)
|
||
return True
|
||
else:
|
||
error_messages = {
|
||
-101: "账号未登录",
|
||
-111: "csrf校验失败",
|
||
-400: "请求错误",
|
||
-404: "视频不存在",
|
||
78001: "举报理由不合��",
|
||
78002: "举报描述过短",
|
||
78003: "举报描述过长",
|
||
78004: "附件格式不支持",
|
||
78005: "附件数量超限"
|
||
}
|
||
error_msg = error_messages.get(result['code'], result['message'])
|
||
print(f"\n举报失败:{error_msg}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"\n举报时出错: {str(e)}")
|
||
return False
|
||
|
||
def get_report_types() -> dict:
|
||
"""获取视频举报类型列表
|
||
|
||
Returns:
|
||
dict: 举报类型映射表 {tid: {name: str, remark: str, controls: list}}
|
||
"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
url = 'https://api.bilibili.com/x/web-interface/archive/appeal/tags'
|
||
response = requests.get(url, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
report_types = {}
|
||
for item in data['data']:
|
||
report_types[str(item['tid'])] = {
|
||
'name': item['name'],
|
||
'remark': item['remark'],
|
||
'controls': item['controls']
|
||
}
|
||
return report_types
|
||
else:
|
||
print(f"获取举报类型失败:{data['message']}")
|
||
return {}
|
||
|
||
except Exception as e:
|
||
print(f"获取举报类型时出��: {str(e)}")
|
||
return {}
|
||
|
||
def show_report_menu(bvid: str, cookies: Dict) -> None:
|
||
"""显示举报菜单"""
|
||
try:
|
||
# 获取视频aid
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
video_url = f'https://api.bilibili.com/x/web-interface/view?bvid={bvid}'
|
||
response = requests.get(video_url, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
aid = str(data['data']['aid'])
|
||
|
||
# 获取举报类型
|
||
report_types = get_report_types()
|
||
if not report_types:
|
||
print("\n获取举报类型失败,使用默认类型")
|
||
# 使用默认的举报类型
|
||
report_types = {
|
||
"2": {"name": "违法违禁", "remark": "为帮助审核人员更快处理,补充违规内容出现位置", "controls": None},
|
||
"3": {"name": "色情低俗", "remark": "为帮助审核人员更快处理,补充违规内容出现位置", "controls": None},
|
||
"5": {"name": "赌博诈骗", "remark": "为帮助审核人员更快处理,补充违规内容出现位置", "controls": None},
|
||
"6": {"name": "血腥暴力", "remark": "为帮助审核人员更快处理,补充违规内容出现位置", "controls": None},
|
||
"7": {"name": "人身攻击", "remark": "为帮助审核人员更快处理,补充违规内容出现位置", "controls": None},
|
||
}
|
||
|
||
print("\n请选择举报理由:")
|
||
for tid, info in report_types.items():
|
||
print(f"{tid}. {info['name']}")
|
||
|
||
tid = input("\n请选择举报理由编号: ").strip()
|
||
|
||
if tid in report_types:
|
||
print(f"\n提示: {report_types[tid]['remark']}")
|
||
|
||
desc = input("\n请输入详细描述(至少10个字):").strip()
|
||
if len(desc) < 10:
|
||
print("\n描述太短,至少需要10个字!")
|
||
return
|
||
|
||
attach = ""
|
||
# 如果需要额外信息(如撞车举报)
|
||
if report_types[tid]['controls']:
|
||
for control in report_types[tid]['controls']:
|
||
if control['required']:
|
||
value = input(f"\n请输入{control['title']} ({control['placeholder']}): ").strip()
|
||
if not value:
|
||
print(f"\n{control['title']}为必填项!")
|
||
return
|
||
attach = value
|
||
else:
|
||
# 普通举报,询问是否需要上传图片
|
||
print("\n是否上传举报附件图片?")
|
||
print("1. 上传本地图片")
|
||
print("2. 直接输入图片URL")
|
||
print("3. 跳过")
|
||
|
||
upload_choice = input("\n请选择 [3]: ").strip()
|
||
if not upload_choice:
|
||
upload_choice = "3"
|
||
|
||
if upload_choice == "1":
|
||
image_path = input("\n请输入图片文件路径: ").strip()
|
||
if os.path.exists(image_path):
|
||
image_url = upload_report_image(image_path, cookies)
|
||
if image_url:
|
||
attach = image_url
|
||
else:
|
||
print("\n图片上传失败!")
|
||
else:
|
||
print("\n找不到图片文件!")
|
||
elif upload_choice == "2":
|
||
attach = input("\n请输入图片URL(多个用逗号分隔):").strip()
|
||
|
||
# 确认举报
|
||
print("\n举报信息确认:")
|
||
print(f"视频:{bvid}")
|
||
print(f"理由:{report_types[tid]['name']}")
|
||
print(f"描述:{desc}")
|
||
if attach:
|
||
if report_types[tid]['controls']:
|
||
print(f"{report_types[tid]['controls'][0]['title']}: {attach}")
|
||
else:
|
||
print(f"附件:{attach}")
|
||
|
||
confirm = input("\n确认提交举报?(y/n) [n]: ").lower()
|
||
if confirm == 'y':
|
||
if report_types[tid]['controls']:
|
||
# 特殊举报(如撞车)不需要上传图片
|
||
report_video(aid, tid, desc, attach, cookies)
|
||
else:
|
||
# 普通举报
|
||
report_video_with_image(aid, tid, desc, attach, cookies)
|
||
else:
|
||
print("\n无效的举报理由编号!")
|
||
else:
|
||
print(f"\n获取视频信息失败:{data['message']}")
|
||
|
||
except Exception as e:
|
||
print(f"\n显示举报菜单时出错: {str(e)}")
|
||
|
||
def upload_report_image(image_path: str, cookies: Dict) -> str:
|
||
"""上传举报附件图片"""
|
||
try:
|
||
# 复制图片到举报图片文件夹
|
||
image_dir = os.path.join("举报", "图片")
|
||
image_name = f"{time.strftime('%Y-%m-%d %H-%M-%S')}_{os.path.basename(image_path)}"
|
||
new_image_path = os.path.join(image_dir, image_name)
|
||
|
||
import shutil
|
||
shutil.copy2(image_path, new_image_path)
|
||
print(f"\n图片已保存到: {new_image_path}")
|
||
|
||
# 上传图片到B站
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
csrf = cookies.get('bili_jct')
|
||
if not csrf:
|
||
print("错误:无法获取csrf令牌!")
|
||
return ""
|
||
|
||
with open(new_image_path, 'rb') as f:
|
||
image_data = f.read()
|
||
base64_data = f"data:image/jpeg;base64,{base64.b64encode(image_data).decode()}"
|
||
|
||
data = {
|
||
'csrf': csrf,
|
||
'cover': base64_data
|
||
}
|
||
|
||
url = 'https://member.bilibili.com/x/vu/web/cover/up'
|
||
params = {'ts': int(time.time() * 1000)}
|
||
response = requests.post(url, params=params, data=data, cookies=cookies, headers=headers)
|
||
result = response.json()
|
||
|
||
if result['code'] == 0:
|
||
return result['data']['url']
|
||
else:
|
||
error_messages = {
|
||
-101: "账号未登录",
|
||
-111: "csrf校验失败",
|
||
-400: "请求错误"
|
||
}
|
||
error_msg = error_messages.get(result['code'], result['message'])
|
||
print(f"上传图片失败:{error_msg}")
|
||
return ""
|
||
|
||
except Exception as e:
|
||
print(f"上传图片时出错: {str(e)}")
|
||
return ""
|
||
|
||
def search_videos(keyword: str, cookies: Dict, page: int = 1, order: str = 'totalrank') -> list:
|
||
"""搜索视频"""
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://search.bilibili.com',
|
||
}
|
||
|
||
try:
|
||
# 构造基础参数
|
||
params = {
|
||
'keyword': keyword,
|
||
'page': page,
|
||
'order': order,
|
||
'search_type': 'video',
|
||
'tids': 0,
|
||
'duration': 0,
|
||
}
|
||
|
||
# 尝试添加WBI签名
|
||
try:
|
||
signed_params = encode_wbi(params)
|
||
use_wbi = True
|
||
except:
|
||
# WBI签名失败,使用普通搜索
|
||
signed_params = params
|
||
use_wbi = False
|
||
print("WBI签名获取失败,使用普通搜索模式")
|
||
|
||
url = 'https://api.bilibili.com/x/web-interface/search/type'
|
||
response = requests.get(url, params=signed_params, cookies=cookies, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
if 'result' in data['data']:
|
||
results = []
|
||
for item in data['data']['result']:
|
||
if item['type'] == 'video':
|
||
video_info = {
|
||
'title': re.sub(r'<.*?>', '', item['title']),
|
||
'bvid': item['bvid'],
|
||
'aid': item['aid'],
|
||
'author': item['author'],
|
||
'mid': item['mid'],
|
||
'play': item['play'],
|
||
'favorites': item['favorites'],
|
||
'duration': item['duration'],
|
||
'description': item['description'],
|
||
'pubdate': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(item['pubdate'])),
|
||
'tag': item['tag']
|
||
}
|
||
results.append(video_info)
|
||
return results
|
||
else:
|
||
print("未找到相关视频")
|
||
return []
|
||
else:
|
||
print(f"搜索失败:{data['message']}")
|
||
return []
|
||
|
||
except Exception as e:
|
||
print(f"搜索时出错: {str(e)}")
|
||
print("错误详细信息:")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return []
|
||
|
||
def show_search_menu(cookies: Dict) -> None: # 添加cookies参数
|
||
"""显示搜索菜单"""
|
||
print("\n=== 视频搜索 ===")
|
||
print("排序方式:")
|
||
print("1. 综合排序")
|
||
print("2. 最多点击")
|
||
print("3. 最新发布")
|
||
print("4. 最多弹幕")
|
||
print("5. 最多收藏")
|
||
|
||
order_map = {
|
||
'1': 'totalrank',
|
||
'2': 'click',
|
||
'3': 'pubdate',
|
||
'4': 'dm',
|
||
'5': 'stow'
|
||
}
|
||
|
||
keyword = input("\n请输入搜索关键词: ").strip()
|
||
if not keyword:
|
||
print("搜索关键词不能为空!")
|
||
return
|
||
|
||
order = input("请选择排序方式 [1]: ").strip()
|
||
if not order:
|
||
order = '1'
|
||
|
||
if order not in order_map:
|
||
print("无效的排序方式,使用默认排序")
|
||
order = '1'
|
||
|
||
page = input("请输入页码 [1]: ").strip()
|
||
if not page or not page.isdigit():
|
||
page = 1
|
||
else:
|
||
page = int(page)
|
||
|
||
results = search_videos(keyword, cookies, page, order_map[order]) # 传入cookies
|
||
|
||
if results:
|
||
print(f"\n找到 {len(results)} 个视频:")
|
||
print("="*50)
|
||
|
||
for i, video in enumerate(results, 1):
|
||
print(f"{i}. {video['title']}")
|
||
print(f" UP主: {video['author']}")
|
||
print(f" 播放: {video['play']} | 收藏: {video['favorites']} | 时长: {video['duration']}")
|
||
print(f" 发布时间: {video['pubdate']}")
|
||
print(f" BV号: {video['bvid']}")
|
||
print(f" 标签: {video['tag']}")
|
||
print(f" 简介: {video['description'][:100]}..." if len(video['description']) > 100 else f" 简介: {video['description']}")
|
||
print("-"*50)
|
||
|
||
while True:
|
||
choice = input("\n请选择要操作的视频序号 (输入q返回): ").strip().lower()
|
||
if choice == 'q':
|
||
break
|
||
|
||
if choice.isdigit() and 1 <= int(choice) <= len(results):
|
||
video = results[int(choice)-1]
|
||
print("\n请选择操作:")
|
||
print("1. 下载音频")
|
||
print("2. 获取弹幕")
|
||
print("3. 获取评论")
|
||
print("4. 举报视频")
|
||
print("5. ���回")
|
||
|
||
op = input("\n请选择 [1]: ").strip()
|
||
if not op:
|
||
op = "1"
|
||
|
||
if op == "1":
|
||
download_audio(video['bvid'], cookies)
|
||
elif op == "2":
|
||
# 获取视频cid
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
video_url = f'https://api.bilibili.com/x/web-interface/view?bvid={video["bvid"]}'
|
||
response = requests.get(video_url, headers=headers)
|
||
data = response.json()
|
||
if data['code'] == 0:
|
||
cid = data['data']['cid']
|
||
get_danmaku(cid, video['bvid'], 1, cookies)
|
||
elif op == "3":
|
||
get_hot_comments(video['bvid'])
|
||
elif op == "4":
|
||
show_report_menu(video['bvid'], cookies)
|
||
else:
|
||
print("无效的选择!")
|
||
|
||
input("\n按回车继续...")
|
||
|
||
def batch_report_videos(cookies: Dict) -> None:
|
||
"""批量举报视频"""
|
||
print("\n=== 批量举报视频 ===")
|
||
|
||
# 1. 先搜索视频
|
||
print("排序方式:")
|
||
print("1. 综合排序")
|
||
print("2. 最多点击")
|
||
print("3. 最新发布")
|
||
print("4. 最多弹幕")
|
||
print("5. 最多收藏")
|
||
|
||
order_map = {
|
||
'1': 'totalrank',
|
||
'2': 'click',
|
||
'3': 'pubdate',
|
||
'4': 'dm',
|
||
'5': 'stow'
|
||
}
|
||
|
||
keyword = input("\n请输入搜索关键词: ").strip()
|
||
if not keyword:
|
||
print("搜索键词不能为!")
|
||
return
|
||
|
||
order = input("请选择排序方式 [1]: ").strip()
|
||
if not order:
|
||
order = '1'
|
||
|
||
if order not in order_map:
|
||
print("无效的排序方式,使用默认排序")
|
||
order = '1'
|
||
|
||
page = input("请输入页码 [1]: ").strip()
|
||
if not page or not page.isdigit():
|
||
page = 1
|
||
else:
|
||
page = int(page)
|
||
|
||
results = search_videos(keyword, page, order_map[order])
|
||
|
||
if not results:
|
||
return
|
||
|
||
# 2. 显示搜索结果并让用户选择要举报的视频
|
||
print(f"\n找到 {len(results)} 个视频:")
|
||
print("="*50)
|
||
|
||
for i, video in enumerate(results, 1):
|
||
print(f"{i}. {video['title']}")
|
||
print(f" UP主: {video['author']}")
|
||
print(f" 播放: {video['play']} | 收藏: {video['favorites']} | 时长: {video['duration']}")
|
||
print(f" 发布时间: {video['pubdate']}")
|
||
print(f" BV号: {video['bvid']}")
|
||
print(f" 标签: {video['tag']}")
|
||
print(f" 简介: {video['description'][:100]}..." if len(video['description']) > 100 else f" 简介: {video['description']}")
|
||
print("-"*50)
|
||
|
||
print("\n请选择要举报的视频序号(用空格分隔,例如:1 3 5)")
|
||
print("支持范围选择(例如:1-5 7 9-11)")
|
||
choice = input("请选择: ").strip()
|
||
|
||
selected_videos = set()
|
||
for part in choice.split():
|
||
if '-' in part:
|
||
start, end = map(int, part.split('-'))
|
||
selected_videos.update(range(start, end +1))
|
||
else:
|
||
if part.isdigit():
|
||
selected_videos.add(int(part))
|
||
|
||
# 过滤无效的序号
|
||
valid_videos = sorted([i for i in selected_videos if 1 <= i <= len(results)])
|
||
|
||
if not valid_videos:
|
||
print("未选择有效的视频!")
|
||
return
|
||
|
||
# 3. 获取举报类型
|
||
report_types = get_report_types()
|
||
if not report_types:
|
||
print("\n获取举报类型失败,使用默认类型")
|
||
report_types = {
|
||
"2": {"name": "违法违禁", "remark": "为帮助审核人员更快处理,补充违规内容出现位置", "controls": None},
|
||
"3": {"name": "色情低俗", "remark": "为帮助审核人员更快处理,补充违规内容出现位置", "controls": None},
|
||
"5": {"name": "赌博诈骗", "remark": "为帮助审���人员更快处理��补充违规内容出现位置", "controls": None},
|
||
"6": {"name": "血腥暴力", "remark": "为帮助审核人员更快处理,补充违规内容出现位置", "controls": None},
|
||
"7": {"name": "人身攻击", "remark": "为帮助审核人员更快处理,补充违规内容出现位置", "controls": None},
|
||
}
|
||
|
||
print("\n请选择举报理由:")
|
||
for tid, info in report_types.items():
|
||
print(f"{tid}. {info['name']}")
|
||
|
||
tid = input("\n请选择举报理由编号: ").strip()
|
||
|
||
if tid not in report_types:
|
||
print("\n无效的举报理由编号!")
|
||
return
|
||
|
||
print(f"\n提示: {report_types[tid]['remark']}")
|
||
|
||
desc = input("\n请输入详细描述(至少10个字):").strip()
|
||
if len(desc) < 10:
|
||
print("\n描述太短,至少需要10个字!")
|
||
return
|
||
|
||
attach = ""
|
||
# 如果需要额外信息(如撞车举报)
|
||
if report_types[tid]['controls']:
|
||
for control in report_types[tid]['controls']:
|
||
if control['required']:
|
||
value = input(f"\n请输入{control['title']} ({control['placeholder']}): ").strip()
|
||
if not value:
|
||
print(f"\n{control['title']}为必填项!")
|
||
return
|
||
attach = value
|
||
else:
|
||
# 普通举报,询问是否需要上传图片
|
||
print("\n是否上传举报附件图片?")
|
||
print("1. 上传本地图片")
|
||
print("2. 直接输入图片URL")
|
||
print("3. 跳过")
|
||
|
||
upload_choice = input("\n请选择 [3]: ").strip()
|
||
if not upload_choice:
|
||
upload_choice = "3"
|
||
|
||
if upload_choice == "1":
|
||
image_path = input("\n请输入图片文件路径: ").strip()
|
||
if os.path.exists(image_path):
|
||
image_url = upload_report_image(image_path, cookies)
|
||
if image_url:
|
||
attach = image_url
|
||
else:
|
||
print("\n图片上传失败!")
|
||
else:
|
||
print("\n找不到图片文件!")
|
||
elif upload_choice == "2":
|
||
attach = input("\n请输入图片URL(多个用逗号分隔):").strip()
|
||
|
||
# 4. 确认并执行批量举报
|
||
print("\n以下视频将被举报:")
|
||
for i in valid_videos:
|
||
video = results[i-1]
|
||
print(f"- {video['title']} (BV号: {video['bvid']}")
|
||
print(f"\n举报理由: {report_types[tid]['name']}")
|
||
print(f"描述: {desc}")
|
||
if attach:
|
||
if report_types[tid]['controls']:
|
||
print(f"{report_types[tid]['controls'][0]['title']}: {attach}")
|
||
else:
|
||
print(f"附件: {attach}")
|
||
|
||
confirm = input("\n确认提交举报?(y/n) [n]: ").lower()
|
||
if confirm == 'y':
|
||
success_count = 0
|
||
fail_count = 0
|
||
|
||
# 创建进度条
|
||
with tqdm(total=len(valid_videos), desc="举报进度") as pbar:
|
||
for i in valid_videos:
|
||
video = results[i-1]
|
||
try:
|
||
if report_types[tid]['controls']:
|
||
# 特殊举报(如撞车)不需要上传图片
|
||
if report_video(str(video['aid']), tid, desc, attach, cookies):
|
||
success_count += 1
|
||
else:
|
||
fail_count += 1
|
||
else:
|
||
# 普通举报
|
||
if report_video_with_image(str(video['aid']), tid, desc, attach, cookies):
|
||
success_count += 1
|
||
else:
|
||
fail_count += 1
|
||
except Exception as e:
|
||
print(f"\n举报视频 {video['bvid']} 时出错: {str(e)}")
|
||
fail_count += 1
|
||
|
||
pbar.update(1)
|
||
# 添加延时,避免请求过快
|
||
time.sleep(2)
|
||
|
||
print(f"\n批量举报完成!成功: {success_count}, 失败: {fail_count}")
|
||
|
||
def save_report_record(video_info: dict, report_info: dict) -> None:
|
||
"""保存举报记录
|
||
|
||
Args:
|
||
video_info (dict): 视频信息
|
||
report_info (dict): 举报信息
|
||
"""
|
||
try:
|
||
record_dir = os.path.join("举报", "记录")
|
||
filename = f"{time.strftime('%Y-%m-%d %H-%M-%S')} {video_info['bvid']}.txt"
|
||
filepath = os.path.join(record_dir, filename)
|
||
|
||
with open(filepath, 'w', encoding='utf-8') as f:
|
||
f.write("视频信息:\n")
|
||
f.write(f"标题: {video_info['title']}\n")
|
||
f.write(f"BV号: {video_info['bvid']}\n")
|
||
f.write(f"AV号: {video_info['aid']}\n")
|
||
f.write(f"UP主: {video_info['author']}\n")
|
||
f.write(f"发布时间: {video_info['pubdate']}\n")
|
||
f.write("\n举报信息:\n")
|
||
f.write(f"举报理由: {report_info['reason']}\n")
|
||
f.write(f"详细描述: {report_info['desc']}\n")
|
||
if report_info.get('attach'):
|
||
if report_info.get('is_image'):
|
||
f.write(f"附件图片: {report_info['attach']}\n")
|
||
else:
|
||
f.write(f"附加信息: {report_info['attach']}\n")
|
||
f.write(f"举报时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||
|
||
print(f"\n举报记录已保存: {filepath}")
|
||
|
||
except Exception as e:
|
||
print(f"保存举报记录时出错: {str(e)}")
|
||
|
||
def get_wbi_keys(cookies: Dict = None) -> tuple:
|
||
"""获取最新的 img_key 和 sub_key,带缓存机制
|
||
|
||
Args:
|
||
cookies (Dict): cookies信息
|
||
|
||
Returns:
|
||
tuple: (img_key, sub_key)
|
||
"""
|
||
cache_file = os.path.join("举报", "wbi_cache.json")
|
||
|
||
try:
|
||
# 检查缓存是否存在且未过期
|
||
if os.path.exists(cache_file):
|
||
with open(cache_file, 'r', encoding='utf-8') as f:
|
||
cache = json.load(f)
|
||
# 检查缓存是否在24小时内
|
||
if time.time() - cache['timestamp'] < 24 * 3600:
|
||
return cache['img_key'], cache['sub_key']
|
||
|
||
# 缓存不存在或已过期,重新获取
|
||
headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.bilibili.com',
|
||
}
|
||
|
||
url = 'https://api.bilibili.com/x/web-interface/nav'
|
||
# 添加cookies参数
|
||
response = requests.get(url, cookies=cookies, headers=headers)
|
||
data = response.json()
|
||
|
||
if data['code'] == 0:
|
||
img_url = data['data']['wbi_img']['img_url']
|
||
sub_url = data['data']['wbi_img']['sub_url']
|
||
|
||
img_key = os.path.splitext(os.path.basename(img_url))[0]
|
||
sub_key = os.path.splitext(os.path.basename(sub_url))[0]
|
||
|
||
# 保存到缓存
|
||
cache_data = {
|
||
'img_key': img_key,
|
||
'sub_key': sub_key,
|
||
'timestamp': time.time()
|
||
}
|
||
|
||
# 确保缓存目录存在
|
||
os.makedirs(os.path.dirname(cache_file), exist_ok=True)
|
||
|
||
with open(cache_file, 'w', encoding='utf-8') as f:
|
||
json.dump(cache_data, f, indent=2)
|
||
|
||
return img_key, sub_key
|
||
else:
|
||
print(f"获取WBI keys失败: {data['message']}")
|
||
return None, None
|
||
|
||
except Exception as e:
|
||
print(f"获取WBI keys时出错: {str(e)}")
|
||
return None, None
|
||
|
||
def get_mixin_key(raw_key: str) -> str:
|
||
"""生成 mixin_key
|
||
|
||
Args:
|
||
raw_key (str): img_key + sub_key 拼接的字符串
|
||
|
||
Returns:
|
||
str: mixin_key
|
||
"""
|
||
MIXIN_KEY_ENC_TAB = [
|
||
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35,
|
||
27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13,
|
||
37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4,
|
||
22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52
|
||
]
|
||
|
||
# 对raw_key按照MIXIN_KEY_ENC_TAB的索引进行重排
|
||
mixin_key = ''
|
||
for i in MIXIN_KEY_ENC_TAB:
|
||
if i < len(raw_key):
|
||
mixin_key += raw_key[i]
|
||
|
||
# 截取前32位
|
||
return mixin_key[:32]
|
||
|
||
def encode_wbi(params: dict, cookies: Dict = None) -> dict:
|
||
"""为请求参数进行WBI签名
|
||
|
||
Args:
|
||
params (dict): 原始请求参数
|
||
cookies (Dict): cookies信息
|
||
|
||
Returns:
|
||
dict: 加入w_rid和wts的新参数
|
||
"""
|
||
# 获取img_key和sub_key,传入cookies
|
||
img_key, sub_key = get_wbi_keys(cookies)
|
||
if not img_key or not sub_key:
|
||
return params
|
||
|
||
# 生成mixin_key
|
||
mixin_key = get_mixin_key(img_key + sub_key)
|
||
|
||
# 添加wts参数
|
||
wts = str(int(time.time()))
|
||
params['wts'] = wts
|
||
|
||
# 按键名升序排序并拼接参数
|
||
sorted_params = dict(sorted(params.items()))
|
||
query = []
|
||
for k, v in sorted_params.items():
|
||
# URL编码,需要大写
|
||
query.append(f"{k}={requests.utils.quote(str(v), safe='').upper()}")
|
||
query = '&'.join(query)
|
||
|
||
# 计算w_rid
|
||
w_rid = hashlib.md5((query + mixin_key).encode()).hexdigest()
|
||
|
||
# 将w_rid和wts添加到原始参数中
|
||
params['w_rid'] = w_rid
|
||
|
||
return params
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
# 检查必要的文件和文件夹
|
||
if not os.path.exists('cookies.txt'):
|
||
print("错误:找不到cookies.txt文件!")
|
||
print("请确保cookies.txt文件存在且包含有效的cookies信息")
|
||
input("\n按回车键退出...")
|
||
exit(1)
|
||
|
||
# 检查是否能导入必要模块
|
||
try:
|
||
import google.protobuf.text_format as text_format
|
||
from bilibili.community.service.dm.v1 import dm_pb2 as Danmaku
|
||
except ImportError as e:
|
||
print("错误:缺少必要的模块!")
|
||
print(f"导入错误: {str(e)}")
|
||
print("\n请确保已安装以下模块:")
|
||
print("1. protobuf")
|
||
print("2. 已编译生成dm_pb2.py文件")
|
||
input("\n按回车键退出...")
|
||
exit(1)
|
||
|
||
# 运行主程序
|
||
main()
|
||
|
||
except Exception as e:
|
||
print("\n程序出现错误:")
|
||
print(f"错误类型: {type(e).__name__}")
|
||
print(f"错误信息: {str(e)}")
|
||
print("\n错误详细信息:")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
finally:
|
||
print("\n" + "="*50)
|
||
input("按回车键退出...") |