最近,在优化一个自己写的音乐播放器。主要目的是回顾、归纳,并希望能够写出一个属于自己的common lib。今天,主要是关于在线音乐API的一些分析结果。此次,主要分析的是歌词、专辑部分。在线搜索音乐、热门音乐及mp3的下载等,会在PART 2.2进行补充。
原始API来源于网络资料,部分是后面使用个人补充的。主要包括百度API、腾讯API及歌词迷API,其中只有歌词迷的API是官方正式发布的。三个API都有着各自的优点、缺点,如下:
(1) 百度API,请求方式稳定,速度快,资源最多,获取歌词比较准确;但是数据结构相对繁杂些,每行的歌词长度差异比较大。
(2) 腾讯API,请求方式相对稳定,速度快,资源较多,准确度高,每行的歌词长度相当;但JSON(Xml相对正常)数据结构并不完全标准,解析麻烦, 专辑图片封面(约50KB|500 x 500 像素)较大。
(3)歌词迷API,有官方正式API,使用简单,专辑封面相对小些(约10KB|185 x 160 像素);遗憾的是资源相对少,尤其在最新的资源方面,有点慢。
提醒:以上全是个人开发的总结,并没有完整体系性的验证。
如专辑封面大小问题,视乎个人开发需要而定,如果需要大图片,腾讯的保真度高,如果需要小图片,无疑歌词迷更好些。
本人在歌词方面使用的腾讯API,专辑封面使用的是歌词迷API。
整个实现思路比较明确,大体上的类图设计如下:
直接使用LyricLoader的loadLyric()方法进行歌词下载,loadLyric()方法封装了具体的处理逻辑,具体实现下载,由子类实现IDownload<Lyric>接口。摘取部分代码:
/** * 歌词助手 * * @author Osmondy * */ public abstract class LyricLoader implements IDownload<Lyric> { public LyricLoader(String name) { } /** * 获取网络请求歌词地址 * * @param music * @return */ public abstract String getServerLyricUrl(Music music); /** * 返回本地存储歌词的路径 * * @param music * @return */ protected String getLocalLyricPath(String songname, String singername) { } /** * 返回歌词, Step1: 本地歌词目录加载; Step2: 网络下载. * * @param music * @return */ public Lyric loadLyric(Music music) { if (TextUtils.isEmpty(music.getArtist()) || TextUtils.isEmpty(music.getTitle())) { Log.W(TAG, "Empty aritst or title, can't find lyric."); return null; } Lyric lyric = null; String localPath = getLocalLyricPath(music.getTitle(), music.getArtist()); File file = new File(localPath); if (file.exists()) { // 本地存在歌词文件, 直接加载此歌词. Log.D(TAG, "Loading lyric from local path."); try { lyric = loadLocalLyric(localPath); if (lyric != null) { lyric.setSongname(music.getTitle()); lyric.setSingername(music.getArtist()); Log.I(TAG, "Load local lyric finished. Lyric: " + lyric); } } catch (IOException e) { if (e instanceof FileNotFoundException) { Log.W(TAG, "Lyric not found."); } else { e.printStackTrace(); } } return lyric; } String requestUrl = getServerLyricUrl(music); if (!TextUtils.isEmpty(requestUrl)) { Log.D(TAG, "---------- Download lyric start ----------"); try { lyric = download(requestUrl, localPath); } catch (HttpRequestException e) { e.printStackTrace(); } Log.D(TAG, "---------- Download lyric end ----------"); return lyric; } Log.W(TAG, "Not found a correct server lyric path."); return null; } /** * 保存歌曲文件, 默认保存至{@link AppConfig#DIRECTORY_LYRIC}, 子类可自行重写保存至其它路径. 保存时, * 先保存成*.lrc.tmp, 下载及保存成功后, 再重命名为*.lrc. 防止异常或停止下载歌词, 下次无法再次下载. * * @param is * @param music * @return */ protected boolean saveLyric(InputStream is, String savePath) { } /** * 返回指定地址的歌词文件 * * @param path * @return * @throws IOException */ public Lyric loadLocalLyric(String path) throws IOException { } }
抽象类LyricLoader提供了对歌词保存、加载的默认处理方式,子类可以自行重写saveLyric()、loadLocalLyric()定义自己的处理方式。子类的实现以百度API为例,它使用的是父类LyricLoader提供的默认实现。
/** * 歌词来源于Baidu * * @author Osmondy * */ public class BaiduLyricHelper extends LyricLoader { private static final String TAG = "BaiduLyricHelper"; /** * 歌曲信息请求地址 */ protected static final String SONGINFO_BASE_URL = "http://box.zhangmen.baidu.com/x"; /** * 歌词文件请求地址 */ protected static final String LYRIC_BASE_URL = "http://box.zhangmen.baidu.com/bdlrc"; public BaiduLyricHelper() { super("BaiDu"); } @Override public Lyric download(String requestUrl, String savePath) throws HttpRequestException { } @Override public String getServerLyricUrl(Music music) { } }
比较完整的代码已经上传至github:https://github.com/osmondy/LyricApi
原始API如下:
(1) 百度API
歌曲信息请求地址:http://box.zhangmen.baidu.com/x?op=12&count=1&title=歌词名称$$歌手名称$$$$
歌词信息请求地址:http://box.zhangmen.baidu.com/bdlrc/歌词ID除以100/歌词ID.lrc
/** * 返回请求歌词的地址, 通过 SongInfo生成最终可请求到歌词文件的地址. </br> * * @param songInfo * @return */ protected String getServerLyricUrlBySongInfo(SongInfo songInfo) { int lrcid = songInfo.getLrcid(); int postfix = lrcid / 100; StringBuffer sb = new StringBuffer(); sb.append(LYRIC_BASE_URL); sb.append("/"); sb.append(postfix); sb.append("/"); sb.append(lrcid); sb.append(".lrc"); return sb.toString(); } @Override public String getServerLyricUrl(Music music) { if (TextUtils.isEmpty(music.getTitle()) || TextUtils.isEmpty(music.getArtist())) { return null; } //protected static final String SONGINFO_BASE_URL = "http://box.zhangmen.baidu.com/x?op=12&count=1&title="; Log.D(TAG, "Songname: " + music.getTitle() + ", Singername: " + music.getArtist()); StringBuffer sb = new StringBuffer(); try { sb.append(SONGINFO_BASE_URL); sb.append("?"); sb.append("op=12"); sb.append("&"); sb.append("count=1"); sb.append("&"); sb.append("title="); sb.append(URLEncoder.encode(music.getTitle(), "utf-8")); sb.append("$$"); sb.append(URLEncoder.encode(music.getArtist(), "utf-8")); sb.append("$$$$"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return sb.toString(); }
(2) 腾讯API
编码并非是UTF-8,而是GBK(gb2312)。
歌曲信息请求地址:http://qqmusic.qq.com/fcgi-bin/qm_getLyricId.fcg?name=连哭都是我的错&singer=东来东往&from=qqplayer
歌词请求地址:http://music.qq.com/miniportal/static/lyric/歌曲ID求余100/歌曲ID.xml
专辑封面请求地址:http://imgcache.qq.com/music/photo/album/专辑ID求余100/albumpic_专辑ID_0.jpg
/** * 返回请求歌词的地址, 通过 SongInfo生成最终可请求到歌词文件的地址. </br> * 请求地址格式: http://music.qq.com/miniportal/static/lyric/67/183767.xml * * @param songInfo * @return */ protected String getServerLyricUrlBySongInfo(SongInfo songInfo) { String id = songInfo.getId(); if (!StringUtils.isNumeric(id)) { return null; } int postfix = Integer.parseInt(id) % 100; StringBuffer sb = new StringBuffer(); sb.append(LYRIC_BASE_URL); sb.append("/"); sb.append(postfix); sb.append("/"); sb.append(id); sb.append(".xml"); return sb.toString(); } /** * 返回请求歌曲信息的地址. * 请求地址格式: http://qqmusic.qq.com/fcgi-bin/qm_getLyricId.fcg?name=连哭都是我的错&singer=东来东往&from=qqplayer */ @Override public String getServerLyricUrl(Music music) { if (TextUtils.isEmpty(music.getTitle()) || TextUtils.isEmpty(music.getArtist())) { return null; } Log.D(TAG, "Songname: " + music.getTitle() + ", Singername: " + music.getArtist()); StringBuffer sb = new StringBuffer(); try { sb.append(SONGINFO_BASE_URL); sb.append("?"); sb.append("name=" + URLEncoder.encode(music.getTitle(), "gbk")); sb.append("&"); sb.append("singer=" + URLEncoder.encode(music.getArtist(), "gbk")); sb.append("&"); sb.append("from=qqplayer"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return sb.toString(); }
(3)歌词迷API
直接提供官方地址:http://api.geci.me/en/latest/
歌词请求地址:http://geci.me/api/lyric/:歌曲名/:歌手名
专辑封面请求地址:http://geci.me/api/cover/:专辑ID
@Override public String getServerLyricUrl(Music music) { if (TextUtils.isEmpty(music.getTitle()) || TextUtils.isEmpty(music.getArtist())) { return null; } Log.D(TAG, "Songname: " + music.getTitle() + ", Singername: " + music.getArtist()); StringBuffer sb = new StringBuffer(); try { sb.append(LYRIC_BASE_PATH); sb.append("/"); sb.append(URLEncoder.encode(music.getTitle(), "utf-8")); sb.append("/"); sb.append(URLEncoder.encode(music.getArtist(), "utf-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return sb.toString(); } /** * 返回歌曲专辑信息请求地址 * * @param albumId * @return */ public String getServerAlbumUrl(String albumId) { return ALBUM_BASE_PATH + "/" + albumId; }
最后,附上腾讯如何获取新歌榜及总榜的请求。
新歌榜:http://music.qq.com/musicbox/shop/v3/data/hit/hit_newsong.js
总榜:http://music.qq.com/musicbox/shop/v3/data/hit/hit_all.js