Files
Bilibili-gadget/哔哩哔哩/6.0bilibili_audio_dl.py
T
2025-02-16 20:22:59 +08:00

1356 lines
55 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()