diff --git a/哔哩哔哩/1.0bilibili_audio_dl.py b/哔哩哔哩/1.0bilibili_audio_dl.py new file mode 100644 index 0000000..8b2150b --- /dev/null +++ b/哔哩哔哩/1.0bilibili_audio_dl.py @@ -0,0 +1,367 @@ +import json +import os +import subprocess +from typing import Dict +from tqdm import tqdm +import re + +def convert_browser_cookies(browser_cookies: list) -> Dict: + """转换浏览器导出的cookies格式为简单的键值对""" + essential_cookies = {} + required_keys = ['SESSDATA', 'bili_jct', 'DedeUserID'] + + for cookie in browser_cookies: + if cookie['name'] in required_keys: + essential_cookies[cookie['name']] = cookie['value'] + + return essential_cookies + +def load_cookies_from_file() -> Dict: + """从文件加载cookies""" + cookie_files = ['cookies.txt', 'bilibili_cookies.json'] + required_keys = ['SESSDATA', 'bili_jct', 'DedeUserID'] + + for cookie_file in cookie_files: + if os.path.exists(cookie_file): + try: + with open(cookie_file, 'r', encoding='utf-8') as f: + cookies = {} + # 逐行读取文件 + for line in f: + line = line.strip() + # 跳过空行和注释行(但不跳过#HttpOnly_开头的行) + if not line or (line.startswith('#') and not line.startswith('#HttpOnly_')): + continue + + try: + # 处理HttpOnly标记 + if line.startswith('#HttpOnly_'): + line = line[9:] # 移除'#HttpOnly_'前缀 + + # 分割字段并移除可能的空字段 + fields = [f for f in line.split('\t') if f] + if len(fields) >= 7: + name = fields[5] + value = fields[6] + if name in required_keys: + # 对于SESSDATA,需要处理URL编码 + if name == 'SESSDATA' and '%2C' in value: + value = value.replace('%2C', ',') + cookies[name] = value + print(f"找到cookie: {name}") # 调试信息 + except Exception as e: + print(f"警告: 解析行 '{line}' 时出错: {str(e)}") + continue + + # 检查是否找到所有必需的cookies + if all(key in cookies for key in required_keys): + print(f"已从 {cookie_file} 加载cookies") + return cookies + else: + missing = [key for key in required_keys if key not in cookies] + print(f"警告: {cookie_file} 中缺少以下cookie字段: {', '.join(missing)}") + except Exception as e: + print(f"警告: 读取 {cookie_file} 时出错: {str(e)}") + return {} + +def parse_cookie_string(cookie_str: str) -> Dict: + """解析浏览器直接复制的cookie字符串""" + cookies = {} + required_keys = ['SESSDATA', 'bili_jct', 'DedeUserID'] + + try: + # 分割cookie字符串 + cookie_pairs = cookie_str.split(';') + for pair in cookie_pairs: + if '=' in pair: + name, value = pair.strip().split('=', 1) + if name in required_keys: + cookies[name] = value + except Exception: + 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) + + # 验证必要的cookie字段 + if all(key in cookies for key in ['SESSDATA', 'bili_jct', 'DedeUserID']): + return cookies + else: + print("Cookie缺少必要字段!需要SESSDATA、bili_jct和DedeUserID") + return {} + +def save_cookies(cookies: Dict): + """保存cookies为Netscape格式""" + with open('cookies.txt', 'w', encoding='utf-8') as f: + # 写入Netscape cookies文件头 + 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(): + # 特殊处理SESSDATA,添加HttpOnly和Secure标记 + if name == '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)}") + # 询问是否删除原始文件 + if input("是否删除原始音频文件?(y/n): ").lower() == '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: str, cookies: Dict): + """下载音频""" + save_cookies(cookies) + + try: + cmd = [ + 'yt-dlp', + '--cookies', 'cookies.txt', + '-f', 'ba', + '--no-playlist', + '--no-check-certificates', + '--progress', + '--newline', + ] + + cmd.append(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): ").lower() == '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取消):") + 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 main(): + # 先尝试从文件加载cookies + cookies = load_cookies_from_file() + + # 如果没有找到有效的cookies文件,则请求用户输入 + if not cookies: + cookies = get_cookies_from_user() + if not cookies: + return + # 保存用户输入的cookies + save_cookies(cookies) + + while True: + print("\n请输入B站视频URL (输入q退出):") + url = input().strip() + + if url.lower() == 'q': + break + + if not url.startswith(('https://www.bilibili.com', 'https://b23.tv')): + print("请输入有效的B站视频链接!") + continue + + download_audio(url, cookies) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/哔哩哔哩/10.0bilibili_audio_dl.py b/哔哩哔哩/10.0bilibili_audio_dl.py new file mode 100644 index 0000000..5d9226e --- /dev/null +++ b/哔哩哔哩/10.0bilibili_audio_dl.py @@ -0,0 +1,2710 @@ +import json +import os +import subprocess +from typing import Dict +from tqdm import tqdm +import re +import requests +import time + +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: + # 获取用户基本信息 + myinfo_url = 'http://api.bilibili.com/x/space/myinfo' + myinfo_resp = requests.get(myinfo_url, cookies=cookies, headers=headers) + myinfo_data = myinfo_resp.json() + + user_id = None + if myinfo_data['code'] == 0: + user_data = myinfo_data['data'] + user_id = user_data['mid'] # 获取用户ID + print(f"用户名: {user_data['name']}") + print(f"用户ID: {user_id}") + print(f"等级: LV{user_data['level']}") + print(f"会员类型: {'大会员' if user_data['vip']['status'] == 1 else '普通用户'}") + print(f"性别: {user_data['sex']}") + print(f"签名: {user_data['sign']}") + + # 获取关注和粉丝数 + if user_id: + stat_url = f'https://api.bilibili.com/x/relation/stat?vmid={user_id}' + 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"关注数: {stat_info['following']}") + print(f"粉丝数: {stat_info['follower']}") + + # 获取硬币数 + coin_url = 'http://account.bilibili.com/site/getCoin' + coin_resp = requests.get(coin_url, cookies=cookies, headers=headers) + coin_data = coin_resp.json() + + if coin_data['code'] == 0: + print(f"硬币数: {coin_data['data']['money']}") + + # 获取今日投币经验 + 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 # B站每日投币多获得50验 + print(f"今日投币经验: {today_exp}/{max_coin_exp}") + remaining_coins = (max_coin_exp - today_exp) // 10 # 每个硬币��得10经验 + if remaining_coins > 0: + print(f"还���投: {remaining_coins}个") + + # 获取登录信息 + 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: + nav_info = nav_data['data'] + print(f"当前登录状态: {'已登录' if nav_info['isLogin'] else '未登录'}") + if nav_info['isLogin']: + print(f"邮箱: {nav_info['email']}") + print(f"硬币余额: {nav_info['money']}") + print(f"B币余额: {nav_info['wallet']['bcoin_balance']}") + + 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. 退出程序") # 原来的退出选项序号改为12 + print("="*50) + + choice = input("请选择功能 (1-12) [1]: ").strip() + if not choice: + return '1' + if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']: + 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("弹幕", "直播弹幕"), # 存放直播弹幕文件 + "评论", # 存放评论文件 + "音频", # 存放下载的音频文件 + ] + + 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感谢使用!再见!") + 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, # 视频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"{'-' * 50}" + ) + + # 打印到控制台 + print(danmaku_info) + + # 添加点赞选项 + print(danmaku_info) + choice = input("是否点赞该弹幕?(y/n) [n]: ").lower() + if choice == 'y': + like_danmaku(str(elem.id), cid, cookies) # 需要传入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) + + # 生成文件名(BV号 + 当前时间 + 弹幕后缀�� + 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)}") + +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": + # 持续监听 + 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: + 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 _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 '' + +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("按回车键退出...") \ No newline at end of file diff --git a/哔哩哔哩/11.0bilibili_audio_dl.py b/哔哩哔哩/11.0bilibili_audio_dl.py new file mode 100644 index 0000000..697c539 --- /dev/null +++ b/哔哩哔哩/11.0bilibili_audio_dl.py @@ -0,0 +1,2703 @@ +import json +import os +import subprocess +from typing import Dict +from tqdm import tqdm +import re +import requests +import time + +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: + # 获取用户基本信息 + myinfo_url = 'http://api.bilibili.com/x/space/myinfo' + myinfo_resp = requests.get(myinfo_url, cookies=cookies, headers=headers) + myinfo_data = myinfo_resp.json() + + user_id = None + if myinfo_data['code'] == 0: + user_data = myinfo_data['data'] + user_id = user_data['mid'] # 获取用户ID + print(f"用户名: {user_data['name']}") + print(f"用户ID: {user_id}") + print(f"等级: LV{user_data['level']}") + print(f"会员类型: {'大会员' if user_data['vip']['status'] == 1 else '普通用户'}") + print(f"性别: {user_data['sex']}") + print(f"签名: {user_data['sign']}") + + # 获取关注和粉丝数 + if user_id: + stat_url = f'https://api.bilibili.com/x/relation/stat?vmid={user_id}' + 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"关注数: {stat_info['following']}") + print(f"粉丝数: {stat_info['follower']}") + + # 获取硬币数 + coin_url = 'http://account.bilibili.com/site/getCoin' + coin_resp = requests.get(coin_url, cookies=cookies, headers=headers) + coin_data = coin_resp.json() + + if coin_data['code'] == 0: + print(f"硬币数: {coin_data['data']['money']}") + + # 获取今日投币经验 + 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 # B站每日投币多获得50验 + print(f"今日投币经验: {today_exp}/{max_coin_exp}") + remaining_coins = (max_coin_exp - today_exp) // 10 # 每个硬币��得10经验 + if remaining_coins > 0: + print(f"还���投: {remaining_coins}个") + + # 获取登录信息 + 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: + nav_info = nav_data['data'] + print(f"当前登录状态: {'已登录' if nav_info['isLogin'] else '未登录'}") + if nav_info['isLogin']: + print(f"邮箱: {nav_info['email']}") + print(f"硬币余额: {nav_info['money']}") + print(f"B币余额: {nav_info['wallet']['bcoin_balance']}") + + 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. 退出程序") # 原来的退出选项序号改为12 + print("="*50) + + choice = input("请选择功能 (1-12) [1]: ").strip() + if not choice: + return '1' + if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']: + 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("弹幕", "直播弹幕"), # 存放直播弹幕文件 + "评论", # 存放评论文件 + "音频", # 存放下载的音频文件 + ] + + 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感谢使用!再见!") + 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": + # 持续监听 + 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: + 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 _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 '' + +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("按回车键退出...") \ No newline at end of file diff --git a/哔哩哔哩/12.0bilibili_audio_dl.py b/哔哩哔哩/12.0bilibili_audio_dl.py new file mode 100644 index 0000000..6108487 --- /dev/null +++ b/哔哩哔哩/12.0bilibili_audio_dl.py @@ -0,0 +1,3875 @@ +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("按回车键退出...") \ No newline at end of file diff --git a/哔哩哔哩/13.0bilibili_audio_dl.py b/哔哩哔哩/13.0bilibili_audio_dl.py new file mode 100644 index 0000000..388d7a5 --- /dev/null +++ b/哔哩哔哩/13.0bilibili_audio_dl.py @@ -0,0 +1,3875 @@ +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签名,传入cookies + try: + signed_params = encode_wbi(params, cookies) # 确保传入cookies + 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) # 确保传入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("按回车键退出...") \ No newline at end of file diff --git a/哔哩哔哩/14.0bilibili_audio_dl.py b/哔哩哔哩/14.0bilibili_audio_dl.py new file mode 100644 index 0000000..f20a2e5 --- /dev/null +++ b/哔哩哔哩/14.0bilibili_audio_dl.py @@ -0,0 +1,4081 @@ +import json +import os +import subprocess +import re +import requests +import time +import random +import base64 +import shutil +import hashlib +from typing import Dict +from tqdm import tqdm + +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. 下载视频音频") + print("5. 获取视频BV号") + print("6. 查看用户信息") + print("7. bv转av") + print("8. 获取视频弹幕") + print("9. 获取视频热评") + print("10. 查看IP地理位置") + print("11. 直播") + print("12. 弹幕点赞") + print("13. 老粉计划") + print("14. 举报视频") + print("15. 搜索视频") + print("16. 退出程序") + print("="*50) + + choice = input("请选择功能 (1-16) [1]: ").strip() + if not choice: + return '1' # 直接回车默认选择1号功能(举报麦片) + if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16']: + 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) + 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': + # 举报麦片 + show_maipi_menu(cookies) + + elif choice == '2': + # ���量点赞 + batch_like(cookies) + + elif choice == '3': + # 批量投币 + batch_coin(cookies) + + elif choice == '4': + # 下载视频音频 + 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 == '5': + # 获取视频BV号 + get_video_bvids() + + elif choice == '6': + # 查看用户信息 + get_user_info(cookies) + + elif choice == '7': + # 新增的功能: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 == '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: + 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 == '9': + # 获取视频热评 + 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 == '10': + # 查看IP地理位置 + get_ip_location() + + elif choice == '11': + # 进入直播子菜单 + 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 == '12': + # 弹幕点赞功能 + 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 == '13': # 原来退出选项改为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 == '14': + # 举报视频 + 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 == '15': + show_search_menu(cookies) # 传入cookies + + elif choice == '16': + 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', + '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} + count_response = requests.get(count_url, params=params, headers=headers) + count_data = count_response.json() + + return count_data['data']['count'] if count_data['code'] == 0 else 0 + + return 0 + except: + 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', + 'Referer': 'https://live.bilibili.com' + } + + seen_msgs = set() # 添加这行来定义 seen_msgs + + 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: + """获取视频举报类型列表""" + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/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']}") + + while True: # 添加循环让用户重新输入 + desc = input("\n请输入详细描述(至少10个字):").strip() + if len(desc) < 10: + print("\n描述太短,至少需要10个字!请重新输入") + continue # 继续循环让用户重新输入 + break # 描述长度符合要求时跳出循环 + + 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) [y]: ").lower() # 修改默认提示为[y] + if not confirm or confirm == 'y': # 如果直接回车或输入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_request(keyword: str, cookies: Dict, page: int = 1, order: str = 'totalrank', ps: int = 50) -> list: + """搜索请求API""" + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Referer': 'https://search.bilibili.com' + } + + try: + params = { + 'keyword': keyword, + 'page': page, + 'order': order, + 'search_type': 'video', + 'tids': 0, + 'duration': 0, + 'ps': ps + } + + # 使用新的搜索API + url = 'https://api.bilibili.com/x/web-interface/wbi/search/all/v2' + response = requests.get(url, params=params, cookies=cookies, headers=headers) + data = response.json() + + if data['code'] == 0: + results = [] + # 从返回的数据中提取视频信息 + for result in data['data']['result']: + if result['result_type'] == 'video': + for video in result['data']: + video_info = { + 'title': re.sub(r'<.*?>', '', video['title']), + 'bvid': video['bvid'], + 'aid': video['aid'], + 'author': video['author'], + 'mid': video['mid'], + 'play': video['play'], + 'favorites': video.get('favorites', 0), + 'duration': video['duration'], + 'description': video['description'], + 'pubdate': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(video['pubdate'])), + 'tag': video.get('tag', '') + } + results.append(video_info) + return results + else: + print(f"搜索失败:{data['message']}") + return [] + + except Exception as e: + print(f"搜索时出错: {str(e)}") + return [] + +def search_videos_result(keyword: str, cookies: Dict, page: int = 1, order: str = 'totalrank', ps: int = 50) -> list: + """搜索结果API (原来的搜索函数)""" + # 保持原来的 search_videos 函数代码不变,只是改名 + # ... 原有代码 ... + +def show_search_menu(cookies: Dict) -> None: + """显示搜索菜单""" + 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: # 如果直接回车 + keyword = "圣体" # 默认关键词 + order = '3' # 默认使用最新发布排序 + else: + 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]) + + 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" 视频链接: https://www.bilibili.com/video/{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_request(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("举报", "记录") + if not os.path.exists(record_dir): + os.makedirs(record_dir) + + # 使用日期作为子文件夹 + date_dir = os.path.join(record_dir, time.strftime('%Y-%m-%d')) + if not os.path.exists(date_dir): + os.makedirs(date_dir) + + # 文件名格式: 时间_BV号.txt + filename = f"{time.strftime('%H-%M-%S')}_{video_info['bvid']}.txt" + filepath = os.path.join(date_dir, filename) + + with open(filepath, 'w', encoding='utf-8') as f: + f.write("="*50 + "\n") + f.write("视频信息:\n") + f.write("-"*30 + "\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(f"视频链接: https://www.bilibili.com/video/{video_info['bvid']}\n") + + f.write("\n举报信息:\n") + f.write("-"*30 + "\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") + f.write("="*50 + "\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) # 确保传入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 + +def save_reported_bvids(bvids: list) -> None: + """保存已举报的BV号列表""" + report_dir = os.path.join("举报", "记录") + if not os.path.exists(report_dir): + os.makedirs(report_dir) + + reported_file = os.path.join(report_dir, "reported_videos.txt") + + try: + with open(reported_file, 'a', encoding='utf-8') as f: + for bvid in bvids: + f.write(f"{bvid}\n") + except Exception as e: + print(f"保存已举报BV号时出错: {str(e)}") + +def get_reported_bvids() -> set: + """获取已举报的BV号集合""" + reported_file = os.path.join("举报", "记录", "reported_videos.txt") + reported_bvids = set() + + if os.path.exists(reported_file): + try: + with open(reported_file, 'r', encoding='utf-8') as f: + for line in f: + bvid = line.strip() + if bvid: + reported_bvids.add(bvid) + except Exception as e: + print(f"读取已举报BV号时出错: {str(e)}") + + return reported_bvids + +def show_maipi_menu(cookies: Dict) -> None: + """显示麦片举报菜单""" + print("\n=== 麦片举报 ===") + print("请选择搜索关键词:") + print("1. 圣体") + print("2. 大雷") + + choice = input("\n请选择 [1]: ").strip() + if not choice: + choice = "1" + + keywords = { + "1": "圣体", + "2": "大雷" + } + + # 定义关键词列表 + blacklist_keywords = ["健身", "肌肉", "训练"] + whitelist_keywords = ["变速卡点", "美女", "诱惑", "小姐姐", "大长腿"] + title_keywords = { + "1": ["圣体", "麦片", "黄片", "色情", "福利"], + "2": ["大雷", "麦片", "黄片", "色情", "福利"] + } + + if choice in keywords: + keyword = keywords[choice] + page = 1 + reported_bvids = get_reported_bvids() + + while True: + results = search_videos(keyword, cookies, page=page, order='pubdate', ps=50) + + if not results: + print("没有更多视频了") + break + + # 过滤和排序视频 + filtered_results = [] + + for video in results: + if video['bvid'] not in reported_bvids: + title = video['title'].lower() + + # 跳过黑名单关键词 + if any(kw.lower() in title for kw in blacklist_keywords): + continue + + # 必须包含必要关键词中的任意一个 + if any(kw.lower() in title for kw in title_keywords[choice]): + filtered_results.append(video) + + results = filtered_results + + if not results: + print("当前页没有符合条件的视频,正在获取下一页...") + page += 1 + continue + + 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" 视频链接: https://www.bilibili.com/video/{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 valid_videos: + print("\n以下视频将被举报:") + for i in valid_videos: + video = results[i-1] + print(f"- {video['title']} (BV号: {video['bvid']}") + + # 在使用 report_types 前先获取 + report_types = get_report_types() + if not report_types: + report_types = { + "3": { + "name": "色情低俗", + "remark": "为帮助审核人员更快处理,补充违规内容出现位置", + "controls": None + } + } + + # 自动使用色情低俗(tid=3)举报 + tid = "3" + # 修改默认举报描述,使其更详细 + desc = "这是黄片,审核为什么不管?这种视频明显违规,建议尽快处理,避免造成不良影响。" # 默认举报描述 + + confirm = input("\n确认开始举报?(y/n) [y]: ").lower() + if not confirm or confirm == 'y': + success_count = 0 + fail_count = 0 + reported = [] + + # 创建进度条 + with tqdm(total=len(valid_videos), desc="举报进度") as pbar: + for i in valid_videos: + video = results[i-1] + try: + # 使用普通举报 + if report_video(str(video['aid']), tid, desc, "", cookies, video): + success_count += 1 + reported.append(video['bvid']) + else: + fail_count += 1 + except Exception as e: + print(f"\n举报视频 {video['bvid']} 时出错: {str(e)}") + fail_count += 1 + + pbar.update(1) + time.sleep(2) + + # 保存新举报的BV号 + if reported: + save_reported_bvids(reported) + + print(f"\n批量举报完成!成功: {success_count}, 失败: {fail_count}") + + # 询问是否继续举报下一页 + choice = input("\n是否继续举报下一页视频?(y/n) [y]: ").lower() + if not choice or choice == 'y': # 直接回车或输入y都继续 + page += 1 + continue + else: + break + else: + print("未选择有效的视频!") + break + else: + print("无效的选择!") + +def search_videos(keyword: str, cookies: Dict, page: int = 1, order: str = 'totalrank', ps: int = 50) -> list: + """搜索视频""" + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Referer': 'https://search.bilibili.com' + } + + try: + params = { + 'keyword': keyword, + 'page': page, + 'order': order, + 'search_type': 'video', + 'tids': 0, + 'duration': 0, + 'ps': ps + } + + # 使用搜索结果API而不是搜索请求API + url = 'https://api.bilibili.com/x/web-interface/search/type' + response = requests.get(url, params=params, cookies=cookies, headers=headers) + data = response.json() + + if data['code'] == 0: + results = [] + if 'result' in data['data']: + for item in data['data']['result']: + 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(f"搜索失败:{data['message']}") + return [] + + except Exception as e: + print(f"搜索时出错: {str(e)}") + return [] + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"程序出错: {str(e)}") + import traceback + traceback.print_exc() + finally: + input("\n按回车键退出...") \ No newline at end of file diff --git a/哔哩哔哩/2.0bilibili_audio_dl.py b/哔哩哔哩/2.0bilibili_audio_dl.py new file mode 100644 index 0000000..8b2150b --- /dev/null +++ b/哔哩哔哩/2.0bilibili_audio_dl.py @@ -0,0 +1,367 @@ +import json +import os +import subprocess +from typing import Dict +from tqdm import tqdm +import re + +def convert_browser_cookies(browser_cookies: list) -> Dict: + """转换浏览器导出的cookies格式为简单的键值对""" + essential_cookies = {} + required_keys = ['SESSDATA', 'bili_jct', 'DedeUserID'] + + for cookie in browser_cookies: + if cookie['name'] in required_keys: + essential_cookies[cookie['name']] = cookie['value'] + + return essential_cookies + +def load_cookies_from_file() -> Dict: + """从文件加载cookies""" + cookie_files = ['cookies.txt', 'bilibili_cookies.json'] + required_keys = ['SESSDATA', 'bili_jct', 'DedeUserID'] + + for cookie_file in cookie_files: + if os.path.exists(cookie_file): + try: + with open(cookie_file, 'r', encoding='utf-8') as f: + cookies = {} + # 逐行读取文件 + for line in f: + line = line.strip() + # 跳过空行和注释行(但不跳过#HttpOnly_开头的行) + if not line or (line.startswith('#') and not line.startswith('#HttpOnly_')): + continue + + try: + # 处理HttpOnly标记 + if line.startswith('#HttpOnly_'): + line = line[9:] # 移除'#HttpOnly_'前缀 + + # 分割字段并移除可能的空字段 + fields = [f for f in line.split('\t') if f] + if len(fields) >= 7: + name = fields[5] + value = fields[6] + if name in required_keys: + # 对于SESSDATA,需要处理URL编码 + if name == 'SESSDATA' and '%2C' in value: + value = value.replace('%2C', ',') + cookies[name] = value + print(f"找到cookie: {name}") # 调试信息 + except Exception as e: + print(f"警告: 解析行 '{line}' 时出错: {str(e)}") + continue + + # 检查是否找到所有必需的cookies + if all(key in cookies for key in required_keys): + print(f"已从 {cookie_file} 加载cookies") + return cookies + else: + missing = [key for key in required_keys if key not in cookies] + print(f"警告: {cookie_file} 中缺少以下cookie字段: {', '.join(missing)}") + except Exception as e: + print(f"警告: 读取 {cookie_file} 时出错: {str(e)}") + return {} + +def parse_cookie_string(cookie_str: str) -> Dict: + """解析浏览器直接复制的cookie字符串""" + cookies = {} + required_keys = ['SESSDATA', 'bili_jct', 'DedeUserID'] + + try: + # 分割cookie字符串 + cookie_pairs = cookie_str.split(';') + for pair in cookie_pairs: + if '=' in pair: + name, value = pair.strip().split('=', 1) + if name in required_keys: + cookies[name] = value + except Exception: + 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) + + # 验证必要的cookie字段 + if all(key in cookies for key in ['SESSDATA', 'bili_jct', 'DedeUserID']): + return cookies + else: + print("Cookie缺少必要字段!需要SESSDATA、bili_jct和DedeUserID") + return {} + +def save_cookies(cookies: Dict): + """保存cookies为Netscape格式""" + with open('cookies.txt', 'w', encoding='utf-8') as f: + # 写入Netscape cookies文件头 + 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(): + # 特殊处理SESSDATA,添加HttpOnly和Secure标记 + if name == '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)}") + # 询问是否删除原始文件 + if input("是否删除原始音频文件?(y/n): ").lower() == '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: str, cookies: Dict): + """下载音频""" + save_cookies(cookies) + + try: + cmd = [ + 'yt-dlp', + '--cookies', 'cookies.txt', + '-f', 'ba', + '--no-playlist', + '--no-check-certificates', + '--progress', + '--newline', + ] + + cmd.append(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): ").lower() == '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取消):") + 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 main(): + # 先尝试从文件加载cookies + cookies = load_cookies_from_file() + + # 如果没有找到有效的cookies文件,则请求用户输入 + if not cookies: + cookies = get_cookies_from_user() + if not cookies: + return + # 保存用户输入的cookies + save_cookies(cookies) + + while True: + print("\n请输入B站视频URL (输入q退出):") + url = input().strip() + + if url.lower() == 'q': + break + + if not url.startswith(('https://www.bilibili.com', 'https://b23.tv')): + print("请输入有效的B站视频链接!") + continue + + download_audio(url, cookies) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/哔哩哔哩/3.0ilibili_audio_dl.py b/哔哩哔哩/3.0ilibili_audio_dl.py new file mode 100644 index 0000000..d05964f --- /dev/null +++ b/哔哩哔哩/3.0ilibili_audio_dl.py @@ -0,0 +1,760 @@ +import json +import os +import subprocess +from typing import Dict +from tqdm import tqdm +import re +import requests +import time + +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: + 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: str, cookies: Dict): + """下载音频""" + save_cookies(cookies) + + try: + cmd = [ + 'yt-dlp', + '--cookies', 'cookies.txt', + '-f', 'ba', + '--no-playlist', + '--no-check-certificates', + '--progress', + '--newline', + ] + + cmd.append(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): ").lower() == '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取消):") + 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 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: + # 获取用户基本信息 + myinfo_url = 'http://api.bilibili.com/x/space/myinfo' + myinfo_resp = requests.get(myinfo_url, cookies=cookies, headers=headers) + myinfo_data = myinfo_resp.json() + + user_id = None + if myinfo_data['code'] == 0: + user_data = myinfo_data['data'] + user_id = user_data['mid'] # 获取用户ID + print(f"用户名: {user_data['name']}") + print(f"用户ID: {user_id}") + print(f"等级: LV{user_data['level']}") + print(f"会员类型: {'大会员' if user_data['vip']['status'] == 1 else '普通用户'}") + print(f"性别: {user_data['sex']}") + print(f"签名: {user_data['sign']}") + + # 获取关注和粉丝数 + if user_id: + stat_url = f'https://api.bilibili.com/x/relation/stat?vmid={user_id}' + 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"关注数: {stat_info['following']}") + print(f"粉丝数: {stat_info['follower']}") + + # 获取硬币数 + coin_url = 'http://account.bilibili.com/site/getCoin' + coin_resp = requests.get(coin_url, cookies=cookies, headers=headers) + coin_data = coin_resp.json() + + if coin_data['code'] == 0: + print(f"硬币数: {coin_data['data']['money']}") + + # 获取今日投币经验 + 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 # B站每日投币多获得50经验 + print(f"今日投币经验: {today_exp}/{max_coin_exp}") + remaining_coins = (max_coin_exp - today_exp) // 10 # 每个硬币获得10经验 + if remaining_coins > 0: + print(f"今日还可投币: {remaining_coins}个") + + # 获取登录信息 + 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: + nav_info = nav_data['data'] + print(f"当前登录状态: {'已登录' if nav_info['isLogin'] else '未登录'}") + if nav_info['isLogin']: + print(f"邮箱: {nav_info['email']}") + print(f"硬币余额: {nav_info['money']}") + print(f"B币余额: {nav_info['wallet']['bcoin_balance']}") + + 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: + print(f"- {bvid}") + + # 询问是否继续投币 + 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. 退出程序") + print("="*50) + + choice = input("请选择功能 (1-6) [1]: ").strip() + if not choice: # 如果直接回车,返回默认值'1' + return '1' + if choice in ['1', '2', '3', '4', '5', '6']: + return choice + print("无效的选择,使用默认选项1") + return '1' + +def extract_bvid(url: str) -> str: + """从URL中提取BV号""" + # 匹配BV号的正则表达式 + bv_pattern = r'BV[a-zA-Z0-9]{10}' + match = re.search(bv_pattern, url) + if match: + return match.group() + return '' + +def get_video_bvids() -> None: + """获取视频BV号并保存到文件""" + bvids = [] + print("请输入B站视频URL,每行一个,输入q结束:") + while True: + url = input().strip() + if url.lower() == 'q': + break + + if not url.startswith(('https://www.bilibili.com', 'https://b23.tv')): + print("请输入有效的B站视频链接!") + continue + + bvid = extract_bvid(url) + if bvid: + bvids.append(bvid) + print(f"已添加: {bvid}") + 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 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: + print(f"- {bvid}") + + # 询问是否继续点赞 + 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 + 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 main(): + # 先尝试从文件加载cookies + cookies = load_cookies_from_file() + + # 如果没有找到有效的cookies文件,则请求用户输入 + if not cookies: + cookies = get_cookies_from_user() + if not cookies: + return + # 保存用户输入的cookies + save_cookies(cookies) + + # 获取并显示用户信息 + get_user_info(cookies) + + 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 (输入q返回主菜单):") + url = input().strip() + + if url.lower() == 'q': + break + + if not url.startswith(('https://www.bilibili.com', 'https://b23.tv')): + print("请输入有效的B站视频链接!") + continue + + download_audio(url, cookies) + + elif choice == '4': + # 获取视频BV号 + get_video_bvids() + + elif choice == '5': + # 查看用户信息 + get_user_info(cookies) + + elif choice == '6': + print("\n感谢使用!再见!") + break + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/哔哩哔哩/4.0bilibili_audio_dl.py b/哔哩哔哩/4.0bilibili_audio_dl.py new file mode 100644 index 0000000..8d8b51f --- /dev/null +++ b/哔哩哔哩/4.0bilibili_audio_dl.py @@ -0,0 +1,998 @@ +import json +import os +import subprocess +from typing import Dict +from tqdm import tqdm +import re +import requests +import time + +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: + 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: 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) + 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, cookies) + else: + # 单P视频直接下载 + download_single_audio(url, cookies) + 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: + # 获取用户基本信息 + myinfo_url = 'http://api.bilibili.com/x/space/myinfo' + myinfo_resp = requests.get(myinfo_url, cookies=cookies, headers=headers) + myinfo_data = myinfo_resp.json() + + user_id = None + if myinfo_data['code'] == 0: + user_data = myinfo_data['data'] + user_id = user_data['mid'] # 获取用户ID + print(f"用户名: {user_data['name']}") + print(f"用户ID: {user_id}") + print(f"等级: LV{user_data['level']}") + print(f"会员类型: {'大会员' if user_data['vip']['status'] == 1 else '普通用户'}") + print(f"性别: {user_data['sex']}") + print(f"签名: {user_data['sign']}") + + # 获取关注和粉丝数 + if user_id: + stat_url = f'https://api.bilibili.com/x/relation/stat?vmid={user_id}' + 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"关注数: {stat_info['following']}") + print(f"粉丝数: {stat_info['follower']}") + + # 获取硬币数 + coin_url = 'http://account.bilibili.com/site/getCoin' + coin_resp = requests.get(coin_url, cookies=cookies, headers=headers) + coin_data = coin_resp.json() + + if coin_data['code'] == 0: + print(f"硬币数: {coin_data['data']['money']}") + + # 获取今日投币经验 + 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 # B站每日投币多获得50经验 + print(f"今日投币经验: {today_exp}/{max_coin_exp}") + remaining_coins = (max_coin_exp - today_exp) // 10 # 每个硬币获得10经验 + if remaining_coins > 0: + print(f"今日还可投币: {remaining_coins}个") + + # 获取登录信息 + 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: + nav_info = nav_data['data'] + print(f"当前登录状态: {'已登录' if nav_info['isLogin'] else '未登录'}") + if nav_info['isLogin']: + print(f"邮箱: {nav_info['email']}") + print(f"硬币余额: {nav_info['money']}") + print(f"B币余额: {nav_info['wallet']['bcoin_balance']}") + + 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("="*50) + + choice = input("请选择功能 (1-8) [1]: ").strip() + if not choice: + return '1' + if choice in ['1', '2', '3', '4', '5', '6', '7', '8']: + return choice + print("无效的选择,使用默认选项1") + return '1' + +def extract_bvid(url: str) -> str: + """从URL中提取BV号""" + # 匹配BV号的正则表达式 + bv_pattern = r'BV[a-zA-Z0-9]{10}' + match = re.search(bv_pattern, url) + 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,每行一个,输入q结束:") + while True: + url = input().strip() + if url.lower() == 'q': + break + + if not url.startswith(('https://www.bilibili.com', 'https://b23.tv')): + print("请输入有效的B站视频链接!") + continue + + bvid = extract_bvid(url) + 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']}") + 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', + '-f', 'ba', + '--no-playlist', + '--no-check-certificates', + '--progress', + '--newline', + ] + + cmd.append(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 main(): + # 先尝试从文件加载cookies + cookies = load_cookies_from_file() + + # 如果没有找到有效的cookies文件,则请求用户输入 + if not cookies: + cookies = get_cookies_from_user() + if not cookies: + return + # 保存用户输入的cookies + save_cookies(cookies) + + # 获取并显示用户信息 + get_user_info(cookies) + + 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 (输入q返回主菜单):") + url = input().strip() + + if url.lower() == 'q': + break + + if not url.startswith(('https://www.bilibili.com', 'https://b23.tv')): + print("请输入有效的B站视频链接!") + continue + + download_audio(url, 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': + # 获取视频弹幕 + url = input("请输入视频URL: ").strip() + if url.startswith(('https://www.bilibili.com', 'https://b23.tv')): + try: + # 获取视频cid + bvid = extract_bvid(url) + if bvid: + # 添加headers定义 + 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, int(segment)) + else: + print("请输入有效的数字!") + else: + print(f"获取视频信息失败:{data['message']}") + else: + print("无法从URL中提取BV号!") + except Exception as e: + print(f"获取弹幕时出错: {str(e)}") + else: + print("请输入有效的B站视频链接!") + + elif choice == '8': + 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_danmaku(cid: str, segment_index: int = 1) -> 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, # 视频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) + + 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, "未知类型") + + # 转换颜色为十六进制 + color = f"#{elem.color:06x}" + + print(f"时间: {minutes:02d}:{seconds:02d}") + print(f"内容: {elem.content}") + print(f"类型: {mode_type}") + print(f"颜色: {color}") + print(f"字号: {elem.fontsize}") + print(f"发送时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(elem.ctime))}") + print("-" * 50) + + else: + print(f"获取弹幕失败: HTTP {resp.status_code}") + + except Exception as e: + print(f"获取弹幕时出错: {str(e)}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/哔哩哔哩/5.0bilibili_audio_dl.py b/哔哩哔哩/5.0bilibili_audio_dl.py new file mode 100644 index 0000000..f200d7d --- /dev/null +++ b/哔哩哔哩/5.0bilibili_audio_dl.py @@ -0,0 +1,1076 @@ +import json +import os +import subprocess +from typing import Dict +from tqdm import tqdm +import re +import requests +import time + +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: + 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: + # 获取用户基本信息 + myinfo_url = 'http://api.bilibili.com/x/space/myinfo' + myinfo_resp = requests.get(myinfo_url, cookies=cookies, headers=headers) + myinfo_data = myinfo_resp.json() + + user_id = None + if myinfo_data['code'] == 0: + user_data = myinfo_data['data'] + user_id = user_data['mid'] # 获取用户ID + print(f"用户名: {user_data['name']}") + print(f"用户ID: {user_id}") + print(f"等级: LV{user_data['level']}") + print(f"会员类型: {'大会员' if user_data['vip']['status'] == 1 else '普通用户'}") + print(f"性别: {user_data['sex']}") + print(f"签名: {user_data['sign']}") + + # 获取关注和粉丝数 + if user_id: + stat_url = f'https://api.bilibili.com/x/relation/stat?vmid={user_id}' + 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"关注数: {stat_info['following']}") + print(f"粉丝数: {stat_info['follower']}") + + # 获取硬币数 + coin_url = 'http://account.bilibili.com/site/getCoin' + coin_resp = requests.get(coin_url, cookies=cookies, headers=headers) + coin_data = coin_resp.json() + + if coin_data['code'] == 0: + print(f"硬币数: {coin_data['data']['money']}") + + # 获取今日投币经验 + 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 # B站每日投币多获得50经验 + print(f"今日投币经验: {today_exp}/{max_coin_exp}") + remaining_coins = (max_coin_exp - today_exp) // 10 # 每个硬币获得10经验 + if remaining_coins > 0: + print(f"今日还可投币: {remaining_coins}个") + + # 获取登录信息 + 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: + nav_info = nav_data['data'] + print(f"当前登录状态: {'已登录' if nav_info['isLogin'] else '未登录'}") + if nav_info['isLogin']: + print(f"邮箱: {nav_info['email']}") + print(f"硬币余额: {nav_info['money']}") + print(f"B币余额: {nav_info['wallet']['bcoin_balance']}") + + 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("="*50) + + choice = input("请选择功能 (1-8) [1]: ").strip() + if not choice: + return '1' + if choice in ['1', '2', '3', '4', '5', '6', '7', '8']: + 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']}") + 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', + '-f', 'ba', + '--no-playlist', + '--no-check-certificates', + '--progress', + '--newline', + ] + + cmd.append(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 main(): + # 先尝试从文件加载cookies + cookies = load_cookies_from_file() + + # 如果没有找到有效的cookies文件,则请求用户输入 + if not cookies: + cookies = get_cookies_from_user() + if not cookies: + return + # 保存用户输入的cookies + save_cookies(cookies) + + # 获取并显示用户信息 + get_user_info(cookies) + + 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)) + else: + print("请输入有效的数字!") + else: + print(f"获取视频信息失败:{data['message']}") + except Exception as e: + print(f"获取弹幕时出错: {str(e)}") + else: + print("无法识别BV号!") + + elif choice == '8': + 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 get_danmaku(cid: str, bvid: str, segment_index: int = 1) -> 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, # 视频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"{'-' * 50}" + ) + + # 打印到控制台 + print(danmaku_info) + + # 添加到列表 + danmaku_list.append(danmaku_info) + + # 询问是否保存到文件 + if danmaku_list: + choice = input("\n是否保存弹幕到文本文件?(y/n) [y]: ").lower() + if not choice or choice == 'y': + # 生成文件名(BV号 + 当前时间 + 弹幕后缀) + filename = f"{bvid} {time.strftime('%Y-%m-%d %H-%M-%S', time.localtime())} 弹幕.txt" + + # 保存到文件 + try: + with open(filename, 'w', encoding='utf-8') as f: + f.write('\n'.join(danmaku_list)) + print(f"弹幕已保存到文件: {filename}") + except Exception as e: + print(f"保存文件时出错: {str(e)}") + + else: + print(f"获取弹幕失败: HTTP {resp.status_code}") + + except Exception as e: + print(f"获取弹幕时出错: {str(e)}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/哔哩哔哩/6.0bilibili_audio_dl.py b/哔哩哔哩/6.0bilibili_audio_dl.py new file mode 100644 index 0000000..5789a5f --- /dev/null +++ b/哔哩哔哩/6.0bilibili_audio_dl.py @@ -0,0 +1,1356 @@ +import json +import os +import subprocess +from typing import Dict +from tqdm import tqdm +import re +import requests +import time + +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: + # 获取用户基本信息 + myinfo_url = 'http://api.bilibili.com/x/space/myinfo' + myinfo_resp = requests.get(myinfo_url, cookies=cookies, headers=headers) + myinfo_data = myinfo_resp.json() + + user_id = None + if myinfo_data['code'] == 0: + user_data = myinfo_data['data'] + user_id = user_data['mid'] # 获取用户ID + print(f"用户名: {user_data['name']}") + print(f"用户ID: {user_id}") + print(f"等级: LV{user_data['level']}") + print(f"会员类型: {'大会员' if user_data['vip']['status'] == 1 else '普通用户'}") + print(f"性别: {user_data['sex']}") + print(f"签名: {user_data['sign']}") + + # 获取关注和粉丝数 + if user_id: + stat_url = f'https://api.bilibili.com/x/relation/stat?vmid={user_id}' + 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"关注数: {stat_info['following']}") + print(f"粉丝数: {stat_info['follower']}") + + # 获取硬币数 + coin_url = 'http://account.bilibili.com/site/getCoin' + coin_resp = requests.get(coin_url, cookies=cookies, headers=headers) + coin_data = coin_resp.json() + + if coin_data['code'] == 0: + print(f"硬币数: {coin_data['data']['money']}") + + # 获取今日投币经验 + 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 # B站每日投币多获得50经验 + print(f"今日投币经验: {today_exp}/{max_coin_exp}") + remaining_coins = (max_coin_exp - today_exp) // 10 # 每个硬币获得10经验 + if remaining_coins > 0: + print(f"今日还可投币: {remaining_coins}个") + + # 获取登录信息 + 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: + nav_info = nav_data['data'] + print(f"当前登录状态: {'已登录' if nav_info['isLogin'] else '未登录'}") + if nav_info['isLogin']: + print(f"邮箱: {nav_info['email']}") + print(f"硬币余额: {nav_info['money']}") + print(f"B币余额: {nav_info['wallet']['bcoin_balance']}") + + 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("="*50) + + choice = input("请选择功能 (1-10) [1]: ").strip() + if not choice: + return '1' + if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']: + 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 = [ + "弹幕", # 存放弹幕文件 + "评论", # 存放评论文件 + "音频", # 存放下载的音频文件 + ] + + 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) + + 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)) + 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': + 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 get_danmaku(cid: str, bvid: str, segment_index: int = 1) -> 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, # 视频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"{'-' * 50}" + ) + + # 打印到控制台 + print(danmaku_info) + + # 添加到列表 + 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) + + # 生成文件名(BV号 + 当前时间 + 弹幕后缀) + 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)}") + +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)}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/哔哩哔哩/7.0bilibili_audio_dl.py b/哔哩哔哩/7.0bilibili_audio_dl.py new file mode 100644 index 0000000..13b2144 --- /dev/null +++ b/哔哩哔哩/7.0bilibili_audio_dl.py @@ -0,0 +1,2153 @@ +import json +import os +import subprocess +from typing import Dict +from tqdm import tqdm +import re +import requests +import time + +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: + # 获取用户基本信息 + myinfo_url = 'http://api.bilibili.com/x/space/myinfo' + myinfo_resp = requests.get(myinfo_url, cookies=cookies, headers=headers) + myinfo_data = myinfo_resp.json() + + user_id = None + if myinfo_data['code'] == 0: + user_data = myinfo_data['data'] + user_id = user_data['mid'] # 获取用户ID + print(f"用户名: {user_data['name']}") + print(f"用户ID: {user_id}") + print(f"等级: LV{user_data['level']}") + print(f"会员类型: {'大会员' if user_data['vip']['status'] == 1 else '普通用户'}") + print(f"性别: {user_data['sex']}") + print(f"签名: {user_data['sign']}") + + # 获取关注和粉丝数 + if user_id: + stat_url = f'https://api.bilibili.com/x/relation/stat?vmid={user_id}' + 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"关注数: {stat_info['following']}") + print(f"粉丝数: {stat_info['follower']}") + + # 获取硬币数 + coin_url = 'http://account.bilibili.com/site/getCoin' + coin_resp = requests.get(coin_url, cookies=cookies, headers=headers) + coin_data = coin_resp.json() + + if coin_data['code'] == 0: + print(f"硬币数: {coin_data['data']['money']}") + + # 获取今日投币经验 + 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 # B站每日投币多获得50验 + print(f"今日投币经验: {today_exp}/{max_coin_exp}") + remaining_coins = (max_coin_exp - today_exp) // 10 # 每个硬币获得10经验 + if remaining_coins > 0: + print(f"������还可投���: {remaining_coins}个") + + # 获取登录信息 + 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: + nav_info = nav_data['data'] + print(f"当前登录状态: {'已登录' if nav_info['isLogin'] else '未登录'}") + if nav_info['isLogin']: + print(f"邮箱: {nav_info['email']}") + print(f"硬币余额: {nav_info['money']}") + print(f"B币余额: {nav_info['wallet']['bcoin_balance']}") + + 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("="*50) + + choice = input("请选择功能 (1-11) [1]: ").strip() + if not choice: + return '1' + if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']: + 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 = [ + "弹幕", # 存放弹幕文件 + "评论", # 存放评论文件 + "音频", # 存放下载的音频文件 + ] + + 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)) + 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_id = input("请输入直播间号: ").strip() + if room_id.isdigit(): + live_room.get_room_info(room_id) + else: + print("请输入有效的直播间号!") + + elif live_choice == '2': + mid = input("请输入用户UID: ").strip() + if mid.isdigit(): + live_room.get_user_live_status(mid) + else: + print("请输入有效的用户UID!") + + elif live_choice == '3': + uid = input("请输入主播UID: ").strip() + if uid.isdigit(): + live_room.get_anchor_info(uid) + else: + print("请输入有效的主播UID!") + + elif live_choice == '4': + print("请输入直播间号,多个房间号用空格分隔(如:1 3 5):") + room_ids = input().strip().split() + if all(rid.isdigit() for rid in room_ids): + live_room.get_room_base_info(room_ids) + else: + print("请输入有效的直播间号!") + + elif live_choice == '5': + print("请输入要查询的主播UID,多个UID用空格分隔(如:672328094 2 3):") + uids = input().strip().split() + if all(uid.isdigit() for uid in uids): + live_room.get_batch_live_status(uids) + else: + print("请输入有效的主播UID!") + + elif live_choice == '6': + room_id = input("请输入直播间号: ").strip() + if room_id.isdigit(): + live_room.get_history_danmaku(room_id) + else: + print("请输入有效的直播间号!") + + elif live_choice == '7': + room_id = input("请输入直播间号: ").strip() + if room_id.isdigit(): + live_room.get_play_info(room_id) + else: + print("请输入有效的直播间号!") + + elif live_choice == '8': + room_id = input("请输入直播间号: ").strip() + if room_id.isdigit(): + live_room.get_room_anchor_info(room_id) + else: + print("请输入有效的直播间号!") + + elif live_choice == '9': + break # 返回主菜单 + + elif choice == '11': + 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 get_danmaku(cid: str, bvid: str, segment_index: int = 1) -> 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, # 视频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"{'-' * 50}" + ) + + # 打印到控制台 + print(danmaku_info) + + # 添加到列表 + 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) + + # 生成文件名(BV号 + 当前时间 + 弹幕后缀) + 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)}") + +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 = "弹幕" + 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") + + 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") + print(f"历史弹幕已保存到文件: {filepath}") + except Exception as e: + print(f"保存文件时出错: {str(e)}") + + 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 get_room_info(self, room_id: str) -> None: + """获取直播间信息""" + # 原get_live_room_info函数的内容 + + def get_user_live_status(self, mid: str) -> None: + """获取用户直播间状态""" + # 原get_user_live_status函数的内容 + + def get_anchor_info(self, uid: str) -> None: + """获取主播信息""" + # 原get_anchor_info函数的内容 + + 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_id: str) -> None: + """获取直播间历史弹幕""" + # 原get_live_history_danmaku函数的内容 + + def get_play_info(self, room_id: str) -> None: + """获取直播间播放信息""" + # 原get_room_play_info函数的内容 + + def get_room_anchor_info(self, room_id: str) -> None: + """获取直播间主播详细信息""" + # 原get_room_anchor_info函数的内容 + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/哔哩哔哩/8.0bilibili_audio_dl.py b/哔哩哔哩/8.0bilibili_audio_dl.py new file mode 100644 index 0000000..3011a55 --- /dev/null +++ b/哔哩哔哩/8.0bilibili_audio_dl.py @@ -0,0 +1,2606 @@ +import json +import os +import subprocess +from typing import Dict +from tqdm import tqdm +import re +import requests +import time + +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: + # 获取用户基本信息 + myinfo_url = 'http://api.bilibili.com/x/space/myinfo' + myinfo_resp = requests.get(myinfo_url, cookies=cookies, headers=headers) + myinfo_data = myinfo_resp.json() + + user_id = None + if myinfo_data['code'] == 0: + user_data = myinfo_data['data'] + user_id = user_data['mid'] # 获取用户ID + print(f"用户名: {user_data['name']}") + print(f"用户ID: {user_id}") + print(f"等级: LV{user_data['level']}") + print(f"会员类型: {'大会员' if user_data['vip']['status'] == 1 else '普通用户'}") + print(f"性别: {user_data['sex']}") + print(f"签名: {user_data['sign']}") + + # 获取关注和粉丝数 + if user_id: + stat_url = f'https://api.bilibili.com/x/relation/stat?vmid={user_id}' + 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"关注数: {stat_info['following']}") + print(f"粉丝数: {stat_info['follower']}") + + # 获取硬币数 + coin_url = 'http://account.bilibili.com/site/getCoin' + coin_resp = requests.get(coin_url, cookies=cookies, headers=headers) + coin_data = coin_resp.json() + + if coin_data['code'] == 0: + print(f"硬币数: {coin_data['data']['money']}") + + # 获取今日投币经验 + 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 # B站每日投币多获得50验 + print(f"今日投币经验: {today_exp}/{max_coin_exp}") + remaining_coins = (max_coin_exp - today_exp) // 10 # 每个硬币��得10经验 + if remaining_coins > 0: + print(f"还可投: {remaining_coins}个") + + # 获取登录信息 + 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: + nav_info = nav_data['data'] + print(f"当前登录状态: {'已登录' if nav_info['isLogin'] else '未登录'}") + if nav_info['isLogin']: + print(f"邮箱: {nav_info['email']}") + print(f"硬币余额: {nav_info['money']}") + print(f"B币余额: {nav_info['wallet']['bcoin_balance']}") + + 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("="*50) + + choice = input("请选择功能 (1-11) [1]: ").strip() + if not choice: + return '1' + if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']: + 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("弹幕", "直播弹幕"), # 存放直播弹幕文件 + "评论", # 存放评论文件 + "音频", # 存放下载的音频文件 + ] + + 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)) + 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': + 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 get_danmaku(cid: str, bvid: str, segment_index: int = 1) -> 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, # 视频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"{'-' * 50}" + ) + + # 打印到控制台 + print(danmaku_info) + + # 添加到列表 + 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) + + # 生成文件名(BV号 + 当前时间 + 弹幕后缀�� + 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)}") + +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": + # 持续监听 + 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: + 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 _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 '' + +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("按回车键退出...") \ No newline at end of file diff --git a/哔哩哔哩/9.0bilibili_audio_dl.py b/哔哩哔哩/9.0bilibili_audio_dl.py new file mode 100644 index 0000000..d158911 --- /dev/null +++ b/哔哩哔哩/9.0bilibili_audio_dl.py @@ -0,0 +1,2164 @@ +import json +import os +import subprocess +from typing import Dict +from tqdm import tqdm +import re +import requests +import time +import logging # 添加日志模块 +import traceback + +# 配置日志 +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(message)s', + filename='error.log' +) + +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: + # 获取用户基本信息 + myinfo_url = 'http://api.bilibili.com/x/space/myinfo' + myinfo_resp = requests.get(myinfo_url, cookies=cookies, headers=headers) + myinfo_data = myinfo_resp.json() + + user_id = None + if myinfo_data['code'] == 0: + user_data = myinfo_data['data'] + user_id = user_data['mid'] # 获取用户ID + print(f"用户名: {user_data['name']}") + print(f"用户ID: {user_id}") + print(f"等级: LV{user_data['level']}") + print(f"会员类型: {'大会员' if user_data['vip']['status'] == 1 else '普通用户'}") + print(f"性别: {user_data['sex']}") + print(f"签名: {user_data['sign']}") + + # 获取关注和粉丝数 + if user_id: + stat_url = f'https://api.bilibili.com/x/relation/stat?vmid={user_id}' + 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"关注数: {stat_info['following']}") + print(f"粉丝数: {stat_info['follower']}") + + # 获取硬币数 + coin_url = 'http://account.bilibili.com/site/getCoin' + coin_resp = requests.get(coin_url, cookies=cookies, headers=headers) + coin_data = coin_resp.json() + + if coin_data['code'] == 0: + print(f"硬币数: {coin_data['data']['money']}") + + # 获取今日投币经验 + 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 # B站��日投币��获��50验 + print(f"今日投币经验: {today_exp}/{max_coin_exp}") + remaining_coins = (max_coin_exp - today_exp) // 10 # 每个硬币��得10经验 + if remaining_coins > 0: + print(f"还可投: {remaining_coins}个") + + # 获取登��信息 + 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: + nav_info = nav_data['data'] + print(f"当前登录状态: {'已登录' if nav_info['isLogin'] else '未登录'}") + if nav_info['isLogin']: + print(f"邮箱: {nav_info['email']}") + print(f"硬币余额: {nav_info['money']}") + print(f"B币余额: {nav_info['wallet']['bcoin_balance']}") + + 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. 获取视频热评") + print("10. 直播功能") + print("11. 退出程序") + print("="*50) + + choice = input("请选择功能 (1-11) [1]: ").strip() + if not choice: + return '1' + if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']: + 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("弹幕", "直播弹幕"), # 存放直播弹幕文件 + "评论", # 存放评论文件 + "音频", # 存放下载的音频文件 + ] + + 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(): + """主程序入口""" + try: + # 创建必要的文件夹 + create_required_directories() + + # 加载cookies + cookies = load_cookies_from_file() + if not cookies: + print("错误:无法从cookies.txt加载有效的cookies") + return + + # 获取并显示用户信息 + get_user_info(cookies) + + # 创建LiveRoom实例 + live_room = LiveRoom() + + while True: + try: + 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 + + if input_text.startswith('BV'): + download_audio(input_text, cookies) + continue + + if not input_text.startswith(('https://www.bilibili.com', 'https://b23.tv')): + print("请输入有效的B站视频链接或BV号!") + continue + + download_audio(input_text, cookies) + elif choice == '4': + get_video_bvids() + elif choice == '5': + get_user_info(cookies) + elif choice == '6': + 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() + bvid = extract_bvid(input_text) + 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)) + else: + print("请输入有效的数字!") + else: + print(f"获取视频信息失败:{data['message']}") + except Exception as e: + print(f"获取弹幕时��错: {str(e)}") + else: + print("无法识别BV号!") + elif choice == '8': + # 发送视频弹幕 + bvid = input("请输入视频BV号: ").strip() + msg = input("请输入弹幕内容: ") + progress = int(input("请输入弹幕出现时间(毫秒): ")) + color = int(input("请输入弹幕颜色(十进制RGB888): ")) + fontsize = int(input("请输入弹幕字号(12-64): ")) + mode = int(input("请输入弹幕类型(1普通/4底部/5顶部/7高级/9BAS): ")) + cookies = get_cookies_from_user() + send_danmaku(bvid, msg, progress, color, fontsize, mode, cookies) + elif choice == '9': + # 获取视频热评 + input_text = input("请输入视频URL或BV号: ").strip() + bvid = extract_bvid(input_text) + 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 == '10': + # 直播功能 + while True: + live_choice = show_live_menu() + if live_choice == '9': + break + # ... 其他直播功能的处理 ... + elif choice == '11': + print("\n感谢使用!再见!") + break + + except Exception as e: + print(f"\n执行功能时出错: {str(e)}") + logging.error(f"功能执行错误: {str(e)}", exc_info=True) + input("\n按回车继续...") + + except Exception as e: + print(f"主程序运行出错: {str(e)}") + logging.error(f"主程序错误: {str(e)}", exc_info=True) + +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 get_danmaku(cid: str, bvid: str, segment_index: int = 1) -> 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, # 视频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"{'-' * 50}" + ) + + # 打印到控制台 + print(danmaku_info) + + # 添加到列表 + 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) + + # 生成文件名(BV号 + 当前时间 + 弹幕后缀�� + 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)}") + +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("="*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', + } + + def get_room_info(self, room_id: str) -> None: + """获取直播间信息""" + 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']}") + print(f"直播状态: {'直播中' if room_info['live_status'] == 1 else '未开播'}") + print("="*50) + 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_danmaku(bvid: str, msg: str, progress: int = 0, color: int = 16777215, + fontsize: int = 25, mode: int = 1, cookies: Dict = None) -> None: + """发送视频弹幕 + Args: + bvid: 视频BV号 + msg: 弹幕内容 + progress: 弹幕出现时间(毫秒) + color: 弹幕颜色(十进制RGB888) + fontsize: 弹幕字号(12-64) + mode: 弹幕类型(1普通/4底部/5顶部/7高级/9BAS) + 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', + 'Origin': 'https://www.bilibili.com' + } + + try: + # 1. 获取视频cid + 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: + print(f"获取视频信息失败: {data['message']}") + return + + cid = data['data']['cid'] + + # 2. 准备发送弹幕的参数 + csrf = cookies.get('bili_jct') + if not csrf: + print("错误: 无法获取CSRF Token") + return + + send_url = 'https://api.bilibili.com/x/v2/dm/post' + params = { + 'type': 1, + 'oid': cid, + 'msg': msg, + 'bvid': bvid, + 'progress': progress, + 'color': color, + 'fontsize': fontsize, + 'pool': 0, + 'mode': mode, + 'rnd': int(time.time() * 1000000), + 'csrf': csrf + } + + # 3. 发送弹幕 + response = requests.post(send_url, data=params, cookies=cookies, headers=headers) + result = response.json() + + if result['code'] == 0: + print(f"弹幕发送成功! dmid: {result['data']['dmid']}") + else: + error_messages = { + -101: "账号未登录", + -102: "账号被封停", + -111: "csrf校验失败", + -400: "请求错误", + -404: "无此���", + 36700: "系统升级中", + 36701: "弹幕包含被禁止的内容", + 36702: "弹幕长度大于100", + 36703: "发送频率过快", + 36704: "禁止向未审核的视频发送弹幕", + 36705: "等级不足,不能发送弹幕", + 36706: "等级不足,不能发送顶端弹幕", + 36707: "等级不足,不能发送底端弹幕", + 36708: "等级不足,不能发送彩色弹幕", + 36709: "等级不足,不能发送高级弹幕", + 36710: "权限不足,不能发送这种样式的弹幕", + 36711: "该视频禁止发送弹幕", + 36712: "level 1用户发送弹幕的最大长度为20", + 36713: "稿件未付费", + 36714: "弹幕发送时间不合法", + 36715: "当日操作数量超过上限", + 36718: "目前您不是大会员,无法使用会员权益" + } + error_msg = error_messages.get(result['code'], result['message']) + print(f"弹幕发送失败: {error_msg}") + + except Exception as e: + print(f"发送弹幕时出错: {str(e)}") + +if __name__ == "__main__": + try: + # 创建必要的文件夹 + try: + if not os.path.exists('logs'): + os.makedirs('logs') + except Exception as e: + print(f"创建日志文件夹失败: {str(e)}") + logging.error(f"创建日志文件夹失败: {str(e)}") + + # 检查cookies.txt + if not os.path.exists('cookies.txt'): + print("错误:找不到cookies.txt文件!") + print("请确保cookies.txt文件存在且包含有效的cookies信息") + logging.error("找不到cookies.txt文件") + 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文件") + logging.error(f"模块导入失败: {str(e)}") + input("\n按回车键退出...") + exit(1) + + # 运行主程序 + try: + main() + except Exception as e: + print("\n程序运行出错:") + print(f"错误类型: {type(e).__name__}") + print(f"错误信息: {str(e)}") + logging.error(f"主程序运行错误: {str(e)}", exc_info=True) + input("\n按回车继续...") + + except Exception as e: + print("\n程序出现未处理的错误:") + print(f"错误类型: {type(e).__name__}") + print(f"错误信息: {str(e)}") + logging.error(f"未处理的错误: {str(e)}", exc_info=True) + print("\n错误详细信息:") + traceback.print_exc() + input("\n按回车键退出...") + + finally: + print("\n" + "="*50) + input("按回车键退出...") \ No newline at end of file diff --git a/哔哩哔哩/bilibili/__init__.py b/哔哩哔哩/bilibili/__init__.py new file mode 100644 index 0000000..2eeb9d9 --- /dev/null +++ b/哔哩哔哩/bilibili/__init__.py @@ -0,0 +1 @@ +# 空文件 \ No newline at end of file diff --git a/哔哩哔哩/bilibili/__pycache__/__init__.cpython-313.pyc b/哔哩哔哩/bilibili/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..03d0d04 Binary files /dev/null and b/哔哩哔哩/bilibili/__pycache__/__init__.cpython-313.pyc differ diff --git a/哔哩哔哩/bilibili/community/__init__.py b/哔哩哔哩/bilibili/community/__init__.py new file mode 100644 index 0000000..2eeb9d9 --- /dev/null +++ b/哔哩哔哩/bilibili/community/__init__.py @@ -0,0 +1 @@ +# 空文件 \ No newline at end of file diff --git a/哔哩哔哩/bilibili/community/__pycache__/__init__.cpython-313.pyc b/哔哩哔哩/bilibili/community/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..592039b Binary files /dev/null and b/哔哩哔哩/bilibili/community/__pycache__/__init__.cpython-313.pyc differ diff --git a/哔哩哔哩/bilibili/community/service/__init__.py b/哔哩哔哩/bilibili/community/service/__init__.py new file mode 100644 index 0000000..2eeb9d9 --- /dev/null +++ b/哔哩哔哩/bilibili/community/service/__init__.py @@ -0,0 +1 @@ +# 空文件 \ No newline at end of file diff --git a/哔哩哔哩/bilibili/community/service/__pycache__/__init__.cpython-313.pyc b/哔哩哔哩/bilibili/community/service/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..bfad751 Binary files /dev/null and b/哔哩哔哩/bilibili/community/service/__pycache__/__init__.cpython-313.pyc differ diff --git a/哔哩哔哩/bilibili/community/service/dm/__init__.py b/哔哩哔哩/bilibili/community/service/dm/__init__.py new file mode 100644 index 0000000..2eeb9d9 --- /dev/null +++ b/哔哩哔哩/bilibili/community/service/dm/__init__.py @@ -0,0 +1 @@ +# 空文件 \ No newline at end of file diff --git a/哔哩哔哩/bilibili/community/service/dm/__pycache__/__init__.cpython-313.pyc b/哔哩哔哩/bilibili/community/service/dm/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..72fb90d Binary files /dev/null and b/哔哩哔哩/bilibili/community/service/dm/__pycache__/__init__.cpython-313.pyc differ diff --git a/哔哩哔哩/bilibili/community/service/dm/v1/__init__.py b/哔哩哔哩/bilibili/community/service/dm/v1/__init__.py new file mode 100644 index 0000000..2eeb9d9 --- /dev/null +++ b/哔哩哔哩/bilibili/community/service/dm/v1/__init__.py @@ -0,0 +1 @@ +# 空文件 \ No newline at end of file diff --git a/哔哩哔哩/bilibili/community/service/dm/v1/__pycache__/__init__.cpython-313.pyc b/哔哩哔哩/bilibili/community/service/dm/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..9cf95c6 Binary files /dev/null and b/哔哩哔哩/bilibili/community/service/dm/v1/__pycache__/__init__.cpython-313.pyc differ diff --git a/哔哩哔哩/bilibili/community/service/dm/v1/__pycache__/dm_pb2.cpython-313.pyc b/哔哩哔哩/bilibili/community/service/dm/v1/__pycache__/dm_pb2.cpython-313.pyc new file mode 100644 index 0000000..65f263e Binary files /dev/null and b/哔哩哔哩/bilibili/community/service/dm/v1/__pycache__/dm_pb2.cpython-313.pyc differ diff --git a/哔哩哔哩/bilibili/community/service/dm/v1/dm_pb2.py b/哔哩哔哩/bilibili/community/service/dm/v1/dm_pb2.py new file mode 100644 index 0000000..7c4e96a --- /dev/null +++ b/哔哩哔哩/bilibili/community/service/dm/v1/dm_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: dm.proto +# Protobuf Python Version: 5.28.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 28, + 1, + '', + 'dm.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x08\x64m.proto\x12 bilibili.community.service.dm.v1\"P\n\x10\x44mSegMobileReply\x12<\n\x05\x65lems\x18\x01 \x03(\x0b\x32-.bilibili.community.service.dm.v1.DanmakuElem\"\xe9\x01\n\x0b\x44\x61nmakuElem\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x10\n\x08progress\x18\x02 \x01(\x05\x12\x0c\n\x04mode\x18\x03 \x01(\x05\x12\x10\n\x08\x66ontsize\x18\x04 \x01(\x05\x12\r\n\x05\x63olor\x18\x05 \x01(\r\x12\x0f\n\x07midHash\x18\x06 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x07 \x01(\t\x12\r\n\x05\x63time\x18\x08 \x01(\x03\x12\x0e\n\x06weight\x18\t \x01(\x05\x12\x0e\n\x06\x61\x63tion\x18\n \x01(\t\x12\x0c\n\x04pool\x18\x0b \x01(\x05\x12\r\n\x05idStr\x18\x0c \x01(\t\x12\x0c\n\x04\x61ttr\x18\r \x01(\x05\x12\x11\n\tanimation\x18\x0e \x01(\tb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dm_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_DMSEGMOBILEREPLY']._serialized_start=46 + _globals['_DMSEGMOBILEREPLY']._serialized_end=126 + _globals['_DANMAKUELEM']._serialized_start=129 + _globals['_DANMAKUELEM']._serialized_end=362 +# @@protoc_insertion_point(module_scope) diff --git a/哔哩哔哩/bvid.txt b/哔哩哔哩/bvid.txt new file mode 100644 index 0000000..e69de29 diff --git a/哔哩哔哩/cookies.txt b/哔哩哔哩/cookies.txt new file mode 100644 index 0000000..e69de29 diff --git a/哔哩哔哩/dm.proto b/哔哩哔哩/dm.proto new file mode 100644 index 0000000..258184c --- /dev/null +++ b/哔哩哔哩/dm.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package bilibili.community.service.dm.v1; + +// 弹幕数据包 +message DmSegMobileReply { + repeated DanmakuElem elems = 1; +} + +// 弹幕条目 +message DanmakuElem { + int64 id = 1; // 弹幕dmid + int32 progress = 2; // 弹幕出现时间(毫秒) + int32 mode = 3; // 弹幕类型 + int32 fontsize = 4; // 字号 + uint32 color = 5; // 颜色 + string midHash = 6; // 发送者hash + string content = 7; // 弹幕内容 + int64 ctime = 8; // 发送时间 + int32 weight = 9; // 权重 + string action = 10; // 动作 + int32 pool = 11; // 弹幕池 + string idStr = 12; // 弹幕dmid字符串 + int32 attr = 13; // 属性 + string animation = 14; // 动画 +} \ No newline at end of file diff --git a/哔哩哔哩/dm_pb2.py b/哔哩哔哩/dm_pb2.py new file mode 100644 index 0000000..7c4e96a --- /dev/null +++ b/哔哩哔哩/dm_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: dm.proto +# Protobuf Python Version: 5.28.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 28, + 1, + '', + 'dm.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x08\x64m.proto\x12 bilibili.community.service.dm.v1\"P\n\x10\x44mSegMobileReply\x12<\n\x05\x65lems\x18\x01 \x03(\x0b\x32-.bilibili.community.service.dm.v1.DanmakuElem\"\xe9\x01\n\x0b\x44\x61nmakuElem\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x10\n\x08progress\x18\x02 \x01(\x05\x12\x0c\n\x04mode\x18\x03 \x01(\x05\x12\x10\n\x08\x66ontsize\x18\x04 \x01(\x05\x12\r\n\x05\x63olor\x18\x05 \x01(\r\x12\x0f\n\x07midHash\x18\x06 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x07 \x01(\t\x12\r\n\x05\x63time\x18\x08 \x01(\x03\x12\x0e\n\x06weight\x18\t \x01(\x05\x12\x0e\n\x06\x61\x63tion\x18\n \x01(\t\x12\x0c\n\x04pool\x18\x0b \x01(\x05\x12\r\n\x05idStr\x18\x0c \x01(\t\x12\x0c\n\x04\x61ttr\x18\r \x01(\x05\x12\x11\n\tanimation\x18\x0e \x01(\tb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dm_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_DMSEGMOBILEREPLY']._serialized_start=46 + _globals['_DMSEGMOBILEREPLY']._serialized_end=126 + _globals['_DANMAKUELEM']._serialized_start=129 + _globals['_DANMAKUELEM']._serialized_end=362 +# @@protoc_insertion_point(module_scope)