【自制程序】识别并转存B站手机端所有缓存音乐

如题,我最近想在学校听音乐,又喜欢刷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! (๑•̀ㅂ•́)و✧

10 个赞

感谢楼主分享

2 个赞

感谢大佬分享

感谢大佬分享

感谢大佬分享 。

//更新:新加入了自选adb设备名称的功能,以及增加和修改部分匹配关键词

(把nightcore打成nightmare这件事,竟然一个人(包括gpt)都没发现吗…)(汗~)

感谢分享,很细致。四个小时的艰苦奋战是建立在能看懂代码的基础上吗?本人纯小白所以问问。这才是GPT存在的意义