Flutter写个Nas音乐播放器(一)
2023-01-05 22:40 ≈ 1.5k字 ≈ 5分钟

因为最近两个月比较无聊,在家里各种折腾我的老NAS,搭了一个音乐服务器,但是找不到特别满意的播放器,在桌面端还有个Sonixd颜值比较高,而且没有花里胡哨的功能,但是手机端的话就不甚满意,只有substreamer可以用,但是不太好看。

最关键的是这两个播放器都不支持歌词,而且还不能模糊查找以及简繁体查找。比如我想听周杰伦的退后,但是我挂搜出来的名字是退後,那我搜索的时候还需要切到繁体去才能搜出来,这就比较烦了。

我第一反应是去Github上找找有没有现成的多端轮子,但是看得上的都是本地播放的,或者有个仿网易云音乐播放器是通过网易云的API搞的,随时可能会被断掉不说,也不支持连Navidrome或者Subsonic的API。实在是找不到了,一咬牙一跺脚自己搞一个吧!

Subsonic API

Navidrome本身有原生的API,但是不多,而且作者正在计划重写,不过好在他针对Subsonic的API做了几乎全部支持,所以即使Navidrome本身没有API文档,看着Subsonic的文档也能放到Postman上试试看

验证服务器

1672924014867

猛看稍微比较懵逼,其实很简单,验证分两种:要么传入密码,要么传入加密后的(密码+六位盐值)再加上六位盐值

Ping一下服务器进行测试(使用dio: ^4.0.6可以省却不少操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
try {
var response = await Dio().get(
_baseUrl +
'rest/ping?v=0.0.1&c=xiumusic&f=json&u=' +
_username + //v是客户端版本号,c是客户端的名称,u是用户名,p是密码
'&p=' +
_password,
);
print(response);
if (response.statusCode == 200) {
//这里需要多做一步,就是即便返回code200也别高兴,还要验证subsonic服务器的反馈
Map _value = response.data['subsonic-response'];
String _status = _value['status'];
//正常的状态是ok,我自己懒得抛错了,自己在debug里面查,产品化的时候肯定要抛出去,不过嘛,嘿嘿
if (_status == 'failed') {
return false;
} else {
return true;
}
} else {
return false;
}
} catch (e) {
print(e);
return false;
}

做完两步验证服务器和Subsonic的api都没有问题了就可以保存服务器信息了,后面的api都使用盐值来获取,其实无所谓啦,都是免费的

加密登陆

  1. 利用Rondom写一个随机生成6位字符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    String generateRandomString() {
    final _random = Random();
    const _availableChars =
    'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
    final randomString = List.generate(6,
    (index) => _availableChars[_random.nextInt(_availableChars.length)])
    .join();

    return randomString;
    }
  2. 然后拼接之后用MD5加密

把这两个字符保存起来用于后面API的使用

获取目录

Subsonic里面的目录是这样自上而下分级的

  • 音乐文件夹
  • 音乐人文件夹
  • 专辑
  • 歌曲

不过Navidrome好像就只有一个音乐文件夹,所以我直接用了第二级的getIndexes来做我的目录主列表,然后根据音乐人ID再下到专辑列表

1672925500580

播放音乐

使用ValueNotifier来进行变量监听,不要轻易使用provider: ^6.0.5 ,虽然说真的方便但是全局变量总会出现各种各样的问题,尤其是当你依赖他之后,想要再把代码改回去,那工作量简直想让你重新写一遍!

其实ValueNotifier使用起来也挺方便的,他后面可以带<int>参数来指定值的类型,也可以自定义监控数据类型,我是根据这篇文章来操作的

  1. 定义数据监控器

    ValueNotifier<String> activeSongValue = ValueNotifier<String>("1");

    理论上这个放在哪个文件里面都可以,不过建议放到统一的全局变量定义文件里便于管理,我这里传入歌曲的id,给一个默认值做过滤,如果是"1"就不调用接口获取歌曲信息了(flutter2.12后的Null safety强制要求,刚开始不适应,现在觉得挺好的)

  2. 改变数据的值

    activeSongValue.value = _tem["id"];

    直接使用就可以,这样看来其实比provider还要简单

  3. 监听数据的变化

    使用ValueListenableBuilder构造来构造监听部分

    1
    2
    3
    4
    5
    6
    7
    8
    return ValueListenableBuilder<String>(
    valueListenable: activeSongValue, //监控器
    builder: ((context, value, child) {
    if (value != "1") { //改变值
    _setAudioSource(value); //直接使用id进行查询等后续工作
    }

    return Container()...
  4. 获取歌曲信息

    有了歌曲id之后,就可以使用getSong来获取音乐信息,使用getCoverArt来获取专辑图片,再使用stream来获取流文件

  5. 播放音乐流

    使用just_audio: ^0.9.31 可以大大加速开发进度,它是排名第二的dependencies,第一是audioplayers,不过正好这几天他更新了版本,官方文档的地址给搞没了,这就叫做缘分

    使用起来也极其简单,如果不管按钮滑块之类的控制展现,从点击音乐到开始播放就三行代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //定义组件
    final _player = AudioPlayer();
    //拼接流的url
    String _url = await getSongStreamUrl(_id);
    //获取流并播放
    try {
    await _player.setAudioSource(AudioSource.uri(Uri.parse(_url)));
    _player.play();
    } catch (e) {
    print("Error loading audio source: $e");
    }

以上就是目前的进度,包含了桌面端的服务器测试、然后目录进到歌曲级,然后播放音乐。

后面还有很多工作,不过Flutter是我从1.x的版本就在关注的,一度吐槽华为的工程师不用Flutter就是个废物,Google更是不遗余力的投入,我相信会很容易完成(写桌面端的时候就留意移动端的适配了,都写完估计简单调整一下移动端就可以了,目前只做功能)

这里是Github的地址,愿意下来玩也挺好,不过仅用于开发,离正常用还差很远。