前言
前面我发了一篇文章,向大家推荐了一个听歌软件:Listen(原文链接),我一般是在Edge浏览器里安装插件使用的,但是由于我是用的iPhone手机,然后Listen的源码我又一直编译不过去(吐槽下ReactNative)所以想自己撸一个听歌软件自己用.
技术选型
本人原本是做ios开发的,但是奈何objective-C比较难用,swift又一言难尽,swiftUI坑又比较多.在加上最近flutter比较火,而我之前也在demo上试了写了一下,感觉还不错,所以就打算使用flutter来写app了.
而且flutter还是跨平台的,还可以把android windows的一起给搞定了,嘿嘿~ 美滋滋
api接口
巧妇难为无米之炊,没有接口也没法进行下去啊. 我第一时间想到的是抓Listen1的包. 但是咪咕渠道的有个sid字段的值不知道是什么规则生成的,每次都不一样.导致我咪咕渠道的api迟迟搞不定.
不过我在github上找到了其它可用的api(链接地址),谢谢这位大哥了嘿嘿.
开始撸代码
先撸一个公共类,作为解析各平台的解析协议
class SongModel { Map<String, dynamic> originMap = {}; //原始数据 String channel = ""; //渠道 //资源id String resourceId() { return ""; } //歌曲名 String songName() { return ""; } //歌曲图片 Future<String> songImg() { return Future.value(""); } //歌手名 String singer() { return ''; } //专辑名 String album() { return ""; } //播放地址 Future<String> playUrl() async { return ""; } //获取歌词 Future<String> lyric() async { return ""; }}
特定平台的api实现
import 'package:dio/dio.dart';import 'package:listen_flutter/api/migu/model/models/migu_models.dart';class MiguApi { //搜索歌曲 static Future<MiguPageListModel> search(String? text) async { var searchText = "jay"; if (text?.isNotEmpty == true) { searchText = text!; } var path = 'http://iecoxe.top:5000/v1/migu/search?key=$searchText'; var response = await Dio().get(path); return MiguPageListModel(response.data); } //获取播放地址 static Future<String> getPlayUrl(MiguSongModel song) async { var copyrightId = song.originMap['copyrightId']; var path = 'http://iecoxe.top:5000/v1/migu/song?cid=$copyrightId'; var response = await Dio().get(path); var map = response.data as Map; return map['lyric']; } //获取播放地址 static Future<String> getLyric(MiguSongModel song) async { var copyrightId = song.originMap['copyrightId']; var path = 'http://iecoxe.top:5000/v1/migu/lyric?cid=$copyrightId'; var response = await Dio().get(path); var map = response.data as Map; return map['lyric']; }}
model类做好解析
import 'package:hive/hive.dart';import 'package:listen_flutter/api/migu/apis/migu_api.dart';import 'package:listen_flutter/models/page_response.dart';import 'package:listen_flutter/models/song_model.dart';part 'migu_models.g.dart';@HiveType(typeId: 0)class MiguSongModel extends HiveObject implements SongModel { @override @HiveField(1) Map<String, dynamic> originMap; //原始数据 @override @HiveField(2) String channel = "migu"; MiguSongModel(this.originMap); //资源id @override String resourceId() { return "migu" + originMap['id']; } //歌曲名 @override String songName() { return originMap['songName']; } //歌曲图片 @override Future<String> songImg() { return Future.value(originMap['cover']); } //歌手名 @override String singer() { return originMap['singerName']; } //专辑名 @override String album() { return originMap['albumName']; } @override Future<String> playUrl() { return Future.value(originMap['mp3']); } @override Future<String> lyric() { return MiguApi.getLyric(this); }}class MiguPageListModel implements PageResponse { late Map<String, dynamic> originMap; //原始数据 MiguPageListModel(this.originMap); @override List<MiguSongModel> list() { var list = originMap["musics"] as List; return list.map((e) => MiguSongModel(e)).toList(); } @override bool hasNextPage() { return true; } @override int nextPage() { return 1; }}
最后就是构建UI,解析歌词啥的了
// import 'package:just_audio/just_audio.dart';import 'dart:math';import 'package:audio_service/audio_service.dart';import 'package:audioplayers/audioplayers.dart';import 'package:get/get.dart';import 'package:get_storage/get_storage.dart';// import 'package:just_audio/just_audio.dart';import 'package:listen_flutter/models/song_model.dart';import 'package:listen_flutter/songlist/songlist.dart';import 'audio_handle.dart';class PlayerManager { var currentSong = Rx<SongModel?>(null); //当前播放歌曲 var playerState = Rx<PlayerState>(PlayerState.PAUSED); //当前播放状态 var position = Duration.zero.obs; var duration = Duration.zero.obs; var isInit = false.obs; var queueState = 0.obs; //0 顺序播放 1:随机播放 2:单曲循环 AudioPlayer audioPlayer = AudioPlayer(); //播放器 var audioHandler = Get.find<MyAudioHandler>(); factory PlayerManager() => _getInstance(); static PlayerManager get instance => _getInstance(); static PlayerManager? _instance; PlayerManager._internal() { audioPlayer.onPlayerStateChanged.listen((event) { playerState.value = event; audioHandler.playbackState.add(audioHandler.playbackState.value .copyWith(playing: event == PlayerState.PLAYING)); }); audioPlayer.onAudioPositionChanged.listen((event) { position.value = event; audioHandler.playbackState.add( audioHandler.playbackState.value.copyWith(updatePosition: event)); }); audioPlayer.onDurationChanged.listen((event) { duration.value = event; }); audioPlayer.onPlayerCompletion.listen((event) { next(); }); queueState.listen((value) { GetStorage().write("queueState", value); }); queueState.value = GetStorage().read<int>("queueState") ?? 0; var resourceId = GetStorage().read<String>("currentSong"); if (resourceId == null) { SongListManager.playList() .then((value) => currentSong.value = value.first); } else { SongListManager.getSong(resourceId) .then((value) => currentSong.value = value); } } static PlayerManager _getInstance() { _instance = _instance ?? PlayerManager._internal(); return _instance!; } play(SongModel songModel) async { SongListManager.addSong(songModel); try { var url = await songModel.playUrl(); int state = await audioPlayer.play(url); audioPlayer.getDuration(); currentSong.value = songModel; GetStorage().write("currentSong", currentSong.value!.resourceId()); addSong(); isInit.value = true; } catch (e) { next(); } } pause() { if (currentSong.value == null) { return; } audioPlayer.pause(); } resume() async { if (currentSong.value == null) { return; } if (isInit.value) { audioPlayer.resume(); } else { play(currentSong.value!); } } next() async { if (currentSong.value == null) { return; } var list = await SongListManager.playList(); if (queueState.value == 0) { var index = list.indexOf(currentSong.value!); var i = index + 1; if (index >= list.length - 1) { i = 0; } var song = list.elementAt(i); currentSong.value = song; play(song); } else if (queueState.value == 1) { var random = Random(); var index = random.nextInt(list.length); var song = list.elementAt(index); currentSong.value = song; play(song); } else { play(currentSong.value!); } } previous() async { if (currentSong.value == null) { return; } var list = await SongListManager.playList(); if (queueState.value == 0) { var index = list.indexOf(currentSong.value!); var i = index - 1; if (index <= 0) { i = list.length - 1; } var song = list.elementAt(i); currentSong.value = song; play(song); } else if (queueState.value == 1) { var random = Random(); var index = random.nextInt(list.length); var song = list.elementAt(index); currentSong.value = song; play(song); } else { play(currentSong.value!); } } seek(Duration duration) async { if (currentSong.value == null) { return; } await audioPlayer.seek(duration); } addSong() async { var songModel = currentSong.value; if (songModel == null) { return; } var img = await songModel.songImg(); var album = songModel.album(); var title = songModel.songName(); // var duration = await audioPlayer.getDuration(); var duration = 200000; var item = MediaItem( id: songModel.resourceId(), album: album, title: title, artist: '', duration: Duration(milliseconds: duration), artUri: Uri.parse(img), ); // audioHandler.playMediaItem(item); audioHandler.mediaItem.add(item); var playbackState = PlaybackState( // Which buttons should appear in the notification now controls: [ MediaControl.skipToPrevious, MediaControl.play, MediaControl.pause, MediaControl.stop, MediaControl.skipToNext, ], // Which other actions should be enabled in the notification systemActions: const { MediaAction.seek, MediaAction.seekForward, MediaAction.seekBackward, }, // Which controls to show in Android's compact view. androidCompactActionIndices: const [0, 1, 3], // Whether audio is ready, buffering, ... processingState: AudioProcessingState.ready, // Whether audio is playing speed: 1.0, // The current queue position queueIndex: 0, playing: true, ); audioHandler.playbackState.add(playbackState); }}
哎,我为啥要在头条上发这种东西呢?这种东西真的有人看吗?
不管了,发几张成品图:
顺便搞了个mac版的,上班好好用 嘿嘿
虽然界面比较简陋,但是听歌的搞这么花里胡哨的干啥呢,是吧?
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!