ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

飞牛音乐二开实战:在线曲库、多歌单聚合与内置大模型源

飞牛音乐二开实战:在线曲库、多歌单聚合与内置大模型源

飞牛音乐这个应用我在自己的机器上跑了小半年,本地曲库管理确实省心,但有个坎一直绕不过去:硬盘里存的那几千首歌,听来听去就那几个歌手。前阵子看到“飞牛音乐添加在线曲库!多歌单 + 内置大模型源【二开】”这个方向,第一反应是这事儿能干,而且值得干。这里的在线曲库不是让你去搬运别人的资源,而是把你自己在别处合法拥有、或者公开授权的音源,通过接口聚合进播放列表;多歌单是把散落在不同来源的歌单拉到同一个界面下统一管理;内置大模型源,则是让模型承担自然语言点歌、按情绪生成歌单、语义搜索这类活。二开两个字说得很直白:装个插件就完事的路子走不通,得动代码、改数据模型、自己搭一层服务。这篇内容适合手里有飞牛OS、会一点 Docker 和 Python、愿意为了听歌体验折腾一个周末的人;如果你只是想点开就听,官方功能其实够用,但要是你也受不了“想听某首歌还得先去找文件”的别扭,往下看。

1. 先把需求拆开:这个二开到底要解决什么

1.1 从“能放本地歌”到“想听什么有什么”

本地曲库的本质是文件索引,它假定你已经拥有了音频文件。而人的听歌行为往往是反过来的:先有一个念头,再去决定听什么。这两个动作之间隔着一整个下载、整理、刮削、入库的流程,流程越长,听歌的冲动衰减得越快。在线曲库要解决的就是这个衰减问题,它把“找文件”变成“查接口”,把分钟级的操作压缩到秒级。

但这里必须先把边界划清楚。我见过不少人一上来就想抓遍全网,这种做法既不稳定也不合适,音源授权、服务条款、接口频率限制都是硬约束。我的做法是只接三类源:自己账号下的收藏与自建歌单、明确提供公开接口的合法音源、以及局域网内另一台机器上的音乐服务。这三类的共同点是“我本来就有权访问”,二开只是把访问路径缩短,不改变内容的归属。

理解这一点之后,整个项目的目标就变得具体了:在飞牛音乐的数据层外面套一层聚合层,把多个来源的曲目、歌单映射成统一的内部结构,再让播放器像访问本地文件一样访问它们。目标一旦具体,技术选型就不会跑偏。

1.2 三个模块的真实边界:曲库、歌单、模型

很多人把这三个东西混在一起做,结果代码越写越乱。我的拆法是三层,每层只干一件事。

曲库层负责“一首歌是什么”。它对外暴露的是统一的曲目对象,内部要处理各来源字段名不一致、时长精度不一致、封面尺寸不一致的问题。这一层不做任何推荐、不做任何排序,只做归一化。

歌单层负责“一组歌怎么组织”。它关心的是顺序、去重、增量同步、以及歌单本身的生命周期。这一层会调用曲库层拿曲目详情,但绝不直接碰音源接口。

模型层负责“用人话表达需求”。它接收自然语言,输出结构化的查询条件或者歌单草稿,然后交给前两层去执行。模型不直接返回音频地址,也不直接写数据库,这个约束非常关键,后面会展开讲。

分层的直接好处是排障时能快速定位。播放失败是曲库层的地址问题,歌单少歌是同步层的合并问题,答非所问是模型层的提示词问题。混在一起写的话,一个现象能牵扯出五个可能原因,排查成本会翻好几倍。

2. 架构设计:先把数据模型钉死

2.1 统一曲目模型是全局地基

二次开发最容易犯的错是先写接口再想数据结构,等接第三个源的时候发现字段根本对不上,只能推倒重来。我的建议是第一天就把曲目模型定下来,后面所有适配器都往这个模型上靠。

核心字段我列了一张表,实际落地时按需增减,但这几个不能省:

字段类型说明易错点
track_idstring内部唯一ID,建议用来源前缀+原始ID不同源ID可能撞车,必须加前缀
titlestring曲名带版本后缀的要单独剥离
artiststring艺术家,多艺术家用固定分隔符分隔符不统一会导致去重失效
albumstring专辑名单曲可能为空,要允许 null
durationint时长,单位秒有的源给毫秒,有的给浮点秒
cover_urlstring封面地址可能有时效,需缓存到本地
sourcestring来源标识用于路由到对应适配器
play_urlstring播放地址可能是临时签名,不能长期存
fingerprintstring去重指纹生成规则要固定且可复现

fingerprint这个字段值得单独说。我用的规则是:曲名和艺术家先做小写化、去空格、去标点,然后拼接,再加上时长按 3 秒粒度取整的值,最后算一个短哈希。3 秒这个粒度是试出来的,太小会把同一首歌的不同码率版本判成两首,太大又会把现场版和录音室版误合并。这个阈值没有标准答案,取决于你的曲库构成,可以先跑一批数据看误判率再定。

数据库我用的是 SQLite,理由很简单:单用户场景、数据量在十万级以内、部署零依赖。如果你的曲库规模更大或者要多端并发写,换成 PostgreSQL 也只需要改连接串和少量语法。

CREATE TABLE tracks ( track_id TEXT PRIMARY KEY, title TEXT NOT NULL, artist TEXT, album TEXT, duration INTEGER, cover_url TEXT, source TEXT NOT NULL, play_url TEXT, fingerprint TEXT NOT NULL, created_at INTEGER DEFAULT (strftime('%s','now')) ); CREATE INDEX idx_tracks_fp ON tracks(fingerprint); CREATE INDEX idx_tracks_source ON tracks(source);

索引这两个字段是有讲究的。fingerprint上加索引是为了去重时的等值查询能走索引,source上加索引是为了按来源批量刷新时不用全表扫。我一开始只建了主键,结果全量同步一次要跑四十多秒,加了索引之后降到三秒出头。

2.2 三层缓存与降级策略

在线曲库最怕的不是没数据,是数据来得太慢。一次搜索打出去,如果每个源都实时请求,页面转圈能转到你以为程序挂了。我做了三层缓存,从快到慢依次是内存、本地库、远端接口。

内存层放热点数据,比如最近播放的五十首、当前歌单的完整曲目列表,过期时间设得很短,六十秒到五分钟。本地库放已经同步过的曲目元数据,播放地址单独存一张表并带过期时间戳。远端接口只在缓存未命中时才碰。

降级策略是这套东西能不能用的关键。我的规则是:远端超时两秒就放弃,返回本地缓存里的结果并打一个“数据可能不完整”的标记;如果本地也没有,返回空列表而不是报错。这个设计听起来很保守,但实际体验下来,用户看到“暂无结果”远比看到红色报错框舒服。

注意:播放地址这类带签名的字段千万别当成长效数据存。我吃过一次亏,把签名地址写进主表,第二天全部失效,整个曲库变成一堆点不动的条目。正确做法是单独一张play_urls表,带expires_at字段,取用前先判断是否过期,过期就走刷新逻辑。

2.3 大模型为什么放在编排层而不是数据层

这是架构上我想强调最久的一点。刚动手的时候我也图省事,让模型直接输出播放地址,结果就是它经常一本正经地编造出一个看起来很像那么回事的 URL。模型擅长的是理解意图和生成结构,不擅长记住精确的字符串,这是它的工作机制决定的,不是提示词能彻底解决的。

把模型放在编排层,它的职责就变成:把“来点适合下雨天写代码听的”翻译成{mood: "calm", tempo: "slow", genre: ["ambient", "post-rock"], limit: 30}这样的结构化条件。真正的检索由曲库层去做,模型看不到也不需要看到任何真实地址。这个改动的效果非常直接,幻觉率从“经常”降到“偶尔”,而且偶尔出错也只是推荐不准,不会出现点了没反应的死链。

模型层还要设一个硬性约束:输出必须是 JSON,且必须通过 schema 校验。校验不过就重试一次,再不过就退回关键词匹配。这条兜底链路看起来笨,但它保证了功能在模型抽风的时候依然可用。

3. 在线曲库接入的实操细节

3.1 音源适配器的接口约定

多个来源要接进来,最省事的办法是定义一个适配器基类,每个源写一个子类,上层只认基类的方法。我用 Python 写的,结构大致是这样:

from abc import ABC, abstractmethod from dataclasses import dataclass @dataclass class Track: track_id: str title: str artist: str album: str | None duration: int cover_url: str | None source: str play_url: str | None = None class SourceAdapter(ABC): name: str @abstractmethod def search(self, keyword: str, limit: int = 20) -> list[Track]: ... @abstractmethod def fetch_play_url(self, track_id: str) -> str | None: ... @abstractmethod def list_playlists(self) -> list[dict]: ... @abstractmethod def fetch_playlist_tracks(self, playlist_id: str) -> list[Track]: ...

四个方法是必须的,少一个都不完整。search支持点歌,fetch_play_url支持播放,后两个支持歌单。如果你的源不支持歌单,就返回空列表,上层做好判断即可,不要为了凑接口去造假数据。

每个适配器内部要自己处理限流。我的做法是在基类里加一个简单的令牌桶,每个源配置自己的速率,比如公开接口一秒一次、自建服务十秒一次。限流做在适配器里而不是调用方,是因为只有适配器自己知道对面能承受多大压力。

3.2 元数据清洗与去重

接进来的原始数据脏得超乎想象。曲名里带着“[320k]”“【Hi-Res】”这类后缀,艺术家字段里塞着“某某 / 某某某”和“某某、某某某”两种分隔符,时长有的是 245 有的是 245000。清洗这步省不得,省了后面全是坑。

我写了一个清洗函数,按顺序处理:去掉括号类质量标记、统一分隔符为顿号、时长超过一万的按毫秒除以一千、曲名和艺术家做全角转半角、去掉首尾空白和不可见字符。这套规则不是一次写完的,是边跑边补,每发现一种新脏数据就加一条规则。

去重用的是指纹加二次校验。指纹相同直接判定为同一首,这是快速路径。指纹不同但曲名和艺术家高度相似时,再做一次时长比对,差值在三秒内才合并。为什么要二次校验?因为有些歌在不同源里时长差得多,比如一个是 3 分 45 秒,一个是 4 分 12 秒,那很可能是两个不同版本,硬合并会让用户听错。

合并策略我是这样定的:优先保留有封面的、时长更完整的、来源更“正式”的那条记录,其余字段做补齐。比如 A 源有封面没歌词,B 源有歌词没封面,合并后两条都有。这个策略要写进配置,方便以后调整优先级。

3.3 播放地址的时效与中转

绝大多数在线音源的播放地址是带签名的临时链接,有效期从几分钟到几小时不等。直接把这种地址丢给前端,会出现“列表能看、点了不响”的经典现象。

我的处理是播放时实时换取地址,不做预取。前端点播放,请求打到我的服务,服务判断本地缓存的地址是否还有五分钟以上有效期,有就直接返回,没有就调适配器刷新一次再返回。五分钟这个阈值是留出的缓冲,防止前端拿到地址后还没开始播就过期了。

还有一个细节是跨域和防盗链。有些源的资源要求带 Referer 或者特定 Header 才能取到,浏览器直接访问会被拦。这种就得在服务端做一个转发接口,前端请求自己的域名,服务端去取流再吐回来。转发接口要注意别把整个文件读进内存,用流式响应,边读边写,否则一首无损歌能直接把内存打满。

from fastapi import FastAPI from fastapi.responses import StreamingResponse import httpx app = FastAPI() @app.get("/stream/{track_id}") async def stream(track_id: str): url = resolve_play_url(track_id) client = httpx.AsyncClient(timeout=10) req = client.build_request("GET", url, headers={"User-Agent": UA}) resp = await client.send(req, stream=True) return StreamingResponse( resp.aiter_bytes(chunk_size=64 * 1024), media_type=resp.headers.get("content-type", "audio/mpeg"), )

chunk_size设成 64KB 是权衡的结果。太小会导致系统调用过于频繁,太大又会让首字节延迟变高。这个值在局域网环境里差别不明显,但在跨网络访问时能感觉到。

4. 多歌单聚合:合并、去重与增量同步

4.1 歌单映射表设计

歌单和曲目是两套东西,得单独建模。我用了三张表:歌单主表、歌单曲目关联表、同步状态表。

歌单主表存歌单的元信息,包括内部 ID、来源、来源侧 ID、名称、封面、曲目数、最近同步时间。关联表存歌单 ID 和曲目 ID 的对应关系,外加一个排序字段。同步状态表记录每个歌单上次同步的游标,用于增量拉取。

排序字段这块我踩过坑。一开始用的是自增主键排序,结果增量同步新插入的歌全跑到末尾,和源站的顺序对不上。后来改成显式存position字段,同步时按源站返回的顺序写,前端按position排序,问题才解决。

CREATE TABLE playlists ( playlist_id TEXT PRIMARY KEY, source TEXT NOT NULL, remote_id TEXT NOT NULL, name TEXT NOT NULL, cover_url TEXT, track_count INTEGER DEFAULT 0, synced_at INTEGER, UNIQUE(source, remote_id) ); CREATE TABLE playlist_tracks ( playlist_id TEXT NOT NULL, track_id TEXT NOT NULL, position INTEGER NOT NULL, added_at INTEGER, PRIMARY KEY (playlist_id, track_id) );

UNIQUE(source, remote_id)这个约束很重要,它保证了同一个源的同一個歌单不会被重复插入。我一开始没加,结果定时同步跑了两轮之后,歌单列表里出现了两个一模一样的条目。

4.2 增量同步与冲突处理

全量同步很奢侈,一个几百首的歌单每次拉一遍,接口压力和耗时都不划算。增量同步的关键是找到一个可靠的游标。有的源支持按更新时间过滤,那就用时间戳;有的源只返回全量,那就退化成“拉全量但只写差异”。

差异写入的逻辑是:拉回来的曲目列表和库里的做对比,新增的插入,消失的删除,顺序变了的更新position。删除这步要谨慎,我设了一个保护阈值,单次同步删除超过总数三成的,直接中止并记日志,防止源站临时抽风返回空列表把本地歌单清空。

冲突处理主要出现在同一首歌被加进多个歌单的情况。这时候曲目本身只存一份,关联表里有多条记录,这没问题。真正麻烦的是同一首歌在不同歌单里元数据不一致,比如 A 歌单里艺术家是“某某”,B 歌单里是“某某乐队”。我的处理是以曲目表里的记录为准,关联表只存 ID,不存冗余字段。这样只要曲目表更新了,所有歌单看到的就是一致的。

4.3 排序与“最近添加”的隐藏问题

“最近添加”这个功能看起来简单,实现起来有个隐蔽的坑:不同源对“添加时间”的定义不一样。有的给的是加入歌单的时间,有的给的是歌曲发行时间,有的干脆不给。混在一起排序,结果就是时间线乱七八糟。

我的做法是给added_at加一个来源可信度标记。源明确提供添加时间的,直接用;源不提供的,用同步时刻代替,但标记为“估算”,在排序时排在真实时间之后。这个细节用户感知不到,但时间线的合理性会明显提升。

还有一个问题是时区。有的源返回的是 UTC 时间戳,有的是本地时间字符串。我在入库前统一转成 Unix 时间戳,转换时按源配置指定时区。这个如果不处理,跨时区排序会差出好几个小时,用户的“最近添加”里会冒出一些莫名其妙的旧歌。

5. 内置大模型源的落地方式

5.1 模型在音乐场景里到底干什么

先把预期摆正:模型不是搜索引擎,它不知道你的曲库里有什么。它的价值在于把模糊的人类表达转成精确的查询条件。我目前用到的场景有三个,按使用频率排。

第一个是自然语言点歌。“找一首适合深夜开车听的、不要太吵的、女声为主的”这种需求,关键词搜索根本没法处理。模型把它拆成风格、情绪、人声类型、节奏几个维度,再映射到曲库的标签字段上。

第二个是歌单生成。给一个主题或者场景,让模型从候选曲目池里挑一批并给出排序建议。这里要注意,候选池必须是曲库层先筛出来的,不能把整个曲库塞给模型,否则上下文长度和成本都撑不住。

第三个是语义搜索增强。用户搜“那种听完想哭的歌”,传统搜索匹配不到任何字段。模型可以把这个意图扩展成一组同义词和情绪标签,再去检索。

5.2 提示词与结构化输出约束

提示词这块我的经验是:越具体越省事,别指望模型自己领会。系统提示里要明确写清楚输出格式、字段含义、取值范围,并给一两个示例。示例的作用比大段描述强得多,这一点在实际调试中非常明显。

我用的输出结构大致是这样,用 JSON Schema 约束:

{ "type": "object", "properties": { "intent": { "type": "string", "enum": ["search", "playlist", "recommend"] }, "keywords": { "type": "array", "items": {"type": "string"}, "maxItems": 8 }, "mood": { "type": "array", "items": {"type": "string"}, "maxItems": 3 }, "tempo": { "type": "string", "enum": ["slow", "medium", "fast", "any"] }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 } }, "required": ["intent", "keywords", "limit"] }

limit设上限是必须的,不加约束模型偶尔会返回一个几百的值,直接把查询拖慢。keywords限制最多八个也是同样道理,太多关键词做交集之后结果往往是空集。

提示:schema 校验失败时的重试提示词里,要把校验错误信息一并带上,比如“上次输出缺少 limit 字段”,比单纯说“格式不对,请重试”有效得多。

5.3 幻觉治理与兜底链路

幻觉在这里的主要表现是编造不存在的曲名和艺术家。治理手段有两层,一层是结构上不给出编造空间,另一层是结果上做校验。

结构上的做法是:模型只能输出查询条件,不能输出具体曲目。生成歌单时,模型输出的是“从候选池里选择满足这些条件的曲目”,实际选择由程序完成。这样它就算想编也编不出来。

结果上的做法是:模型返回的每个曲目名都要在曲库层做一次存在性校验,不存在的直接丢弃并记日志。这个校验成本很低,一次批量查询就够,但能挡掉绝大部分脏数据。

兜底链路我设了三档。模型正常返回且校验通过,走模型结果;模型超时或校验失败,走关键词匹配,用原始输入直接搜;关键词也搜不到,返回热门推荐。这三档切换对用户是不可见的,界面上只会显示“为你推荐”,不会暴露背后降级了。

6. 部署与配置:一份可直接抄的落地清单

6.1 目录结构与容器编排

飞牛OS 上的应用大多跑在容器里,二次开发的服务也建议容器化,好处是依赖隔离、升级回滚方便。我的目录结构是这样:

/mnt/apps/music-plus/ ├── app/ │ ├── main.py │ ├── adapters/ │ ├── services/ │ └── models.py ├── data/ │ ├── music.db │ └── covers/ ├── config/ │ └── sources.yaml └── docker-compose.yml

数据目录单独挂出来,容器重建不丢数据。封面缓存也放进去,别放容器内部,否则每次更新镜像都要重新拉一遍封面。

编排文件我用的是 compose,两个服务:主服务和定时同步任务。定时任务用同一个镜像,只是启动命令不同。

services: music-plus: image: music-plus:latest container_name: music-plus restart: unless-stopped ports: - "8686:8000" volumes: - ./data:/app/data - ./config:/app/config environment: - TZ=Asia/Shanghai - DB_PATH=/app/data/music.db - CACHE_TTL=300 - MODEL_TIMEOUT=8 sync-worker: image: music-plus:latest container_name: music-plus-sync restart: unless-stopped command: python -m app.sync_daemon volumes: - ./data:/app/data - ./config:/app/config depends_on: - music-plus

restart: unless-stopped这个策略是必要的,服务偶发崩溃时能自动拉起。但要注意别写成always,否则你手动停服务调试的时候它又自己起来了,很烦。

6.2 关键参数取值与计算

几个参数看起来不起眼,取值不对体验差很多。我把自己的取值和推导过程列出来。

CACHE_TTL设 300 秒。这个值的约束来自两方面:太短会导致频繁回源,太长会导致歌单更新后界面迟迟不刷新。五分钟是个折中,实测下来用户基本感知不到延迟,回源压力也还好。

MODEL_TIMEOUT设 8 秒。模型响应时间波动很大,同样的输入有时两秒有时十几秒。设太短会频繁走兜底,设太长会让界面卡住。八秒的依据是:超过八秒还没返回的请求,用户大概率已经在怀疑程序卡死了,这时候走关键词兜底反而体验更好。

同步任务的并发数设 3。这个数字取决于源站的限流策略和你的网络状况。并发太高容易触发限流甚至临时封禁,太低又会让全量同步慢得离谱。三个并发加上每源独立的令牌桶,是我这里比较平衡的配置。

封面缓存的清理策略是按访问时间淘汰,保留最近三十天访问过的,总量上限设 2GB。这个上限是按平均每张封面 100KB、两万张算的。如果你的曲库更大,按比例往上调。

6.3 上线前的自检清单

部署完别急着用,先把这几项过一遍,能省掉后面一堆排查。

  • 曲库搜索返回的每一条,track_id都要能在曲目表里查到,对不上说明清洗逻辑有问题
  • 随机抽十个曲目,点播放,确认都能出声,这一步能发现地址过期和防盗链问题
  • 歌单同步跑两轮,第二轮之后曲目数不应该变化,变了说明去重没生效
  • 模型输入一句带情绪的话,确认返回的是合法 JSON 且能被 schema 校验通过
  • 手动停掉远端源,确认界面降级到本地缓存而不是报错
  • 检查日志里有没有重复的同步记录,有的话查一下定时任务的锁有没有生效

这份清单我每次改完代码都会跑一遍,看着麻烦,实际十分钟能过完,比出事之后翻日志强得多。

7. 踩坑记录:那些文档里不会写的问题

7.1 播放中断与封面空白

播放到一半中断,最常见的两个原因,一个是地址过期,一个是连接被复用了但服务端已经关了。前者前面说过,实时换取加缓冲阈值能解决。后者稍微隐蔽一点,表现是每首歌播到固定时长就断,像是被掐了一样。

我的解决办法是禁用长连接复用,每次播放新建连接,播完就关。代价是多一点握手开销,但换来了稳定。这个改动之后,中断率基本降到零。

封面空白的原因通常是跨域或者防盗链。浏览器加载图片时不会带页面的 Referer,很多源会因此拒绝。解决办法是把封面也走服务端转发并缓存到本地,前端只请求自己的域名。缓存这步顺便解决了另一个问题:源站封面加载慢时,界面不会一片空白等着。

7.2 歌单同步的幽灵条目

有段时间我的歌单里总会出现几条“幽灵”,点进去提示找不到,刷新一下又没了,过一会儿又冒出来。查了很久才定位到原因:增量同步时先删后插,中间有个短暂的空窗,前端正好在这个窗口里请求了列表。

解决办法是把删除和插入包在一个事务里,要么全成要么全不成,前端永远看不到中间态。SQLite 支持事务,写法上注意别在事务里做网络请求,把数据准备好再开事务,否则事务会持有锁很久,其他读请求全被堵住。

还有一个相关的问题是同步锁。定时任务如果上一轮还没跑完下一轮就开始了,两个进程同时写同一个歌单,会出现重复条目。我加了一个基于数据库的简易锁,任务开始前先写一条带时间戳的锁记录,结束时删除,启动时检查锁是否超时。这个方案不优雅,但对单机场景够用。

7.3 模型响应慢拖垮整个界面

最初我把模型调用放在了搜索接口的同步链路里,结果模型一慢,整个搜索页面就转圈。用户不知道后面发生了什么,只知道点了没反应。

改法是拆成两步:搜索接口先返回关键词匹配的结果,同时异步发起模型调用,模型返回后再通过轮询或者推送更新界面。这样用户点下去立刻有东西看,模型的结果晚一两秒补上,感知上顺畅很多。

另一个优化是把模型调用做成可开关的。配置文件里加一个enable_model开关,关掉之后所有请求走关键词匹配。调试的时候我经常关掉它,能快速判断问题出在模型层还是数据层。

现象可能原因排查动作处理方式
点了播放没反应地址过期或防盗链看服务端日志是否有 403换成实时换取加转发
歌单曲目数对不上去重规则过严或过松抽查几条记录的指纹调整时长阈值
模型答非所问提示词缺少约束打印模型原始输出补 schema 和示例
界面加载慢同步请求阻塞看接口耗时分布拆异步加开关
封面时有时无缓存未命中或跨域直接访问封面地址本地缓存加转发

8. 几条我自己趟出来的经验

这套东西我从动手到能用大概花了一个月,中间推倒重来过一次。回头看,最值得分享的不是某段代码,而是几个判断。

一是别追求一次接全所有源。我一开始贪多,同时接了四个,结果每个都半生不熟,出了问题不知道是哪个环节。后来砍到只留一个源,把它跑透,再逐步加,效率反而高得多。每加一个源,适配器的抽象是否合理会被检验一次,不合理就改,改到第三个源的时候基本就稳定了。

二是把“能用”和“好用”分开做。第一版我只求能搜到能播出来,界面丑、没缓存、模型也没接。这一版上线自己用了两周,把最痛的点记下来,第二版集中解决。如果一开始就想着全都要,很容易卡在某个细节上出不来。

三是日志要打得足够细。二次开发涉及多个外部依赖,出问题时没有日志基本等于瞎猜。我在每个适配器的请求前后都打了日志,包含请求参数、耗时、返回条数,出问题一眼能看出来是超时还是返回空。日志级别设成可配置,平时开 info,排查时开 debug。

最后提醒一句,音源的选择要守住边界,只用自己有权访问的内容,公开接口注意频率,别给对面添麻烦,也别给自己找麻烦。这个项目本质上是个聚合器,它的价值在于把分散的合法来源整理得顺手,不在于绕过什么。想清楚这一点,后面所有的技术决策都会变得简单。

返回列表