先来说说布景吧。前段时刻,我用Flutter断断续续的开发了一款桌面版的网易云音乐,制品仍是比较满意的。已完成的功能包含推荐音乐、私家FM、我喜欢的音乐、我的保藏、歌曲谈论、我的下载等。当然了,有关音乐的中心功能播映下载也都完成了。

前文引导:

根据Flutter开发的桌面版网易云音乐(一)

根据Flutter开发的桌面版网易云音乐(二)

一、MP3+JSON

问题就出在下载这儿。当时关于音频文件下载的完成方法是经过MP3+JSON的方法完成的。有人就要问了,为什么要这么完成?一起下载一个JSON文件又是什么鬼?别急,且听我渐渐道来。

首要,我开发的DreamMusic项目播映音乐时运用的是外链,它长这样"https://music.163.com/song/media/outer/url?id=$songId.mp3"。至于为何不用歌曲概况中的链接而直接运用外链,由于不是本文要点,我就不解释了。咱们仍是回到播映和下载上。经过这个音乐URL,咱们能够播映在线音乐,当然也能够经过这个URL直接下载MP3文件。它是一个不包含任何媒体信息的原始音频文件(这儿的媒体信息指的是歌名,歌手,专辑信息等)

现在咱们来想一下,在做音乐下载模块时需求做什么?

  1. 下载中进度展现。
  2. 下载完展现在列表上。
  3. 运用启动时能读取到音乐信息并展现出来。

根据Flutter的云音乐桌面版-音乐下载篇

其间1和2都是在运转中进行的,因此,音乐信息是能够直接从操作的音乐模型中获取到。那么,3应该怎么完成。我上面有说到,从URL中下载的音频文件是一个原始的MP3文件,不包含其他任何媒体信息。假如要完成运用启动时展现已下载音乐列表,并能够展现出歌曲封面,歌名,歌手,专辑等信息,那么咱们在下载的时分还需求一起存取一份音乐的媒体信息才行。因此才会有我第一版中说到的MP3+JSON方法。

根据Flutter的云音乐桌面版-音乐下载篇

上图中,那一串数字是云音乐平台的歌曲ID,其间的json文件就存储了歌曲的根本信息。用于在运用启动时加载歌曲信息。流程便是先找到JSON文件,读取JSON信息,转成已下载音乐的模型。写成代码便是下面这样。

String name =
            directory.uri.pathSegments[directory.uri.pathSegments.length - 2];
        final jsonFile = File("${directory.path}/$name.json");
        final exist = await jsonFile.exists();
        if (exist) {
          final content = await jsonFile.readAsString();
          final data = json.decode(content);
          if (data is Map<String, dynamic>) {
            final song = DownloadSongModel.fromJson(data);
            return song;
          }
        } else {
          debugPrint("[download]音乐[$name]json文件没有找到,删掉对应文件夹内容");
          await directory.delete(recursive: true);
        }

那么,这样写会有什么问题?

一个显而易见的问题便是音频文件和媒体信息分离了,不便办理。这儿或许有人就要说了,你这不是P话吗,媒体信息不分敞开莫非放MP3里?诶~还真能够,那便是运用ID3,这个我后面会说到。咱们仍是持续说MP3+JSON这种方法。还有没有其他问题?有的,比如用户能够随意独自删去JSON或MP3,或打开JSON文件,修正其间的信息,导致音乐和媒体信息不一致。其间随意修正JSON信息真是致命的。

那么我在之前是怎么处理上述问题的呢。咱们接着往下看。

针对删去文件

场景如下,用户打开着运用,然后直接操作下载文件夹,独自删去了JSON,或MP3或整个DreamMusic目录。假如咱们要做同步,那就需求监听这些文件的变动。Flutter文件体系为咱们提供了这个方法:

Stream<FileSystemEvent> watch(
      {int events = FileSystemEvent.all, bool recursive = false})

这个方法会监听文件的事情FileSystemEvent,并经过回调的方法告知咱们。所以,咱们能够很容易的写出下列代码,加入文件/文件夹删去监听。

/// 监听下载目录的改变,主要看文件有没有减少
  void _addFileDeleteObserverIfNeeded() async {
    if (!FileSystemEntity.isWatchSupported) {
      return;
    }
    if (hasDirectoryObserver) {
      return;
    }
    hasDirectoryObserver = true;
    final directory = Directory(fileCacheDirectorPath);
      if (!directory.existsSync()) {
        await directory.create();
      }
      final stream =
          directory.watch(events: FileSystemEvent.delete, recursive: true);
      stream.listen((event) async {
        // debugPrint("[download]$event");
        String path = event.path;
        if (path == fileCacheDirectorPath) {
          // 删去了整个下载目录
          _downloadedSongModels.clear();
          hasDirectoryObserver = false;
          debugPrint("[download]删去整个下载目录");
        } else {
            // 删去其间某个文件,这会导致信息不完整,因此直接悉数删去即可
          final lastSegment = Uri(path: path).pathSegments.last;
          final fileName = lastSegment.split('.').first;
          final songId = int.tryParse(fileName);
          if (songId != null) {
            final path = "$fileCacheDirectorPath/$songId";
            final dir = Directory(path);
            final exist = await dir.exists();
            if (exist) {
              await dir.delete(recursive: true);
            }
            final key = SongDownloadTask.createTaskId(songId);
            _downloadedSongModels.remove(key);
          }
          debugPrint("[download]删去文件$lastSegment,songId-$songId");
        }
        notifyListeners();
      });
      debugPrint("[download]开端监听$fileCacheDirectorPath目录的改变");
  }

逻辑处理很简略,假如用户单单删去了JSON或MP3,这就导致下载的音频文件不完整,所以,直接删去整个音乐文件夹就好**(这儿指的是上面说到的那一串歌曲ID的文件夹,不是最外层的DreamMusic目录)**。假如用户是删去了整个DreamMusic下载目录,那么不多说,悉数删去。

根据Flutter的云音乐桌面版-音乐下载篇

针对修正文件

很抱愧,我没做这个处理。因为我懒。其实是想出了更好的方法。那便是MP3+ID3的方法。

当然,我仍是能够提供下思路。原理仍是运用上述说到的监听文件修正的方法,这儿咱们监听修正JSON文件,一单文件的内容经过修正,体系会回调一个FileSystemModifyEvent对象给咱们,里边有个特点叫contentChanged,咱们判别下内容是否真的变了,变了就删掉,谁让你乱改下载文件的。当然最主要的原因是,文件体系没告知我改了什么,改变前和改变后又是什么,实在不好判别呀~

二、MP3+ID3

所以,我就抛弃持续在JSON上转牛角尖的想法,转而考虑是否能够将媒体信息放入到音频文件内部。所以,顺理成章的了解到了ID3(真的是问题不可怕,它是行进的动力)。

ID3维基百科。不了解ID3的能够先看看这是何物,有何效果。简略来说,ID3便是存在于音频文件中用于存放媒体信息的一段内容。它有自己的格局,现在流行的是ID3v2.3ID3v2.4版别。

了解了ID3根本的信息后,我就去找对应的三方库呀,看看有没有现成的能够协助我处理问题的id3解析库存在。很快的,我就找到了一个排名靠前的ID3库完成,id3。然后,我又别离实验了下自己下载的mp3文件和Mac版网易云音乐下载的mp3文件,里边都有些什么。发现公然有些东西。

下面是网易云下载的歌曲读取出来的ID3信息:

{
Version: v2.3.0,
Settings: Lavf57.25.100,
TPOS: 1,
Track: 12, Artist: 大壮, 
APIC: {mime: image/jpg, textEncoding: 0, picType: Other, description: , base64: iVBORw0K...}, 
Title: 为你我受冷风吹, 
Album: 大壮首张限量定制翻唱
}

而我自己下载的歌曲文件的ID3中没有任何媒体信息:

{
Version: v2.3.0, 
Settings: Lavf57.71.100
}

其实,在Mac上,咱们平时方便预览MP3文件时也会出现一些媒体信息,而这些信息便是mac桌面体系经过读取ID3显示出来的。

根据Flutter的云音乐桌面版-音乐下载篇

这下,咱们总算知道要将媒体信息存到哪里去了。那便是ID3中。可问题来了,id3这个三方库它不支撑修改啊。先不说它有没有bug,它不支撑修改啊

所以,我检查了ID3有关v1,v2的一切版别的官方信息。又在网上看了不少前辈的文章讲解,心里有了明悟,我为什么不自己写呢?

所以前后经历一个月时刻,一个支撑ID3解码编码的id3_codec总算完成了。而且还支撑ID3一切版别(编码这块v2.2不做支撑,因为根本没人用)。

有关id3_codec完成能够看我下列文章:

  • Flutter ID3解码完成- v1、v1.1、v2.2、v2.3
  • Flutter ID3解码完成-v2.4
  • Flutter下 ID3 编码完成-超详细

有了ID3的支撑,咱们就能够将媒体信息存入MP3中了,这样就处理了上面一切的问题。再也不忧虑用户乱改了(当然,假如有用户用编码器修正ID3信息,那我服了)。

咱们只需求将写入JSON的逻辑改成写入MP3原文件即可。看代码:

/// 将歌曲信息写入
  void _writeSongInfoAsync(DownloadSongModel song) async {
    if (_cacheMode == DownloadCacheMode.json) {
      // 略
    } else if (_cacheMode == DownloadCacheMode.id3) {
      final path = _generateSongId3SavePath(song.name);
      final file = File(path);
      bool exist = await file.exists();
      if (exist) {
        final bytes = await file.readAsBytes();
        final encoder = ID3Encoder(bytes);
        final al = json.encode(song.al.toJson());
        final resultBytes = encoder.encodeSync(MetadataV2_3Body(
            title: song.name,
            artist: song.authorNmae,
            album: song.al.name,
            userDefines: {
              "duration": song.time.toString(),
              "songId": song.songId.toString(),
              "ar": json.encode(song.ar.map((e) => e.toJson()).toList()),
              "al": al,
            }));
        file.writeAsBytes(resultBytes, mode: FileMode.write);
        debugPrint("[download]finish encode id3 info: ${song.name}");
      }
    }
  }

我下载了一首歌“是你”,桌面预览能直接看到歌曲标题等信息。假如要看详细点,咱们能够直接经过id3_codec的ID3Decoder,当然还能够运用其他东西,这儿我运用一款叫MediaInfo的东西,还看到了咱们自定义存储的araldurationsongId信息。

根据Flutter的云音乐桌面版-音乐下载篇

根据Flutter的云音乐桌面版-音乐下载篇

其实剩下的就没有啥悬念了。咱们经过id3_codec的ID3Decoder读取对应的信息,组装成模型展现出来即可。代码都在项目的download_manager.dart_loadSongModelFromPath下,感兴趣的自行取检查。

总结

本文主要讲解了音乐下载中存储的方法和期间遇到的问题,以及最后的处理方法。也简略介绍了ID3,它的运用,以及相应的编解码库id3_codec。本文涉及到的项目地址点我检查DreamMusic,感谢支撑。