Android:本地音樂檔案寫入專輯圖
技術標籤:kotlinAndroidAndroid專輯圖kotlinjavajaudiotagger
網上搜索這個Android寫入專輯圖的資料少之又少,都是讀取的,這裡我找到了一個方法,然後自己做了個寫入專輯圖的小工具,有需要的童鞋可以參考一下。
先上效果圖:
依賴第三方庫:jaudiotagger(java)、Okhttp
demo語言:Kotlin
不會kotlin的小夥伴別慌,我也是邊學kotlin先寫的這個demo,看過kotlin基礎語法的小夥伴應該還是比較好理解的。
核心寫入程式碼:
//path:本地音樂檔案路徑;picFile:本地圖片檔案(網路圖片的話我這邊的做法是儲存到本地再用路徑new一個檔案) fun writeTag(path: String?, picFile: File?) { val mp3File = MP3File(path) if (mp3File.hasID3v2Tag()) { mp3File.run { val artWork = ArtworkFactory.createArtworkFromFile(picFile) iD3v2Tag.setField(artWork) save() } } }
三步:
- 首先我們要獲取本地所有音樂檔案
- 然後點選某個item為它搜尋專輯圖(這裡用網易雲音樂搜尋API)
- 最後就是寫入
儲存許可權什麼的這裡就不寫了,可以參考demo或者自己經驗寫。下面是獲取本地音樂檔案列表:
/** * 得到媒體的音樂檔案列表 */ private fun getMusicList(): MutableList<SongBean> { val list: MutableList<SongBean> = ArrayList() val cursor: Cursor? = this.contentResolver.query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.AudioColumns.IS_MUSIC ) if (cursor != null) { var song: SongBean while (cursor.moveToNext()) { var singer = "" var name = "" val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)) val split = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)) .split("-") val size = split.size when { size == 1 -> { name = split[0] } size == 2 -> { singer = split[0] name = split[1] } size > 2 -> { name = split.last() for (i in 0 until size-1){ singer += " "+split[i] } } } song = SongBean(name.replace(".mp3", "").trim(), singer.trim(), path) list.add(song) } cursor.close() } list.reverse() return list }
這個SongBean是裝音樂的實體,裡面有歌名、歌手、路徑(搜尋歌曲的話這個路徑用來儲存圖片連結):
class SongBean() :Parcelable{ var name:String? = null var singer:String? = null var path:String? = null constructor(parcel: Parcel) : this() { name = parcel.readString() singer = parcel.readString() path = parcel.readString() } constructor(name: String?, singer: String?, path: String?) : this(){ this.name = name this.singer = singer this.path = path } override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(name) parcel.writeString(singer) parcel.writeString(path) } override fun describeContents(): Int { return 0 } companion object CREATOR : Parcelable.Creator<SongBean> { override fun createFromParcel(parcel: Parcel): SongBean { return SongBean(parcel) } override fun newArray(size: Int): Array<SongBean?> { return arrayOfNulls(size) } } fun getArtBitmap(): Bitmap? { val myRetriever = MediaMetadataRetriever() try { myRetriever.setDataSource(path) // the URI of audio file } catch (e: Exception) { Log.e("error", "getArtBitmapError: $e") return null } val artwork = myRetriever.embeddedPicture return if (artwork != null) { BitmapFactory.decodeByteArray(artwork, 0, artwork.size) } else { null } } }
這個SongBean裡面實現了序列化,因為列表item點選之後我們要把這個item的資料傳到下一個介面。然後最後面這個getArtBitmap方法是原生的用來獲取歌曲自帶專輯圖的方法,待會列表展示的時候要用到。
初始化介面的時候,把歌曲列表給到RecyclerView,展示給使用者:
item的佈局很簡單,就垂直並列的三個TextView加上最右邊的ImageView:(三個TextView的id分別是tv1、tv2、tv3,圖片id是img)
列表的介面卡:
class MyListAdapter(private val data: MutableList<SongBean>) :
RecyclerView.Adapter<MyListAdapter.MyViewHolder>() {
private val TAG = "MyListAdapter"
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.adapter_item,parent, false
)
)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.itemView.apply {
tv1.text = data[position].name
tv2.text = data[position].singer
tv3.text = data[position].path
if(tv3.text.startsWith("http")){
Log.i(TAG, "load_picture:${data[position].path}")
img.visibility = View.VISIBLE
img.load(data[position].path){
crossfade(true)
}
}else{
val artBitmap = data[position].getArtBitmap()
if(artBitmap != null){
img.visibility = View.VISIBLE
img.setImageBitmap(artBitmap)
}else{
img.visibility = View.GONE
}
}
setOnClickListener{
listener?.setOnItemClickListener(data[position],position)
}
}
}
override fun getItemCount(): Int = data.size
private var listener:OnListener? = null
fun setListener(listener:OnListener){
this.listener = listener
}
interface OnListener{
fun setOnItemClickListener(bean: SongBean,position: Int)
}
fun setNewData(newData: List<SongBean>){
data.clear()
data.addAll(newData)
notifyDataSetChanged()
}
}
給recyclerView賦值:
allList = getMusicList()
recycler.adapter = adapter
recycler.layoutManager = LinearLayoutManager(this)
adapter.setListener(object : MyListAdapter.OnListener {
override fun setOnItemClickListener(bean: SongBean,position:Int) {
[email protected] = position
val intent = Intent([email protected], MatchActivity::class.java)
intent.putExtra("data", bean)
startActivityForResult(intent,1000)
}
})
adapter.setNewData(allList)
這裡用startActivityForResult是因為待會在下一個介面如果寫入專輯圖成功的話,返回出來需要更新這個item的右邊圖片,所以這個頁面我把點選的position儲存到全域性變數裡面。
到達下一個介面了之後,就搜尋歌手+歌名,用的oktttp。
網易雲歌曲搜尋api:
http://music.163.com/api/search/pc?csrf_token=hlpretag=&hlposttag=&s=關鍵詞&type=1&offset=頁數從0開始&total=true&limit=每頁數量
fun search(name: String?, singer: String?) {
btn_search.isEnabled = false
btn_search.text = "搜尋中……"
if (TextUtils.isEmpty(name) && TextUtils.isEmpty(singer)) {
Toast.makeText(this, "請輸入歌曲資訊", Toast.LENGTH_SHORT).show()
return
}
val word = singer?.trim() + name?.trim()
val url = "http://music.163.com/api/search/pc?csrf_token=hlpretag=&hlposttag=&s=$word&type=1&offset=0&total=true&limit=20"
Log.i(TAG, "search: $url")
val okHttpClient = OkHttpClient()
val request: Request = Request.Builder()
.url(url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
.build()
val call = okHttpClient.newCall(request)
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
//失敗處理
btn_search.isEnabled = true
btn_search.text = "搜尋"
Toast.makeText(applicationContext, "請求失敗", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call, response: Response) {
val jsonObject = JSONObject(response.body()!!.string())
runOnUiThread {
btn_search.isEnabled = true
btn_search.text = "搜尋"
val code: Int = jsonObject.getInt("code")
if (code == 200) {
val jsonArray = jsonObject.getJSONObject("result")
.getJSONArray("songs")
val list = ArrayList<SongBean>()
for (i in 0 until jsonArray.length()) {
val item = jsonArray.get(i) as JSONObject
val singerArr = item.getJSONArray("artists")
var singerStr = ""
for (j in 0 until singerArr.length()){
val singerObj = singerArr.get(j) as JSONObject
singerStr += singerObj.get("name")
singerStr += " "
}
list.add(
SongBean(
item.getString("name"),
singerStr,
item.getJSONObject("album").getString("picUrl")
)
)
}
adapter.setNewData(list)
if (adapter.itemCount == 0) {
Toast.makeText(applicationContext, "什麼都沒有搜到", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(applicationContext, "伺服器資料錯誤", Toast.LENGTH_SHORT).show()
}
}
}
})
}
設定列表的點選事件,確定寫入彈框防誤觸:(這裡我以為寫入是個耗時操作,實際現象並不會耗時,秒寫入)
adapter.setListener(object : MyListAdapter.OnListener {
override fun setOnItemClickListener(bean: SongBean,position:Int) {
val url = bean.path
val img = ImageView(applicationContext)
img.load(url)
alert("寫入專輯圖", "確定寫入嗎?") {
negativeButton("下次一定") {}
positiveButton("寫入") {
// indeterminateProgressDialog("正在寫入")
val request = ImageRequest.Builder([email protected])
.data(bean.path)
.target { drawable ->
// Handle the result.
val bitmapDrawable: BitmapDrawable = drawable as BitmapDrawable
val bitmap: Bitmap = bitmapDrawable.bitmap
val filePath = "${filesDir.absoluteFile}/temp.jpg";
Log.i(TAG, "filePath: $filePath")
val file = bitmapToFile(
filePath,
bitmap, 80
)
Log.i(TAG, "BitmapFilePath: ${file?.absoluteFile}")
writeTag(path, File(filePath))
}
.build()
val disposable = imageLoader.enqueue(request)
}
}.show()
}
})
這裡用到的儲存bitmap到本地,還有寫入專輯圖的方法
/**
* bitmap儲存為file
*/
@Throws(IOException::class)
fun bitmapToFile(
filePath: String,
bitmap: Bitmap?, quality: Int
) : File? {
if (bitmap != null) {
val file = File(
filePath.substring(
0,
filePath.lastIndexOf(File.separator)
)
)
if (!file.exists()) {
file.mkdirs()
}
val bos = BufferedOutputStream(
FileOutputStream(filePath)
)
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, bos)
bos.flush()
bos.close()
return file
}
return null
}
/**
* 寫入專輯圖
*/
fun writeTag(path: String?, picFile: File?) {
val mp3File = MP3File(path)
if (mp3File.hasID3v2Tag()) {
mp3File.run {
val artWork = ArtworkFactory.createArtworkFromFile(picFile)
iD3v2Tag.setField(artWork)
save()
Toast.makeText(applicationContext, "寫入完成", Toast.LENGTH_SHORT).show()
setResult(1001)
}
}else{
Toast.makeText(applicationContext, "此歌曲沒有ID3v2Tag", Toast.LENGTH_SHORT).show()
}
}
寫入專輯圖我也是百度搜的方法,這裡判斷了本地音樂檔案需要有ID3v2Tag才能寫入,我測試寫了ID3v1Tag.setField確實沒有作用,我自己的本地歌曲確實有那麼幾首是這樣的,如果有知道的大佬可以指點一二。不過大部分歌曲都是有這個id3v2tag的。
至此所有核心方法都已給出。具體程式碼可以參照demo,程式碼我已上傳GitHub:https://github.com/xaEHu/Mp3TagTool