如题,我最近想在学校听音乐,又喜欢刷B站,就养成了用B站缓存视频当做音乐软件的习惯,但发现原版B站的缓存视频排序机制是真拉()(╯°□°)╯︵ ┻━┻
同时还有一些音乐不在音乐平台上有,我也嫌一个个找几十首歌太麻烦,于是就打算自己把我喜欢,缓存的音乐统一转存到电脑上,方便再处理。
总结一下,这脚本就干3件事:
1.访问对应目录(/storage/emulated/0/Android/data/tv.danmaku.bili/download)
2.通过识别标题(如:Nightcore, -, 《, 》, feat., 翻唱),视频长度(2.5分钟-6.5分钟)等筛选对应视频的audio.m4s文件,
3.将该文件转换为.mp3,修改文件名为视频标题后存储到电脑指定文件夹上。
//更新:新加入了自选adb设备名称的功能,以及增加匹配关键词
这是我完整写的需求报告(给gpt看的)( •̀ ω •́ )✧
需求
需求名称:一个一键将所有B站手机端缓存的音乐转换并打包为mp3的程序。
功能描述:
1.访问对应目录(/storage/emulated/0/Android/data/tv.danmaku.bili/download)
2.通过识别标题(如:Nightmare, -, 《, 》, feat., 翻唱),视频长度(2.5分钟-6.5分钟)等筛选对应视频的audio.m4s文件,
3.将该文件转换为.mp3,修改文件名为视频标题后存储到电脑指定文件夹上。
附加资料:
1.在手机存储目录下,每个视频占用一个文件夹,具体示例文件目录如下:
-download(文件夹)
-
- 272579578(文件夹)
-
-
- c_1170785648(文件夹)
-
-
-
-
- entry.json(存放视频的基本信息,例如视频标题)
-
-
-
-
-
- danmaku.xml(存放视频弹幕)
-
-
-
-
-
- 64(文件夹)
-
-
-
-
-
-
- audio.m4s(无画面,有声音的视频文件)
-
-
-
-
-
-
-
- video.m4s(有画面,无声音的视频文件)
-
-
-
-
-
-
-
- index.json(存放该视频在B站内的数据等)
-
-
-
-
- 321856328(文件夹)
-
- 317068286(文件夹)
-
- (还有许多视频文件夹,略,你需要操作这些所有文件夹)
2.上条的272579578 c_1170785648是无意义的随机数字名称,每条视频不同,且不提供信息。而其他的entry.json danmaku.xml 64 audio.m4s video.m4s是固定的文件结构。所以每个视频文件夹的需读取文件路径不是统一的。
3.如果你需要使用递归遍历每一个entry.json和audio.m4s ,务必保证每次找到的entry和audio归属同一个视频。
4.目标是安卓设备,你需要通过adb来连接目标设备和操作文件。
5.你需要读取的entry.json文件示例如下:
{“media_type”:2,“has_dash_audio”:true,“is_completed”:true,“total_bytes”:8273680,“downloaded_bytes”:8273680,“title”:“【无损音质】《Jar Of Love》-曲婉婷”,“type_tag”:“64”,“cover”:“http://i1.hdslb.com/bfs/archive/e9e3feb5d2f0da034e54828337c2cd70d778d05c.jpg”,“video_quality”:64,“prefered_video_quality”:64,“guessed_total_bytes”:0,“total_time_milli”:227880,“danmaku_count”:1200,“time_update_stamp”:1728195821050,“time_create_stamp”:1728195757436,“can_play_in_advance”:true,“interrupt_transform_temp_file”:false,“quality_pithy_description”:“720P”,“quality_superscript”:“”,“cache_version_code”:8100300,“preferred_audio_quality”:0,“audio_quality”:0,“avid”:272579578,“spid”:0,“seasion_id”:0,“bvid”:“”,“owner_id”:3493286545197755,“owner_name”:“迷茫久生”,“is_charge_video”:false,“verification_code”:0,“page_data”:{“cid”:1170785648,“page”:1,“from”:“vupload”,“part”:“【无损音质】《Jar Of Love》-曲婉婷”,“link”:“”,“rich_vid”:“”,“vid”:“”,“has_alias”:false,“tid”:193,“width”:1280,“height”:720,“rotate”:0,“download_title”:“”,“download_subtitle”:“”}}
在经历了四个小时的艰苦奋战(?),大修3遍以上,问了好几次o1和十几次4o后,总算攒出了个能用的代码。
import os
import subprocess
import json
import re
import shutil
import posixpath
from pathlib import Path
# 配置部分(请自行修改此内容)
DOWNLOAD_DIR = "/storage/emulated/0/Android/data/tv.danmaku.bili/download"
LOCAL_TEMP_DIR = r"\temp" # 本地临时存储目录
OUTPUT_DIR = r"\output" # 最终输出的MP3文件目录
ADB_PATH = r"C:\???\adb.exe" # 确保这是adb.exe的完整路径
FFMPEG_PATH = r"C:\???\ffmpeg.exe" # 请指定FFmpeg的完整路径
ADB_DEVICE_NAME = "0123456789ABCDEF" # 请指定目标adb设备名称
# 即使可在命令行中执行adb和ffmpeg,我依然会遇到找不到对应命令的问题(?,所以在此保留对应路径
# 筛选条件(可自行修改或添加)
TITLE_KEYWORDS = ["Nightcore", "-", "《", "》", "feat.", "cover", "唱", "「", "」", "音乐", "歌词", "pv付", "无损", "Hi-Res", "歌单"]
MIN_DURATION = 150000 # 毫秒,2.5分钟, 150秒
MAX_DURATION = 390000 # 毫秒,6.5分钟, 390秒
# 创建必要的本地目录
os.makedirs(LOCAL_TEMP_DIR, exist_ok=True)
os.makedirs(OUTPUT_DIR, exist_ok=True)
def adb_command_with_device(*args):
"""构建带设备选择的ADB命令"""
cmd = [ADB_PATH]
if ADB_DEVICE_NAME:
cmd.extend(["-s", ADB_DEVICE_NAME])
cmd.extend(args)
return cmd
def adb_pull(remote_path, local_path):
"""使用ADB拉取文件或文件夹"""
cmd = adb_command_with_device("pull", remote_path, local_path)
print(f"执行命令: {' '.join(cmd)}") # 调试信息
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8', # 指定编码
errors='ignore' # 忽略无法解码的字节
)
if result.returncode != 0:
print(f"ADB pull failed for {remote_path}: {result.stderr}")
return False
return True
def adb_shell_ls(remote_path):
"""使用ADB列出远程目录内容"""
cmd = adb_command_with_device("shell", "ls", "-1", remote_path)
print(f"执行命令: {' '.join(cmd)}") # 调试信息
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8', # 指定编码
errors='ignore' # 忽略无法解码的字节
)
if result.returncode != 0:
print(f"ADB ls failed for {remote_path}: {result.stderr}")
return []
return result.stdout.strip().split('\n')
def convert_m4s_to_mp3(m4s_path, mp3_path):
"""使用FFmpeg将m4s文件转换为mp3"""
cmd = [
FFMPEG_PATH, # 使用完整路径
"-y", # 覆盖输出文件
"-i", m4s_path,
"-vn", # 不处理视频
"-ar", "44100", # 采样率
"-ac", "2", # 声道数
"-b:a", "192k", # 音频比特率
mp3_path
]
print(f"执行命令: {' '.join(cmd)}") # 调试信息
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8', # 指定编码
errors='ignore' # 忽略无法解码的字节
)
if result.returncode != 0:
print(f"FFmpeg转换失败: {m4s_path}\n错误信息: {result.stderr}")
return False
return True
def sanitize_filename(filename):
"""移除文件名中的非法字符"""
return re.sub(r'[\\/*?:"<>|]', "_", filename)
def main():
# 获取所有顶级视频文件夹
video_folders = adb_shell_ls(DOWNLOAD_DIR)
if not video_folders:
print("没有找到任何视频文件夹。")
return
for video_folder in video_folders:
remote_video_path = posixpath.join(DOWNLOAD_DIR, video_folder)
print(f"正在处理视频文件夹: {remote_video_path}") # 调试信息
# 进入视频文件夹,获取子文件夹(随机数字命名)
sub_folders = adb_shell_ls(remote_video_path)
for sub_folder in sub_folders:
remote_sub_path = posixpath.join(remote_video_path, sub_folder)
print(f"找到子文件夹: {remote_sub_path}") # 调试信息
# 检查是否包含64文件夹
remote_64_path = posixpath.join(remote_sub_path, "64")
sub_contents = adb_shell_ls(remote_sub_path)
if "64" not in sub_contents:
print(f"子文件夹中不包含64文件夹: {remote_sub_path}") # 调试信息
continue # 不包含64文件夹,跳过
# 拉取entry.json
remote_entry_json = posixpath.join(remote_sub_path, "entry.json")
local_video_temp_dir = os.path.join(LOCAL_TEMP_DIR, video_folder, sub_folder)
os.makedirs(local_video_temp_dir, exist_ok=True)
if not adb_pull(remote_entry_json, local_video_temp_dir):
print(f"拉取entry.json失败: {remote_entry_json}") # 调试信息
continue # 拉取失败,跳过
entry_json_path = os.path.join(local_video_temp_dir, "entry.json")
if not os.path.exists(entry_json_path):
print(f"缺少entry.json: {remote_entry_json}")
continue
# 解析entry.json
try:
with open(entry_json_path, 'r', encoding='utf-8') as f:
entry_data = json.load(f)
title = entry_data.get("title", "")
duration = entry_data.get("total_time_milli", 0)
print(f"解析entry.json成功: 标题='{title}', 时长={duration}毫秒") # 调试信息
except Exception as e:
print(f"解析entry.json失败: {entry_json_path}, 错误: {e}")
continue
# 应用筛选条件
if not any(keyword in title for keyword in TITLE_KEYWORDS):
print(f"标题不符合筛选条件: {title}") # 调试信息
continue
if not (MIN_DURATION <= duration <= MAX_DURATION):
print(f"视频时长不符合筛选条件: {duration}毫秒") # 调试信息
continue
# 拉取audio.m4s
remote_audio_m4s = posixpath.join(remote_sub_path, "64", "audio.m4s")
if not adb_pull(remote_audio_m4s, local_video_temp_dir):
print(f"拉取audio.m4s失败: {remote_audio_m4s}") # 调试信息
continue
local_audio_m4s = os.path.join(local_video_temp_dir, "audio.m4s")
if not os.path.exists(local_audio_m4s):
print(f"缺少audio.m4s: {remote_audio_m4s}")
continue
# 转换为MP3
sanitized_title = sanitize_filename(title)
mp3_filename = f"{sanitized_title}.mp3"
mp3_output_path = os.path.join(OUTPUT_DIR, mp3_filename)
if convert_m4s_to_mp3(local_audio_m4s, mp3_output_path):
print(f"成功转换并保存: {mp3_output_path}")
else:
print(f"转换失败: {local_audio_m4s}")
# 清理临时文件夹
shutil.rmtree(local_video_temp_dir, ignore_errors=True)
# 清理总临时文件夹
shutil.rmtree(LOCAL_TEMP_DIR, ignore_errors=True)
print("所有操作完成。")
if __name__ == "__main__":
main()
详细说明:(by GPT)
一个自动化脚本,一键将B站手机端缓存的音乐转换并打包为MP3文件。此脚本结合了ADB和FFmpeg的强大功能,实现了从安卓设备到PC的高效音频转换流程。
(by 我自己)
注意事项:本脚本通过标题和时长来识别音乐内容,肯定有不准的,建议自行修改。(;・∀・)
如果想抄作业的话,要安装adb和ffmpeg,要打开安卓的usb调试…大概就这些了。( ´▽` )ノ
还有我这个需求也挺特殊的,只能给能够安装B站,但无法保证时刻联网的设备使用,可能是独一份了。
碎碎碎念:
第一次尝试做一点原创内容(其实也不是那么原创…)感觉还是不错的,尤其是反复调试后终于成功的那一刻,good! (๑•̀ㅂ•́)و✧