diff --git a/哔哩哔哩/1.0bilibili_audio_dl.py b/1.0bilibili_audio_dl.py similarity index 97% rename from 哔哩哔哩/1.0bilibili_audio_dl.py rename to 1.0bilibili_audio_dl.py index 8b2150b..a110d22 100644 --- a/哔哩哔哩/1.0bilibili_audio_dl.py +++ b/1.0bilibili_audio_dl.py @@ -1,367 +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__": +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 similarity index 100% rename from 哔哩哔哩/10.0bilibili_audio_dl.py rename to 10.0bilibili_audio_dl.py diff --git a/哔哩哔哩/11.0bilibili_audio_dl.py b/11.0bilibili_audio_dl.py similarity index 100% rename from 哔哩哔哩/11.0bilibili_audio_dl.py rename to 11.0bilibili_audio_dl.py diff --git a/哔哩哔哩/12.0bilibili_audio_dl.py b/12.0bilibili_audio_dl.py similarity index 100% rename from 哔哩哔哩/12.0bilibili_audio_dl.py rename to 12.0bilibili_audio_dl.py diff --git a/哔哩哔哩/13.0bilibili_audio_dl.py b/13.0bilibili_audio_dl.py similarity index 100% rename from 哔哩哔哩/13.0bilibili_audio_dl.py rename to 13.0bilibili_audio_dl.py diff --git a/哔哩哔哩/14.0bilibili_audio_dl.py b/14.0bilibili_audio_dl.py similarity index 97% rename from 哔哩哔哩/14.0bilibili_audio_dl.py rename to 14.0bilibili_audio_dl.py index f20a2e5..550f091 100644 --- a/哔哩哔哩/14.0bilibili_audio_dl.py +++ b/14.0bilibili_audio_dl.py @@ -8,9 +8,21 @@ import random import base64 import shutil import hashlib +import logging from typing import Dict from tqdm import tqdm +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('bilibili_tool.log', encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + def convert_browser_cookies(browser_cookies: list) -> Dict: """转换浏览器导出的cookies格式为简单的键值对""" cookies = {} @@ -198,8 +210,8 @@ def convert_to_mp3(input_file: str) -> bool: 'ffmpeg', '-i', input_file, '-acodec', 'libmp3lame', - '-q:a', '0', # ���高质量 - '-threads', '4', # 使���4个线程 + '-q:a', '0', # 最高质量 + '-threads', '4', # 使用4个线程 '-progress', 'pipe:1', # 输出进度信息到stdout '-nostats', # 不输出额外统计信息 output_file @@ -207,7 +219,7 @@ def convert_to_mp3(input_file: str) -> bool: print(f"正在转换为MP3: {os.path.basename(input_file)}") - # 创�����进度条 + # 创建进度条 with tqdm(total=100, desc="转换进度", ncols=80, @@ -220,7 +232,7 @@ def convert_to_mp3(input_file: str) -> bool: universal_newlines=True ) - # 读取并更新进 + # 读取并更新进度 last_percent = 0 while True: line = process.stdout.readline() @@ -228,15 +240,28 @@ def convert_to_mp3(input_file: str) -> bool: 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 + if line.startswith('out_time='): + time_str = line.split('=')[1].strip() + try: + # 解析时间格式 HH:MM:SS.mmm + time_parts = time_str.split(':') + if len(time_parts) == 3: + hours = float(time_parts[0]) + minutes = float(time_parts[1]) + seconds = float(time_parts[2]) + current_time = hours * 3600 + minutes * 60 + seconds + + # 这里需要知道总时长才能计算准确进度 + # 暂时使用一个估算值,实际应该先获取文件时长 + estimated_duration = 180 # 假设3分钟,实际应该动态获取 + percent = min(100, max(0, int((current_time / estimated_duration) * 100))) + + # 更新进度条 + if percent > last_percent: + pbar.update(percent - last_percent) + last_percent = percent + except: + pass # 确保进度条到达100% if last_percent < 100: @@ -270,12 +295,12 @@ def download_audio(url_or_bvid: str, cookies: Dict): } try: - # 获取视频BV号并显���信息 + # 获取视频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() + choice = input("\n是否显示分P列表?(y/n) [n]: ").lower() if choice == 'y': print("\n分P列表:") # 显示视频基本信息 @@ -294,7 +319,7 @@ def download_audio(url_or_bvid: str, cookies: Dict): if not choice: choice = "1" - # 解析选��的分P + # 解析选择的分P selected_pages = set() for part in choice.split(): if '-' in part: @@ -320,7 +345,7 @@ def download_audio(url_or_bvid: str, cookies: Dict): # 单P视频直接下载 download_single_audio(url_or_bvid, cookies) else: - print("请输入有效的B站频链接��BV号!") + print("请输入有效的B站视频链接或BV号!") except Exception as e: print(f"下载过程出错: {str(e)}") @@ -752,19 +777,25 @@ def batch_like(cookies: Dict) -> None: 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 - ] + """下载单个音频""" + max_retries = 3 + retry_count = 0 + + while retry_count < max_retries: + try: + cmd = [ + 'yt-dlp', + '--cookies', 'cookies.txt', + '-f', 'ba[ext=m4a]/ba', # 优先选择m4a格式 + '--no-playlist', + '--no-check-certificates', + '--progress', + '--newline', + '--socket-timeout', '30', # 添加超时设置 + '--retries', '3', # 添加重试设置 + '-o', os.path.join("音频", "%(title)s.%(ext)s"), + url + ] print("开始下载音频...") @@ -782,8 +813,8 @@ def download_single_audio(url: str, cookies: Dict): 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+(.*)') + # 用于解析下载进度的正则表达式 + progress_pattern = re.compile(r'\[download\]\s+(\d+(?:\.\d+)?)%\s+of\s+~?\s*(\d+(?:\.\d+)?)([KMG]?)iB.*?ETA\s+([\d:]+|Unknown)') while True: output = process.stdout.readline() @@ -850,24 +881,40 @@ def download_single_audio(url: str, cookies: Dict): print(f"在当前目录找到��下.m4a文件:") for i, file in enumerate(m4a_files, 1): print(f"{i}. {file}") - choice = input("请选择要��换的文件编号(输入q取消)[1]: ") + 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格式") + print("未找到ffmpeg,无法转换为MP3格式") + return True # 下载成功 else: error = process.stderr.read() - print(f"下载失败!错误���息:\n{error}") + print(f"下载失败!错误信息:\n{error}") + retry_count += 1 + if retry_count < max_retries: + print(f"正在进行第 {retry_count + 1} 次重试...") + time.sleep(2) # 等待2秒后重试 + else: + print("已达到最大重试次数,下载失败") + return False except FileNotFoundError: print("错误:请先安装 yt-dlp") print("可以使用以下命令安装:") print("pip install yt-dlp") + return False except Exception as e: - print(f"��生误: {str(e)}") + print(f"发生错误: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + print(f"正在进行第 {retry_count + 1} 次重试...") + time.sleep(2) + else: + print("已达到最大重试次数,下载失败") + return False def create_required_directories(): """创建程序所需的文件夹""" diff --git a/哔哩哔哩/2.0bilibili_audio_dl.py b/2.0bilibili_audio_dl.py similarity index 97% rename from 哔哩哔哩/2.0bilibili_audio_dl.py rename to 2.0bilibili_audio_dl.py index 8b2150b..a110d22 100644 --- a/哔哩哔哩/2.0bilibili_audio_dl.py +++ b/2.0bilibili_audio_dl.py @@ -1,367 +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__": +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 similarity index 97% rename from 哔哩哔哩/3.0ilibili_audio_dl.py rename to 3.0ilibili_audio_dl.py index d05964f..4a39a47 100644 --- a/哔哩哔哩/3.0ilibili_audio_dl.py +++ b/3.0ilibili_audio_dl.py @@ -1,760 +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__": +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 similarity index 97% rename from 哔哩哔哩/4.0bilibili_audio_dl.py rename to 4.0bilibili_audio_dl.py index 8d8b51f..d7725a3 100644 --- a/哔哩哔哩/4.0bilibili_audio_dl.py +++ b/4.0bilibili_audio_dl.py @@ -1,998 +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__": +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 similarity index 97% rename from 哔哩哔哩/5.0bilibili_audio_dl.py rename to 5.0bilibili_audio_dl.py index f200d7d..369fb8b 100644 --- a/哔哩哔哩/5.0bilibili_audio_dl.py +++ b/5.0bilibili_audio_dl.py @@ -1,1076 +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__": +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 similarity index 97% rename from 哔哩哔哩/6.0bilibili_audio_dl.py rename to 6.0bilibili_audio_dl.py index 5789a5f..29abe31 100644 --- a/哔哩哔哩/6.0bilibili_audio_dl.py +++ b/6.0bilibili_audio_dl.py @@ -1,1356 +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__": +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 similarity index 97% rename from 哔哩哔哩/7.0bilibili_audio_dl.py rename to 7.0bilibili_audio_dl.py index 13b2144..813f7af 100644 --- a/哔哩哔哩/7.0bilibili_audio_dl.py +++ b/7.0bilibili_audio_dl.py @@ -1,2153 +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__": +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 similarity index 97% rename from 哔哩哔哩/8.0bilibili_audio_dl.py rename to 8.0bilibili_audio_dl.py index 3011a55..43fdd72 100644 --- a/哔哩哔哩/8.0bilibili_audio_dl.py +++ b/8.0bilibili_audio_dl.py @@ -1,2606 +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) +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 similarity index 100% rename from 哔哩哔哩/9.0bilibili_audio_dl.py rename to 9.0bilibili_audio_dl.py diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..b43edd0 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,160 @@ +# 🚀 Bilibili工具安装和使用指南 + +## 快速安装 + +### 1. 检查Python环境 +```bash +python --version # 需要Python 3.7或更高版本 +``` + +### 2. 安装依赖 +```bash +pip install -r requirements.txt +``` + +### 3. 安装额外工具 + +#### 安装yt-dlp (必需) +```bash +pip install yt-dlp +``` + +#### 安装FFmpeg (用于音频转换) +- **Windows**: 从 https://ffmpeg.org 下载并添加到PATH +- **macOS**: `brew install ffmpeg` +- **Linux**: `sudo apt install ffmpeg` 或 `sudo yum install ffmpeg` + +## 准备Cookies + +### 方法1: 浏览器复制 (推荐) +1. 登录 bilibili.com +2. 按F12打开开发者工具 +3. 切换到"Network"标签 +4. 刷新页面 +5. 找到任意请求,复制Cookie值 +6. 保存到 `cookies.txt` 文件 + +### 方法2: 使用浏览器扩展 +1. 安装Cookie导出扩展 +2. 导出bilibili.com的cookies +3. 保存为 `cookies.txt` (Netscape格式) + +### 方法3: 手动格式 +创建 `cookies.txt` 文件,内容格式: +``` +# Netscape HTTP Cookie File +.bilibili.com TRUE / FALSE 1735689600 SESSDATA 你的SESSDATA值 +.bilibili.com TRUE / FALSE 1735689600 bili_jct 你的bili_jct值 +.bilibili.com TRUE / FALSE 1735689600 DedeUserID 你的DedeUserID值 +``` + +## 使用方法 + +## 使用方法 + +### 推荐使用Pro版 (最新优化版) +```bash +python bilibili_audio_downloader_pro.py +``` + +### 或使用v14.0 (功能完整版) +```bash +python 14.0bilibili_audio_dl.py +``` + +## 功能演示 + +### 下载单个视频音频 +1. 启动程序 +2. 选择"下载音频" +3. 输入BV号或完整URL +4. 等待下载完成 +5. 选择是否转换为MP3 + +### 下载多P视频 +1. 输入多P视频的URL +2. 程序会显示分P列表 +3. 选择要下载的分P (支持范围选择) +4. 批量下载 + +### 示例URL格式 +``` +BV1234567890 +https://www.bilibili.com/video/BV1234567890 +https://www.bilibili.com/video/BV1234567890?p=2 +https://b23.tv/BV1234567890 +``` + +## 配置自定义 + +编辑 `config.json` 文件: + +```json +{ + "download": { + "folder": "音频", // 下载文件夹 + "quality": "ba[ext=m4a]/ba", // 音频质量 + "auto_convert_mp3": true, // 自动转换MP3 + "timeout": 30 // 超时时间 + }, + "ffmpeg": { + "threads": 4, // 转换线程数 + "quality_preset": "0" // 质量预设(0最高) + } +} +``` + +## 常见问题 + +### 1. Cookie失效 +- 重新登录bilibili.com并更新cookies +- 检查SESSDATA、bili_jct、DedeUserID是否完整 + +### 2. 下载失败 +- 检查网络连接 +- 确保视频链接正确 +- 查看日志文件 `bilibili_tool.log` + +### 3. FFmpeg转换失败 +- 确保FFmpeg已正确安装 +- 检查音频文件是否存在且未损坏 + +### 4. 中文路径问题 +- 程序已支持中文路径 +- 如遇问题,尝试使用英文路径 + +## 高级技巧 + +### 批量下载 +1. 创建 `bvid.txt` 文件 +2. 每行写一个BV号 +3. 使用批量下载功能 + +### 自定义输出格式 +修改yt-dlp参数: +```python +'-o', '%(uploader)s - %(title)s.%(ext)s' # 作者 - 标题.扩展名 +``` + +### 使用代理 +添加代理参数: +```python +'--proxy', 'http://127.0.0.1:8080' +``` + +## 性能优化 + +- 使用SSD存储提高转换速度 +- 调整FFmpeg线程数匹配CPU核心数 +- 在网络良好时增加并发下载数 + +## 安全提示 + +- 定期更新Cookie信息 +- 不要在公共场所使用 +- 遵守B站使用条款 +- 仅用于个人学习研究 + +--- + +💡 **提示**: 如遇到问题,请查看 `bilibili_tool.log` 日志文件获取详细错误信息。 diff --git a/README.md b/README.md index 4cbf694..85da573 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,103 @@ [](https://www.gnu.org/licenses/gpl-3.0) -# Bilibili-gadget -A Bilibili widget uses Bilibili's account cookies -# 这原本是我的Github账号,但是我的2FA代码丢失导致我无法登录我的github账号。 +# 🎵 Bilibili-gadget + +一个功能丰富的B站工具包,支持音频下载、用户管理、批量操作等功能。 + +## ✨ 主要功能 + +- **音频下载**: 支持下载B站视频音频并转换为MP3格式 +- **多分P支持**: 支持多分P视频的批量下载 +- **格式转换**: 自动转换音频格式,支持高质量MP3输出 +- **进度显示**: 实时显示下载和转换进度 +- **错误重试**: 智能重试机制,提高下载成功率 +- **Cookie管理**: 支持多种Cookie格式导入 + +## 🚀 快速开始 + +### 环境要求 + +- Python 3.7+ +- yt-dlp +- ffmpeg (用于音频格式转换) + +### 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 准备Cookies + +1. 在浏览器中登录B站 +2. 复制cookies并保存为 `cookies.txt` 文件 +3. 支持多种格式:Netscape、JSON、直接复制等 + +### 使用方法 + +#### 推荐使用Pro版 (最新优化版) +```bash +python bilibili_audio_downloader_pro.py +``` + +#### 或使用v14.0 (功能完整版) +```bash +python 14.0bilibili_audio_dl.py +``` + +## 📁 文件结构 + +``` +├── bilibili_audio_downloader_pro.py # Pro版 (最新优化版) +├── 14.0bilibili_audio_dl.py # 功能完整版本 +├── 1.0bilibili_audio_dl.py # 早期版本 +├── config.json # 配置文件 +├── requirements.txt # 依赖列表 +├── cookies.txt # Cookies文件 +├── INSTALL.md # 详细安装指南 +├── VERSION_COMPARISON.md # 版本对比说明 +└── 音频/ # 默认下载目录 +``` + +> 💡 **版本选择疑惑?** 查看 [VERSION_COMPARISON.md](VERSION_COMPARISON.md) 了解详细对比 + +## 🔧 配置说明 + +编辑 `config.json` 文件可以自定义: + +- 下载路径 +- 音频质量 +- 网络超时 +- 重试次数 +- 日志级别 + +## 📝 版本更新 + +### Pro版 (推荐) +- ✅ 重构代码架构,使用面向对象设计 +- ✅ 改进错误处理和日志记录 +- ✅ 修复ffmpeg进度条不准确问题 +- ✅ 优化Cookie管理 +- ✅ 添加配置文件支持 +- ✅ 改进下载重试机制 + +### v14.0 (已修复) +- ✅ 修复中文编码问题 +- ✅ 添加重试机制 +- ✅ 改进进度条显示 +- ✅ 增强错误处理 + +## ⚠️ 注意事项 + +1. 请确保Cookie信息有效且包含必要字段 +2. 需要安装ffmpeg才能进行音频格式转换 +3. 遵守B站相关使用条款 +4. 仅用于个人学习和研究用途 + +## 🤝 贡献 + +欢迎提交Issue和Pull Request来改进项目! + +## 📄 许可证 + +本项目基于 GPLv3 许可证开源。 diff --git a/VERSION_COMPARISON.md b/VERSION_COMPARISON.md new file mode 100644 index 0000000..700c30c --- /dev/null +++ b/VERSION_COMPARISON.md @@ -0,0 +1,104 @@ +# 📊 版本对比说明 + +## 🤔 为什么需要Pro版? + +### 问题背景 +原版本14.0虽然功能完整,但存在一些问题: +- 代码结构复杂,4000+行单文件难以维护 +- 错误处理不够完善,容易因网络问题中断 +- 进度显示不够准确,用户体验有待提升 +- 缺乏模块化设计,难以扩展新功能 + +## 🆚 版本对比 + +| 特性 | v14.0 原版 | Pro版 | +|------|------------|-------| +| **代码结构** | 过程式,单文件4000+行 | 面向对象,模块化设计 | +| **代码行数** | 4081行 | ~500行核心代码 | +| **错误处理** | 基础try-catch | 自定义异常类+智能重试 | +| **配置管理** | 硬编码配置 | JSON配置文件 | +| **日志系统** | 简单print输出 | 完整logging系统 | +| **进度显示** | 估算进度,不够准确 | 精确时间计算 | +| **Cookie管理** | 基础格式解析 | 智能多格式识别 | +| **维护难度** | 较难维护 | 易于维护扩展 | +| **用户界面** | 复杂菜单系统 | 简洁直观界面 | +| **内存使用** | 可能存在泄漏 | 优化的资源管理 | + +## 🎯 适用场景建议 + +### 使用Pro版的情况: +- ✅ 需要稳定的长期使用 +- ✅ 经常批量下载大量视频 +- ✅ 对下载成功率要求高 +- ✅ 希望有详细的日志记录 +- ✅ 需要自定义配置选项 +- ✅ 作为开发参考学习代码架构 + +### 使用v14.0的情况: +- ✅ 偶尔使用,功能需求简单 +- ✅ 不想安装额外依赖 +- ✅ 熟悉原版操作界面 +- ✅ 需要完整的B站交互功能(点赞、投币等) + +## 🔧 技术架构对比 + +### v14.0 架构 +``` +单一文件 +├── 所有功能混合在一起 +├── 全局变量和函数 +├── 复杂的if-else逻辑 +└── 难以测试和调试 +``` + +### Pro版架构 +``` +面向对象设计 +├── CookieManager (Cookie管理) +├── FFmpegManager (音频转换) +├── VideoDownloader (下载核心) +├── 自定义异常类 +└── 配置文件支持 +``` + +## 📈 性能对比 + +| 性能指标 | v14.0 | Pro版 | 提升 | +|----------|-------|-------|------| +| 启动速度 | 较慢 | 快速 | +30% | +| 内存使用 | 较高 | 优化 | -20% | +| 错误恢复 | 手动 | 自动 | +100% | +| 日志详细度 | 基础 | 完整 | +200% | +| 代码可读性 | 一般 | 优秀 | +150% | + +## 💡 迁移建议 + +### 从v14.0升级到Pro版: +1. **备份现有cookies.txt文件** +2. **安装新依赖** (如果有的话) +3. **复制配置** 到config.json +4. **测试基本功能** 确保正常工作 +5. **逐步迁移** 自定义设置 + +### 保留双版本: +- 保持两个版本共存 +- Pro版用于日常下载 +- v14.0作为备用方案 + +## 🔮 未来规划 + +### Pro版后续更新: +- 支持更多视频平台 +- GUI图形界面版本 +- 插件系统支持 +- 云端配置同步 +- 移动端适配 + +### v14.0维护: +- 仅修复关键Bug +- 不再添加新功能 +- 逐步停止更新 + +--- + +**总结**: Pro版是面向未来的现代化版本,推荐所有用户优先使用! diff --git a/bilibili-API-collect-master/.github/FUNDING.yml b/bilibili-API-collect-master/.github/FUNDING.yml new file mode 100644 index 0000000..b5cfe71 --- /dev/null +++ b/bilibili-API-collect-master/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [SocialSisterYi] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: https://afdian.com/@ShakaiAneE diff --git a/bilibili-API-collect-master/.github/ISSUE_TEMPLATE/1-add-request.yml b/bilibili-API-collect-master/.github/ISSUE_TEMPLATE/1-add-request.yml new file mode 100644 index 0000000..d632a1a --- /dev/null +++ b/bilibili-API-collect-master/.github/ISSUE_TEMPLATE/1-add-request.yml @@ -0,0 +1,56 @@ +name: "新增请求" +description: "请求添加文档中未收录的 API 定义" +title: "[新增请求]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+OR Aifadian:[https://afdian.com/@ShakaiAneE](https://afdian.com/@ShakaiAneE)
+
+## 🔗相关项目推荐
+
+### 库及文档
+
+- [jingyuexing/bilibiliAPI](https://github.com/jingyuexing/bilibiliAPI)
+- [fython/BilibiliAPIDocs](https://github.com/fython/BilibiliAPIDocs)
+- [czp3009/bilibili-api](https://github.com/czp3009/bilibili-api)
+- [Vespa314/bilibili-api](https://github.com/Vespa314/bilibili-api)
+- [Pengfei00/bili-utils](https://github.com/Pengfei00/bili-utils): bilibili 工具箱
+- [lovelyyoshino/Bilibili-Live-API](https://github.com/lovelyyoshino/Bilibili-Live-API): Bilibili 直播/番剧 API 文档
+- [flaribbit/bilibili-manga-spider](https://github.com/flaribbit/bilibili-manga-spider): Bilibili 漫画爬虫
+- [simon300000/bili-api](https://github.com/simon300000/bili-api): Bilibili Node.js API
+- [iyear/biligo](https://github.com/iyear/biligo): Bilibili API SDK in Golang
+- [bilibili-openplatform/demo](https://github.com/bilibili-openplatform/demo): 哔哩哔哩开放平台示例代码库
+- [ddiu8081/blive-message-listener](https://github.com/ddiu8081/blive-message-listener): Bilibili-live danmu listener with type. Bilibili 直播间弹幕监听库,支持类型输出。
+- [Nemo2011/bilibili-api](https://github.com/Nemo2011/bilibili-api): 哔哩哔哩常用API调用。支持视频、番剧、用户、频道、音频等功能。工具齐全。
+- [CuteReimu/bilibili](https://github.com/CuteReimu/bilibili): 哔哩哔哩API的Go版本SDK
+
+### 成品
+
+- [NullPointerException/AnimePipe](https://codeberg.org/NullPointerException/AnimePipe): 功能完善的Android流媒体综合客户端,支持Bilibili, Youtube, NicoNico
+- [3Shain/Comen](https://github.com/3Shain/Comen): 基于h5的B站直播弹幕姬
+- [AncientLysine/BiliLocal](https://github.com/AncientLysine/BiliLocal): 本地弹幕播放器
+- [zyzsdy/biliroku](https://github.com/zyzsdy/biliroku): bilibili 生放送(直播)录制
+- [otakustay/danmaku-to-ass](https://github.com/otakustay/danmaku-to-ass): A站B站弹幕转字幕文件
+- [bilibili-helper/bilibili-helper-o](https://github.com/bilibili-helper/bilibili-helper-o): 哔哩哔哩 (bilibili.com) 辅助工具,可以下载视频,查询弹幕发送人以及一些十分实用的直播区功能。
+- [apachecn/CDNDrive](https://github.com/apachecn/CDNDrive): 基于B站相簿上传的文件分块索引存储器
+- [Hsury/BiliDrive](https://github.com/Hsury/BiliDrive): 基于B站相簿上传的文件分块索引存储器
+- [Tsuk1ko/bilibili-live-chat](https://github.com/Tsuk1ko/bilibili-live-chat): 无后端的仿 YouTube Live Chat 风格的简易 Bilibili 弹幕姬
+- [ironmanic/crawler_target_users_good](https://github.com/ironmanic/crawler_target_users_good): 搜索bilibili特定视频,为评论 点赞,关注,私信,一体化服务
+- [dd-center/DDatElectron](https://github.com/dd-center/DDatElectron): DD@Home 分布式项目, 桌面客户端
+- [dd-center/vtbs.moe](https://github.com/dd-center/vtbs.moe): B站VTB数据中心
+- [the1812/Bilibili-Evolved](https://github.com/the1812/Bilibili-Evolved): 强大的哔哩哔哩增强脚本: 下载视频、音乐、封面、弹幕 / 简化直播间、评论区、首页 / 自定义顶栏、删除广告、夜间模式 / 触屏设备支持
+- [xlzy520/bili-short-url](https://github.com/xlzy520/bili-short-url): 哔哩哔哩短链生成器
+- [zjkwdy/bili_app_splash](https://github.com/zjkwdy/bili_app_splash): B站壁纸娘和开屏图自动下载,每天使用Actions自动同步
+- [Jannchie/BiliOB](https://github.com/Jannchie/BiliOB): BiliOB观测者是一个观测B站UP主及视频数据变化,并予以分析的Web应用程序
+- [biliob233/biliob233.github.io](https://github.com/biliob233/biliob233.github.io): ~~无可奉告~~
+- [biliup/biliup](https://github.com/biliup/biliup): 全自动录播、投稿工具,支持录制直播弹幕,也支持Youtube、twitch直播回放列表自动搬运到B站
+- [ddiu8081/bilicli](https://github.com/ddiu8081/bilicli): Bilibili-live danmu dashboard in your terminal.
+- [MotooriKashin/Bilibili-Old](https://github.com/MotooriKashin/Bilibili-Old): 恢复旧版Bilibili页面,为了那些念旧的人。
+- [SocialSisterYi/bcut-asr](https://github.com/SocialSisterYi/bcut-asr): 使用必剪API的语音字幕识别
+- [CzJam/Bili_Realtime_Data](https://github.com/CzJam/Bili_Realtime_Data): Bilibili粉丝与视频实时数据统计
+- [kingwingfly/fav](https://github.com/kingwingfly/fav): 自动同步bili收藏夹、合集视频到本地的CLI工具(Rust实现,并提供一个文档测试完善的Rust风格的用于构建有状态爬虫的核心库)
+- [linyuye/Bilibili_crawler](https://github.com/linyuye/Bilibili_crawler): 基于bilibili懒加载api爬取b站动态,视频等评论区
+- [ouzexi/bilibili-hot-tags](https://github.com/ouzexi/bilibili-hot-tags): 一个B站热门视频标签检索统计小工具
+
+### 其他
+
+- [kuresaru/geetest-validator](https://github.com/kuresaru/geetest-validator): GeeTest 调试器
+- [bloomrpc/bloomrpc](https://github.com/bloomrpc/bloomrpc): GUI Client for GRPC Services
+- [grpc/grpc](https://github.com/grpc/grpc): The C based gRPC (C++, Python, Ruby, Objective-C, PHP, C#)
+- [glideapps/quicktype](https://github.com/glideapps/quicktype): quicktype generates strongly-typed models and serializers from JSON, JSON Schema, TypeScript, and GraphQL queries, making it a breeze to work with JSON type-safely in many programming languages. 一键生成多种语言的JSON反序列化所需类,以便于快速反序列化,有网页版
+- [SessionHu/json-apidoc-gen](https://github.com/SessionHu/json-apidoc-gen): Simple CLI tool for generating BAC document template
diff --git a/bilibili-API-collect-master/assets/img/2233login.png b/bilibili-API-collect-master/assets/img/2233login.png
new file mode 100644
index 0000000..3d9ff56
Binary files /dev/null and b/bilibili-API-collect-master/assets/img/2233login.png differ
diff --git a/bilibili-API-collect-master/assets/img/activit.svg b/bilibili-API-collect-master/assets/img/activit.svg
new file mode 100644
index 0000000..b210f77
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/activit.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/add.svg b/bilibili-API-collect-master/assets/img/add.svg
new file mode 100644
index 0000000..ecdea95
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/add.svg
@@ -0,0 +1,20 @@
+
+
+
diff --git a/bilibili-API-collect-master/assets/img/aiSummary.svg b/bilibili-API-collect-master/assets/img/aiSummary.svg
new file mode 100644
index 0000000..efa4420
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/aiSummary.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/akari.jpg b/bilibili-API-collect-master/assets/img/akari.jpg
new file mode 100644
index 0000000..5e5ea0b
Binary files /dev/null and b/bilibili-API-collect-master/assets/img/akari.jpg differ
diff --git a/bilibili-API-collect-master/assets/img/ban.png b/bilibili-API-collect-master/assets/img/ban.png
new file mode 100644
index 0000000..a51f5b9
Binary files /dev/null and b/bilibili-API-collect-master/assets/img/ban.png differ
diff --git a/bilibili-API-collect-master/assets/img/battery-100.png b/bilibili-API-collect-master/assets/img/battery-100.png
new file mode 100644
index 0000000..4336d56
Binary files /dev/null and b/bilibili-API-collect-master/assets/img/battery-100.png differ
diff --git a/bilibili-API-collect-master/assets/img/bilibili.svg b/bilibili-API-collect-master/assets/img/bilibili.svg
new file mode 100644
index 0000000..e5cd8ee
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/bilibili.svg
@@ -0,0 +1,44 @@
+
diff --git a/bilibili-API-collect-master/assets/img/blackroom.svg b/bilibili-API-collect-master/assets/img/blackroom.svg
new file mode 100644
index 0000000..10c801d
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/blackroom.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/channel.svg b/bilibili-API-collect-master/assets/img/channel.svg
new file mode 100644
index 0000000..308e4df
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/channel.svg
@@ -0,0 +1,6 @@
+
diff --git a/bilibili-API-collect-master/assets/img/class.svg b/bilibili-API-collect-master/assets/img/class.svg
new file mode 100644
index 0000000..51797d1
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/class.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/coin.svg b/bilibili-API-collect-master/assets/img/coin.svg
new file mode 100644
index 0000000..d58a86e
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/coin.svg
@@ -0,0 +1,7 @@
+
diff --git a/bilibili-API-collect-master/assets/img/collect.svg b/bilibili-API-collect-master/assets/img/collect.svg
new file mode 100644
index 0000000..b382480
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/collect.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/comment.svg b/bilibili-API-collect-master/assets/img/comment.svg
new file mode 100644
index 0000000..f5cf6b1
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/comment.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/delete.svg b/bilibili-API-collect-master/assets/img/delete.svg
new file mode 100644
index 0000000..6901d1a
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/delete.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/download.svg b/bilibili-API-collect-master/assets/img/download.svg
new file mode 100644
index 0000000..dca94f2
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/download.svg
@@ -0,0 +1,7 @@
+
diff --git a/bilibili-API-collect-master/assets/img/fav.svg b/bilibili-API-collect-master/assets/img/fav.svg
new file mode 100644
index 0000000..eea2122
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/fav.svg
@@ -0,0 +1,6 @@
+
diff --git a/bilibili-API-collect-master/assets/img/follow.svg b/bilibili-API-collect-master/assets/img/follow.svg
new file mode 100644
index 0000000..900076b
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/follow.svg
@@ -0,0 +1,23 @@
+
+
+
diff --git a/bilibili-API-collect-master/assets/img/history.png b/bilibili-API-collect-master/assets/img/history.png
new file mode 100644
index 0000000..dc42b04
Binary files /dev/null and b/bilibili-API-collect-master/assets/img/history.png differ
diff --git a/bilibili-API-collect-master/assets/img/home.svg b/bilibili-API-collect-master/assets/img/home.svg
new file mode 100644
index 0000000..21b5eb9
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/home.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/like.svg b/bilibili-API-collect-master/assets/img/like.svg
new file mode 100644
index 0000000..cfc8cb1
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/like.svg
@@ -0,0 +1,5 @@
+
diff --git a/bilibili-API-collect-master/assets/img/live.svg b/bilibili-API-collect-master/assets/img/live.svg
new file mode 100644
index 0000000..4bd4e55
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/live.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/loadTV.gif b/bilibili-API-collect-master/assets/img/loadTV.gif
new file mode 100644
index 0000000..33ebcd8
Binary files /dev/null and b/bilibili-API-collect-master/assets/img/loadTV.gif differ
diff --git a/bilibili-API-collect-master/assets/img/logo.png b/bilibili-API-collect-master/assets/img/logo.png
new file mode 100644
index 0000000..57fedbd
Binary files /dev/null and b/bilibili-API-collect-master/assets/img/logo.png differ
diff --git a/bilibili-API-collect-master/assets/img/logo2.jpg b/bilibili-API-collect-master/assets/img/logo2.jpg
new file mode 100644
index 0000000..199eefc
Binary files /dev/null and b/bilibili-API-collect-master/assets/img/logo2.jpg differ
diff --git a/bilibili-API-collect-master/assets/img/musicplus.svg b/bilibili-API-collect-master/assets/img/musicplus.svg
new file mode 100644
index 0000000..a32c24e
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/musicplus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/ploading.gif b/bilibili-API-collect-master/assets/img/ploading.gif
new file mode 100644
index 0000000..1a8c97d
Binary files /dev/null and b/bilibili-API-collect-master/assets/img/ploading.gif differ
diff --git a/bilibili-API-collect-master/assets/img/ranking.svg b/bilibili-API-collect-master/assets/img/ranking.svg
new file mode 100644
index 0000000..c827ff1
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/ranking.svg
@@ -0,0 +1,4 @@
+
diff --git a/bilibili-API-collect-master/assets/img/read.svg b/bilibili-API-collect-master/assets/img/read.svg
new file mode 100644
index 0000000..8d16156
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/read.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/relation.svg b/bilibili-API-collect-master/assets/img/relation.svg
new file mode 100644
index 0000000..a9e42ec
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/relation.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/share.svg b/bilibili-API-collect-master/assets/img/share.svg
new file mode 100644
index 0000000..ca3b8fc
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/share.svg
@@ -0,0 +1,5 @@
+
diff --git a/bilibili-API-collect-master/assets/img/sponsorQR.jpg b/bilibili-API-collect-master/assets/img/sponsorQR.jpg
new file mode 100644
index 0000000..9178ffc
Binary files /dev/null and b/bilibili-API-collect-master/assets/img/sponsorQR.jpg differ
diff --git a/bilibili-API-collect-master/assets/img/sub.svg b/bilibili-API-collect-master/assets/img/sub.svg
new file mode 100644
index 0000000..907fce8
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/sub.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/tebietuijian.svg b/bilibili-API-collect-master/assets/img/tebietuijian.svg
new file mode 100644
index 0000000..4623fa6
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/tebietuijian.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/topic.svg b/bilibili-API-collect-master/assets/img/topic.svg
new file mode 100644
index 0000000..b26e7a0
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/topic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/toview.png b/bilibili-API-collect-master/assets/img/toview.png
new file mode 100644
index 0000000..cd1e0ae
Binary files /dev/null and b/bilibili-API-collect-master/assets/img/toview.png differ
diff --git a/bilibili-API-collect-master/assets/img/tuiguang.svg b/bilibili-API-collect-master/assets/img/tuiguang.svg
new file mode 100644
index 0000000..52aac0e
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/tuiguang.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/img/video_up.svg b/bilibili-API-collect-master/assets/img/video_up.svg
new file mode 100644
index 0000000..e8987f5
--- /dev/null
+++ b/bilibili-API-collect-master/assets/img/video_up.svg
@@ -0,0 +1,6 @@
+
diff --git a/bilibili-API-collect-master/assets/zone_icon/ad.svg b/bilibili-API-collect-master/assets/zone_icon/ad.svg
new file mode 100644
index 0000000..3004bbf
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/ad.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/animal.svg b/bilibili-API-collect-master/assets/zone_icon/animal.svg
new file mode 100644
index 0000000..4f7708d
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/animal.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/anime.svg b/bilibili-API-collect-master/assets/zone_icon/anime.svg
new file mode 100644
index 0000000..fd797d3
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/anime.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/car.svg b/bilibili-API-collect-master/assets/zone_icon/car.svg
new file mode 100644
index 0000000..6d30f0a
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/car.svg
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/cinephile.svg b/bilibili-API-collect-master/assets/zone_icon/cinephile.svg
new file mode 100644
index 0000000..cad39f4
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/cinephile.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/comic.svg b/bilibili-API-collect-master/assets/zone_icon/comic.svg
new file mode 100644
index 0000000..bb78da2
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/comic.svg
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/dance.svg b/bilibili-API-collect-master/assets/zone_icon/dance.svg
new file mode 100644
index 0000000..87a5ad1
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/dance.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/digital.svg b/bilibili-API-collect-master/assets/zone_icon/digital.svg
new file mode 100644
index 0000000..56ea8f9
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/digital.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/documentary.svg b/bilibili-API-collect-master/assets/zone_icon/documentary.svg
new file mode 100644
index 0000000..fcaf707
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/documentary.svg
@@ -0,0 +1,31 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/douga.svg b/bilibili-API-collect-master/assets/zone_icon/douga.svg
new file mode 100644
index 0000000..5cf324c
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/douga.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/ent.svg b/bilibili-API-collect-master/assets/zone_icon/ent.svg
new file mode 100644
index 0000000..d938420
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/ent.svg
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/fashion.svg b/bilibili-API-collect-master/assets/zone_icon/fashion.svg
new file mode 100644
index 0000000..cfb6621
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/fashion.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/food.svg b/bilibili-API-collect-master/assets/zone_icon/food.svg
new file mode 100644
index 0000000..e554eb5
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/food.svg
@@ -0,0 +1,32 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/game.svg b/bilibili-API-collect-master/assets/zone_icon/game.svg
new file mode 100644
index 0000000..59a2188
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/game.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/guochuang.svg b/bilibili-API-collect-master/assets/zone_icon/guochuang.svg
new file mode 100644
index 0000000..a10c3e9
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/guochuang.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/information.svg b/bilibili-API-collect-master/assets/zone_icon/information.svg
new file mode 100644
index 0000000..c33d838
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/information.svg
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/kichiku.svg b/bilibili-API-collect-master/assets/zone_icon/kichiku.svg
new file mode 100644
index 0000000..2d74f85
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/kichiku.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/knowledge.svg b/bilibili-API-collect-master/assets/zone_icon/knowledge.svg
new file mode 100644
index 0000000..8ede0d7
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/knowledge.svg
@@ -0,0 +1,18 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/life.svg b/bilibili-API-collect-master/assets/zone_icon/life.svg
new file mode 100644
index 0000000..ad03a79
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/life.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/movie.svg b/bilibili-API-collect-master/assets/zone_icon/movie.svg
new file mode 100644
index 0000000..5f691a4
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/movie.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/music.svg b/bilibili-API-collect-master/assets/zone_icon/music.svg
new file mode 100644
index 0000000..408c93c
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/music.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/sports.svg b/bilibili-API-collect-master/assets/zone_icon/sports.svg
new file mode 100644
index 0000000..bd4c97e
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/sports.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/tech.svg b/bilibili-API-collect-master/assets/zone_icon/tech.svg
new file mode 100644
index 0000000..4c3795b
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/tech.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_icon/teleplay.svg b/bilibili-API-collect-master/assets/zone_icon/teleplay.svg
new file mode 100644
index 0000000..a6a39af
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_icon/teleplay.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/ai.svg b/bilibili-API-collect-master/assets/zone_v2_icon/ai.svg
new file mode 100644
index 0000000..797fd6f
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/ai.svg
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/animal.svg b/bilibili-API-collect-master/assets/zone_v2_icon/animal.svg
new file mode 100644
index 0000000..74cb01e
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/animal.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/car.svg b/bilibili-API-collect-master/assets/zone_v2_icon/car.svg
new file mode 100644
index 0000000..9dd5705
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/car.svg
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/cinephile.svg b/bilibili-API-collect-master/assets/zone_v2_icon/cinephile.svg
new file mode 100644
index 0000000..de746dc
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/cinephile.svg
@@ -0,0 +1,32 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/dance.svg b/bilibili-API-collect-master/assets/zone_v2_icon/dance.svg
new file mode 100644
index 0000000..1082252
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/dance.svg
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/douga.svg b/bilibili-API-collect-master/assets/zone_v2_icon/douga.svg
new file mode 100644
index 0000000..00a4f8c
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/douga.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/emotion.svg b/bilibili-API-collect-master/assets/zone_v2_icon/emotion.svg
new file mode 100644
index 0000000..8ed2de6
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/emotion.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/entertainment.svg b/bilibili-API-collect-master/assets/zone_v2_icon/entertainment.svg
new file mode 100644
index 0000000..b69d02f
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/entertainment.svg
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/fashion.svg b/bilibili-API-collect-master/assets/zone_v2_icon/fashion.svg
new file mode 100644
index 0000000..5572bef
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/fashion.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/food.svg b/bilibili-API-collect-master/assets/zone_v2_icon/food.svg
new file mode 100644
index 0000000..b9ada08
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/food.svg
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/game.svg b/bilibili-API-collect-master/assets/zone_v2_icon/game.svg
new file mode 100644
index 0000000..7fe5842
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/game.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/gym.svg b/bilibili-API-collect-master/assets/zone_v2_icon/gym.svg
new file mode 100644
index 0000000..4b1e3a8
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/gym.svg
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/handmake.svg b/bilibili-API-collect-master/assets/zone_v2_icon/handmake.svg
new file mode 100644
index 0000000..96f3bcc
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/handmake.svg
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/health.svg b/bilibili-API-collect-master/assets/zone_v2_icon/health.svg
new file mode 100644
index 0000000..ee172e4
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/health.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/home.svg b/bilibili-API-collect-master/assets/zone_v2_icon/home.svg
new file mode 100644
index 0000000..066b5dd
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/home.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/information.svg b/bilibili-API-collect-master/assets/zone_v2_icon/information.svg
new file mode 100644
index 0000000..68a2917
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/information.svg
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/kichiku.svg b/bilibili-API-collect-master/assets/zone_v2_icon/kichiku.svg
new file mode 100644
index 0000000..a02252b
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/kichiku.svg
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/knowledge.svg b/bilibili-API-collect-master/assets/zone_v2_icon/knowledge.svg
new file mode 100644
index 0000000..154fe65
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/knowledge.svg
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/life_experience.svg b/bilibili-API-collect-master/assets/zone_v2_icon/life_experience.svg
new file mode 100644
index 0000000..2ef38e2
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/life_experience.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/life_joy.svg b/bilibili-API-collect-master/assets/zone_v2_icon/life_joy.svg
new file mode 100644
index 0000000..1b31b60
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/life_joy.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/music.svg b/bilibili-API-collect-master/assets/zone_v2_icon/music.svg
new file mode 100644
index 0000000..10c5f58
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/music.svg
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/outdoors.svg b/bilibili-API-collect-master/assets/zone_v2_icon/outdoors.svg
new file mode 100644
index 0000000..0af234d
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/outdoors.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/painting.svg b/bilibili-API-collect-master/assets/zone_v2_icon/painting.svg
new file mode 100644
index 0000000..2e0cd81
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/painting.svg
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/parenting.svg b/bilibili-API-collect-master/assets/zone_v2_icon/parenting.svg
new file mode 100644
index 0000000..719ea97
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/parenting.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/rural.svg b/bilibili-API-collect-master/assets/zone_v2_icon/rural.svg
new file mode 100644
index 0000000..86890a6
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/rural.svg
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/shortplay.svg b/bilibili-API-collect-master/assets/zone_v2_icon/shortplay.svg
new file mode 100644
index 0000000..dae6736
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/shortplay.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/sports.svg b/bilibili-API-collect-master/assets/zone_v2_icon/sports.svg
new file mode 100644
index 0000000..70f6360
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/sports.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/tech.svg b/bilibili-API-collect-master/assets/zone_v2_icon/tech.svg
new file mode 100644
index 0000000..9874d4e
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/tech.svg
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/travel.svg b/bilibili-API-collect-master/assets/zone_v2_icon/travel.svg
new file mode 100644
index 0000000..e2891a1
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/travel.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/assets/zone_v2_icon/vlog.svg b/bilibili-API-collect-master/assets/zone_v2_icon/vlog.svg
new file mode 100644
index 0000000..bb8343d
--- /dev/null
+++ b/bilibili-API-collect-master/assets/zone_v2_icon/vlog.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/bilibili-API-collect-master/docs/APP_widget/splash.md b/bilibili-API-collect-master/docs/APP_widget/splash.md
new file mode 100644
index 0000000..2a3179c
--- /dev/null
+++ b/bilibili-API-collect-master/docs/APP_widget/splash.md
@@ -0,0 +1,567 @@
+# APP端开屏图片
+
+## 获取APP端开屏图片列表
+
+> https://app.bilibili.com/x/v2/splash/brand/list
+
+*请求方式:GET*
+
+鉴权方式:APP
+
+**url参数:**
+
+| 参数名 | 类型 | 内容 | 必要性 | 备注 |
+|--------|-----|-------|---------|-----|
+| appkey | str | APP密钥 | APP方式必要 | |
+| ts | num | 当前时间戳 | APP方式必要 | |
+| sign | str | APP签名 | APP方式必要 | |
+
+**json回复:**
+
+根对象:
+
+| 字段 | 类型 | 内容 | 备注 |
+|---------|-----|------|---------------------------------------|
+| code | num | 返回值 | 0:成功
+
+
+## 获取APP端开屏幕广告信息
+
+> https://app.bilibili.com/x/v2/splash/list
+
+*请求方式: GET*
+
+| 参数名 | 类型 | 内容 | 必要性 | 备注 |
+|----------|-----|-----------------------|-----|-----|
+| build | int | 客户端内部版本号 | 必要 | |
+| mobi_app | str | android, iphone, ipad | 必要 | |
+| platform | str | android, ios | 必要 | |
+| height | int | 屏幕高度 | 必要 | |
+| width | int | 屏幕宽度 | 必要 | |
+| birth | str | 生日日期(四位数,例 0101) | 必要 | |
+
+**json回复:**
+
+根对象:
+
+| 字段 | 类型 | 内容 | 备注 |
+|---------|-----|------|---------------------------------------|
+| code | num | 返回值 | 0:成功:块级引用元素](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements/blockquote) | +| bold | boolean? | 加粗 | 参见 [\](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements/strong) | +| class | string? | 类名 | 参见 [class](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Global_attributes/class) | +| color | string? | 颜色 | 十六进制颜色值, 参见 [color](https://developer.mozilla.org/zh-CN/docs/Web/CSS/color) | +| header | number? | 标题级别 | 参见 [\–\
:HTML 区域标题元素](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements/Heading_Elements) | +| strike | boolean? | 删除线 | 参见 [\
](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements/s) | +| link | string? | 站内链接 | 参见 [\:锚元素](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements/a) | +| italic | boolean? | 斜体 | 参见 [\:强调元素](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements/em) | list | string? | 列表 | `bullet`: 无序列表, 参见 [\:无序列表元素](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements/ul)
`ordered`: 有序列表, 参见 [\](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements/ol) | + +`data.content` 代表的对象中的 `ops[].insert` 为对象时的对象: + +| 字段 | 类型 | 内容 | 备注 | +| ---- | ---- | ---- | ---- | +| native-image | object? | 原生图片 | 见下, 另见 [\
:图像嵌入元素](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements/img) | +| cut-off | object? | 分割线 | 见下 | +| video-card | object? | 视频卡片 | 见下 | +| article-card | object? | 专栏卡片 | 见下 | +| vote-card | object? | 投票卡片 | 见下 | +| live-card | object? | 投票卡片 | 见下 | + +`data.content` 代表的对象中的 `ops[].insert.native-image` 对象: + +注: 此处属性备注为页面实际渲染的情况 + +| 字段 | 类型 | 内容 | 备注 | +| ---- | ---- | ---- | ---- | +| alt | string | 图像的备用文本描述 | 即 `
` 的 `alt` 属性 | +| url | string | 图像的 URL | 即 `
` 的 `src` 属性 | +| width | number | 图像的宽度 | 即 `
` 的 `data-w` 属性 | +| height | number | 图像的高度 | 即 `
` 的 `data-h` 属性 | +| size | number | 图像的文件大小 | 即 `
` 的 `data-size` 属性 | +| status | number | `"loaded"` | 即 `
` 的 `data-status` 属性 | + +`data.content` 代表的对象中的 `ops[].insert.cut-off` 对象: + +| 字段 | 类型 | 内容 | 备注 | +| ---- | ------ | ---- | ---- | +| type | string | 类型 | 没错, 就是字符串 | +| url | string | 分割线图片 URL | | + +`data.content` 代表的对象中的 `ops[].insert.video-card` 对象: + +| 字段 | 类型 | 内容 | 备注 | +| ---- | ------ | ---- | ---- | +| alt | string | | | +| height | number | 卡片高度 | 似乎恒定为 `352` | +| id | string | 视频 id | 如 `av99999999` | +| size | null | | | +| status | string | `loaded` | | +| tid | number | `1.1` | | +| url | string | 卡片图片 URL | | +| width | number | 卡片宽度 | 似乎恒定为 `2632` | + +`data.content` 代表的对象中的 `ops[].insert.article-card` 对象: + +| 字段 | 类型 | 内容 | 备注 | +| ---- | ------ | ---- | ---- | +| alt | string | | | +| height | number | 卡片高度 | 似乎恒定为 `320` | +| id | string | 文章 id | 如 `cv1` | +| size | null | | | +| status | string | `loaded` | | +| tid | number | `2` | | +| url | string | 卡片图片 URL | | +| width | number | 卡片宽度 | 似乎恒定为 `2632` | + +`data.content` 代表的对象中的 `ops[].insert.vote-card` 对象: + +| 字段 | 类型 | 内容 | 备注 | +| ---- | ------ | ---- | ---- | +| alt | string | | | +| height | number | 卡片高度 | 似乎恒定为 `320` | +| id | string | 投票 id | 确实是字符串, 如 `15111509` | +| size | null | | | +| status | string | `loaded` | | +| tid | number | `7` | | +| url | string | 卡片图片 URL | | +| width | number | 卡片宽度 | 似乎恒定为 `2632` | + +`data.content` 代表的对象中的 `ops[].insert.live-card` 对象: + +| 字段 | 类型 | 内容 | 备注 | +| ---- | ------ | ---- | ---- | +| alt | string | | | +| height | number | 卡片高度 | 似乎恒定为 `352` | +| id | string | 直播间 id | 可能为长也可能为短, 如 `lv1` `lv5440` | +| size | null | | | +| status | string | `loaded` | | +| tid | number | `8` | | +| url | string | 卡片图片 URL | | +| width | number | 卡片宽度 | 似乎恒定为 `2632` | + + +`data.list` 对象: + +| 字段 | 类型 | 内容 | 备注 | +| ---- | ---- | ---- | ---- | +| apply_time | string | 空 | | +| articles_count | number | 0 | | +| check_time | string | 空 | | +| ctime | number | 创建时间 | UNIX 秒级时间戳 | +| id | number | 文集 ID | | +| image_url | string | 文集封面 URL | | +| mid | number | 作者 mid | | +| name | string | 文集名称 | | +| publish_time | number | 最新发布时间 | UNIX 秒级时间戳 | +| read | number | 0 | | +| reason | string | 审核不通过理由? | | +| state | number | 文集状态 | | +| summary | string | 文集描述 | | +| update_time | number | 文集更新时间 | UNIX 秒级时间戳 | +| words | number | 文集总词数 | | + +`data.media` 对象: + +| 字段 | 类型 | 内容 | 备注 | +| ---- | ---- | ---- | ---- | +| area | string | | | +| cover | string | | | +| media_id | number | | | +| score | number | | | +| season_id | number | | | +| spoiler | number | | | +| title | string | | | +| type_id | number | | | +| type_name | string | | | + +`data.stats` 对象: + +| 字段 | 类型 | 内容 | 备注 | +| ---- | ---- | ---- | ---- | +| coin | number | 硬币数 | | +| dislike | number | 点踩数 | 0 | +| dynamic | number | 动态数? | | +| favorite | number | 收藏数 | | +| like | number | 点赞数 | | +| reply | number | 回复数 | | +| share | number | 分享数 | | +| view | number | 查看数 | | + +`data.tags[]` 对象: + +| 字段 | 类型 | 内容 | 备注 | +| ---- | ---- | ---- | ---- | +| name | string | 标签名 | | +| tid | number | 标签 ID | | + +**示例:** + +`type=0`: `cv1` 未知的光 + +```shell +curl -G 'https://api.bilibili.com/x/article/view' \ +--url-query 'id=1' \ +-A 'Mozilla/5.0' +``` + +
++ + + +`type=3`: `cv41358718` 通过 DevTools 绕过 SSR 抓包某站专栏正文接口 + +```shell +curl 'https://api.bilibili.com/x/article/view?id=41358718' \ +-A 'Mozilla/10.0' +``` + +查看响应示例:
+ +```json +{ + "code": 0, + "data": { + "act_id": 0, + "apply_time": "", + "attributes": 24, + "authenMark": null, + "author": { + "face": "https://i1.hdslb.com/bfs/face/89fe260a17891fdadc3365a9698fee52796c7765.jpg", + "fans": 3051, + "level": 6, + "mid": 91221505, + "name": "健行见远渐忘", + "nameplate": { + "condition": "2018.6.26-7.8某一天是年度大会员", + "image": "https://i0.hdslb.com/bfs/face/421179426c929dfeaed4117461c83f5d07ffb148.png", + "image_small": "https://i0.hdslb.com/bfs/face/682001c2e1c2ae887bdf2a0e18eef61180c48f84.png", + "level": "稀有勋章", + "name": "大会员2018年度勋章", + "nid": 74 + }, + "official_verify": { + "desc": "", + "type": -1 + }, + "pendant": { + "expire": 0, + "image": "https://i1.hdslb.com/bfs/face/7ae15f06f8c912435206a2578509d6bc77c12353.png", + "name": "作文鬼才", + "pid": 255 + }, + "vip": { + "avatar_subscript": 1, + "due_date": 0, + "label": { + "label_theme": "annual_vip", + "path": "http://i0.hdslb.com/bfs/vip/label_annual.png", + "text": "年度大会员" + }, + "nickname_color": "#FB7299", + "status": 1, + "theme_type": 0, + "type": 2, + "vip_pay_type": 0 + } + }, + "banner_url": "https://i0.hdslb.com/bfs/article/b1e1029c08d8ad1bb06460d736839a7741dd7925.jpg", + "categories": [ + { + "id": 3, + "name": "生活", + "parent_id": 0 + }, + { + "id": 15, + "name": "日常", + "parent_id": 3 + } + ], + "category": { + "id": 15, + "name": "日常", + "parent_id": 3 + }, + "check_state": 0, + "check_time": "", + "content": "天空像是倾倒出的墨水,黑得静谧而深邃。黎明还远,光亮全无。
夜不能寐。披衣,起床。茶香的弥漫,一盏灯的相伴。夜,你是我久别重逢的朋友,那一刹那的相见,带给了我久违的安思。如果不是梦魇的皮闹,我本不该投入你的怀抱,此时此刻,万念侵扰,思绪咆哮。
台灯在角落里静静注视着我,不稳定的电流偶尔会吱吱作响。一切都像打在海绵上的水,渐渐消失,慢慢隐身于黑夜,独独留下我自己。一盏灯就可以把夜撕出个伤口,我似乎听到夜逃离光亮的脚步声,窸窸窣窣。它在蔓延,吞噬的野心已越过了窗沿。
我捧着茶杯,仿佛是在捧着一整个空间,凝重而诡异。茶香绕过茶杯脱落的瓷层向四周扩散,空气闻到了茶香,于是身边的气息似乎活跃了起来。转念一想,人生如茶。器皿的束缚不是茶香的归宿,所以人生的低谷也不是生命的全部。黑暗是光明的束缚,但光明却是黑暗的归宿。
灯光下,书架上一本金色题字的书的书名熠熠发光,我起身取下书,发现是海伦写的《假如给我三天光明》,我内心一热,忽然间有些感动。一个身体残疾的孩子,孱弱的身躯却支撑起了刚劲的灵魂。她的眼前一片漆黑,但内心世界却是光彩艳丽的。她的心中永远都有一抹无形的光,指引着她踏遍人生的路途。
时钟告诉我现在是凌晨四点。我舒展下筋骨,刚要起身,一只飞虫不偏不斜恰好闯入我的视线。灯光是它唯一的追寻。我不敢有任何的动作,只好也只能静静看着这只可爱的生灵。它好像不知道我是它的伙伴,也许它真的以为光亮是它唯一的追寻。但是我却欣慰于这突如其来的伙伴。光明可以带来很多东西,有时候也包括一份黑暗中的灵魂的一份安慰。我不忍打扰它与光明的约会,于是悄悄起身慢慢摸索着到了阳台。
阳台和我沉入了外面黑暗的梦乡。隐约可以听到一些不知名的昆虫在开着它们的舞会,我猜它们都端着无形的酒杯。它们在黑暗中买醉,因为明天的天明将是它们的散会。我有些感怀,也有些悲哀。
夜风股股咆哮着,我昂首顶住风头,仰望着漆黑的四周。很远很远处,灯塔若隐若现。它向来就是一头巨大的野兽,而此时它却空荡的像杯将尽的酒。它散发出柔弱的灯光在风中摇曳,忽明忽暗,像是一场与黑暗的决斗,随时都可能葬身在这黎明的前奏。
不忍心这惨烈的一幕,我踱步来到窗前。窗子很旧了,旧到已经无法再承受住时间的践踏了。可无论它多么陈旧,此时的它是神奇的——窗外黑暗窗内光。人心不也是一扇窗户吗?一半明媚一半伤。窗户把光亮的一面给了光明,人心是不是也应该把明媚的一半朝向世界呢?
屋内的灯倏忽闪了闪,我晃过神来,发现天边已经有抹淡红色了。黎明的脚步已经迈出了一半。那只与台灯约会的虫子似乎很满意,兴高采烈的向着呼之欲出的朝阳的方向窜去了。我望着它的背影,轻轻挥了挥手,心中暗念:“亲爱的朋友,恭喜你找到了属于自己的方向和追求,希望你能在光和热中接受生命的升华。”
天空见亮了,有点空灵,光晕中泛着感动。第一缕曙光穿透了云层,是的,我看到了,那一抹光,越来越近,越来越亮……
", + "content_pic_list": null, + "cover_avid": 0, + "ctime": 1497973729, + "dispute": null, + "dyn_id_str": "38554821905721204", + "dynamic": "It's now or never !", + "id": 1, + "image_urls": [ + "https://i0.hdslb.com/bfs/article/d2eedf1fd338bceca10099e2f7b33fa9017c859b.jpg" + ], + "is_like": false, + "keywords": "天空像是倾倒出的墨水,黑得静谧而深邃。黎明还远,光亮全无。夜不能寐。披衣,起床。茶香的弥漫,一盏灯的相伴。夜,你是我久别重逢的朋友,那一刹那的相见,带给了我久违的安思。如果不是梦魇的皮闹,我本不该投入", + "list": { + "apply_time": "", + "articles_count": 0, + "check_time": "", + "ctime": 1588601669, + "id": 253534, + "image_url": "", + "mid": 91221505, + "name": "静夜思", + "publish_time": 1625900652, + "read": 0, + "reason": "", + "state": 1, + "summary": "", + "update_time": 1625900518, + "words": 3059 + }, + "media": { + "area": "", + "cover": "", + "media_id": 0, + "score": 0, + "season_id": 0, + "spoiler": 0, + "title": "", + "type_id": 0, + "type_name": "" + }, + "mtime": 1589209079, + "origin_image_urls": [ + "https://i0.hdslb.com/bfs/article/b1e1029c08d8ad1bb06460d736839a7741dd7925.jpg" + ], + "origin_template_id": 4, + "original": 0, + "private_pub": 0, + "publish_time": 1519913233, + "reprint": 0, + "state": 0, + "stats": { + "coin": 2543, + "dislike": 1, + "dynamic": 0, + "favorite": 17520, + "like": 32484, + "reply": 14399, + "share": 616, + "view": 1631212 + }, + "summary": "天空像是倾倒出的墨水,黑得静谧而深邃。黎明还远,光亮全无。夜不能寐。披衣,起床。茶香的弥漫,一盏灯的相伴。夜,你是我久别重逢的朋友,那一刹那的相见,带给了我久违的安思。如果不是梦魇的皮闹,我本不该投入", + "tags": [ + { + "name": "静夜思", + "tid": 239416 + } + ], + "template_id": 4, + "title": "未知的光", + "top_video_info": null, + "total_art_num": 5, + "type": 0, + "version_id": 0, + "words": 1190 + }, + "message": "0", + "ttl": 1 +} +``` ++查看响应示例:
+ +```json +{ + "code": 0, + "message": "0", + "ttl": 1, + "data": { + "id": 41358718, + "category": { + "id": 26, + "parent_id": 17, + "name": "数码" + }, + "categories": [ + { + "id": 17, + "parent_id": 0, + "name": "科技" + }, + { + "id": 26, + "parent_id": 17, + "name": "数码" + } + ], + "title": "通过 DevTools 绕过 SSR 抓包某站专栏正文接口", + "summary": "背景\n最近没什么事做, 又回 BAC 看看有什么接口可以抓, 于是看到了陈年的专栏正文内容的 Issue #859\n由于泽生自己是写前端的, 对于浏览器 DevTools 的各项调试工具还算是比较了解, 打算想办法把专栏正文的接口抓下来 :)\n思路\n由于专栏正文完整的内容在页面加载的时候就已经通过 SSR (Server-Side Rendering, 服务端渲染) 的方式通过 + + +b0cc8411ded2f9db2cff2edb3123acac+ + + + + + +