本地播放

This commit is contained in:
ocean 2024-05-27 14:29:44 +08:00
parent 42c95b9a02
commit 2a151145a2
13 changed files with 147 additions and 65 deletions

View File

@ -41,7 +41,7 @@ class LaunchActivity : BaseActivity() {
}
private fun toMainActivity() {
startActivity(Intent(this, MainActivity::class.java))
startActivity(Intent(this, PrimaryActivity::class.java))
finish()
}
}

View File

@ -14,11 +14,15 @@ import android.view.View
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager
import relax.offline.music.R
import relax.offline.music.media.MediaControllerManager
@ -26,13 +30,17 @@ import relax.offline.music.sp.AppStore
import relax.offline.music.util.LogTag
import relax.offline.music.view.MusicPlayerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import relax.offline.music.bean.OfflineBean
import relax.offline.music.innertube.Innertube
import relax.offline.music.util.FileSizeConverter
@UnstableApi
@OptIn(UnstableApi::class)
abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope(),
LifecycleOwner {
private var playerListener: Player.Listener? = null
@ -220,4 +228,18 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
return text.split("\n\n")[0]
}
fun insertOfflineData(mediaItem: MediaItem) {
CoroutineScope(Dispatchers.IO).launch {
val bean = OfflineBean(
videoId = mediaItem.mediaId,
title = mediaItem.mediaMetadata.title.toString(),
name = mediaItem.mediaMetadata.artist.toString(),
thumbnail = mediaItem.mediaMetadata.artworkUri.toString(),
isOffline = true
)
LogTag.LogD(Innertube.TAG, "insertOfflineBean bean->${bean}")
relax.offline.music.App.appOfflineDBManager.insertOfflineBean(bean)
}
}
}

View File

@ -2,18 +2,17 @@ package relax.offline.music.activity
import android.annotation.SuppressLint
import android.view.View
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import com.gyf.immersionbar.ktx.immersionBar
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import relax.offline.music.App
import relax.offline.music.adapter.OfflineSongsAdapter
import relax.offline.music.bean.OfflineBean
import relax.offline.music.databinding.ActivityOfflineSongsBinding
import relax.offline.music.util.LogTag.LogD
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
@UnstableApi
class MoOfflineSongsActivity : MoBaseActivity() {
private val requests: Channel<Request> = Channel(Channel.UNLIMITED)
@ -102,12 +101,10 @@ class MoOfflineSongsActivity : MoBaseActivity() {
showLoadingUi()
offlineList.clear()
offlineList.addAll(relax.offline.music.App.appOfflineDBManager.getAllOfflineBeans())
for (offline in offlineList){
LogD(TAG,"offline id->${offline.videoId}")
}
val offlineBeans = App.appOfflineDBManager.getAllOfflineBeans()
val filteredBeans =
offlineBeans.filter { it.bytesDownloaded?.let { bytes -> bytes > 0 } == true }
offlineList.addAll(filteredBeans)
if (offlineList.size > 0) {
showDataUi()
} else {

View File

@ -41,6 +41,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import relax.offline.music.App
@OptIn(UnstableApi::class)
class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
@ -61,7 +62,6 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
private var comeFrom: Class<*>? = null
private var playListAdapter: PlayListAdapter? = null
private var downloadManager: DownloadManager? = null
private fun initImmersionBar() {
immersionBar {
@ -91,15 +91,37 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
if (meController != null && meController.currentMediaItem != null) {
updateInfoUi(meController.currentMediaItem)
}
} else if (comeFrom != null && comeFrom == MoOfflineSongsActivity::class.java) {
LogD(TAG, "从offline songs 进入")
binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME)
binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC)
//从数据库获取所有的offline
val offlineBeans = App.appOfflineDBManager.getAllOfflineBeans()
//过滤只有大小大于0的才添加到集合中
val allFilteredBeans = offlineBeans
.filter { it.bytesDownloaded?.let { bytes -> bytes > 0 } == true }
//找到当前点击进来的歌曲media
val findCurrentMedia = allFilteredBeans.find { it.videoId == videoId }?.asMediaItem
if (findCurrentMedia != null) {
binding.likeAndDownloadLayout.visibility = View.VISIBLE
updateInfoUi(findCurrentMedia)
binding.playbackErrorLayout.visibility = View.GONE
binding.totalDurationTv.visibility = View.GONE
meController?.let {
it.setMediaItem(findCurrentMedia, true)
it.prepare()
it.play()
val mediaItems = allFilteredBeans
.map { mapAll -> mapAll.asMediaItem }//转换成MediaItem
.filter { filter -> filter.mediaId != videoId }//过滤掉id相等的。
it.addMediaItems(mediaItems)
}
updatePlayListDataAndAdapter()
} else {
binding.playbackErrorLayout.visibility = View.VISIBLE
}
} else {
LogD(TAG, "从点击任意歌曲进入")
// if (meController != null && meController.currentMediaItem != null && videoId == meController.currentMediaItem?.mediaId) {
// //进入的id与当前的id一样就不重新去获取播放
// updateCurrentMediaItemInfo()
// updateInfoUi(meController.currentMediaItem)
// } else {
//
// }
binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME)
binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC)
@ -152,11 +174,14 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
LogD(TAG, "initDownloadFlow id ->${id}")
val currentScreenDownloads = downloads[id]
if (currentScreenDownloads != null) {
LogD(TAG, "initDownloadFlow Download id->${currentScreenDownloads?.request?.id}")
LogD(TAG, "updateDownloadUI id->${currentScreenDownloads.request.id}")
updateDownloadUI(currentScreenDownloads)
} else {
if (id != null) {
updateDownloadUi(id)
}
}
}
}
}
@ -203,7 +228,9 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
binding.downloadBtn.isClickable = false
binding.downloadBtn.isEnabled = false
} else {
binding.downloadLoading.visibility = View.GONE
binding.downloadImg.setImageResource(R.drawable.download_icon)
binding.downloadImg.visibility = View.VISIBLE
binding.downloadBtn.isClickable = true
binding.downloadBtn.isEnabled = true
}
@ -383,6 +410,9 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
if (DownloadUtil.downloadResourceExist(contentId)) {
return@setOnClickListener
}
insertOfflineData(currentMediaItem!!)
val downloadRequest = DownloadRequest
.Builder(contentId, contentId.toUri())
.setCustomCacheKey(contentId)

View File

@ -2,12 +2,15 @@ package relax.offline.music.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import relax.offline.music.R
import relax.offline.music.activity.MoOfflineSongsActivity
import relax.offline.music.activity.MoPlayDetailsActivity
import relax.offline.music.bean.OfflineBean
import relax.offline.music.databinding.OfflineListItemBinding
import relax.offline.music.media.MediaControllerManager
@ -28,18 +31,12 @@ class OfflineSongsAdapter(
holder.bind(bean)
holder.itemView.setOnClickListener {
// val meController = MediaControllerManager.getController()
// if (meController != null && meController.currentMediaItem != null) {
// var index = holder.bindingAdapterPosition
// if (index > meController.mediaItemCount) {
// index = 1
// }
// meController.seekTo(index, C.TIME_UNSET)
// if (!meController.isPlaying) {
// meController.prepare()
// meController.play()
// }
// }
val intent = Intent(context, MoPlayDetailsActivity::class.java)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_VIDEO_ID, bean.videoId)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_NAME, bean.title)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_DESC, bean.name)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_COME_FROM, MoOfflineSongsActivity::class.java)
context.startActivity(intent)
}
}

View File

@ -13,6 +13,7 @@ data class OfflineBean(
@ColumnInfo(name = "title") var title: String,
@ColumnInfo(name = "name") var name: String,
@ColumnInfo(name = "thumbnail") var thumbnail: String? = null,
@ColumnInfo(name = "bytesDownloaded") var bytesDownloaded: Long? = null,
@ColumnInfo(name = "size") var size: String? = null,
@ColumnInfo(name = "isOffline") var isOffline: Boolean
) : Serializable {

View File

@ -77,7 +77,7 @@ class AppOfflineDBManager private constructor(context: Context) {
}
}
private suspend fun getOfflineBeanByID(id: String): OfflineBean? {
suspend fun getOfflineBeanByID(id: String): OfflineBean? {
return dao.getOfflineBeanByID(id)
}
}

View File

@ -4,7 +4,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import androidx.viewbinding.ViewBinding
import relax.offline.music.sp.AppStore
import relax.offline.music.util.LogTag
@ -14,6 +16,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
@OptIn(UnstableApi::class)
abstract class MoBaseFragment<T : ViewBinding> : Fragment(), CoroutineScope by MainScope() {
enum class Event {
FragmentOnResume,

View File

@ -12,6 +12,7 @@ import relax.offline.music.util.DownloadUtil
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import relax.offline.music.App
class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
@ -59,7 +60,8 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
}
binding.offlineSongsBtn.setOnClickListener {
if (DownloadUtil.getDownloadCount() > 0) {
val count = binding.offlineSongsTv.text.toString().trim().toInt()
if (count > 0) {
val intent = Intent(context, MoOfflineSongsActivity::class.java)
startActivity(intent)
} else {
@ -72,8 +74,11 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
}
}
private fun fragmentOnResume() {
binding.offlineSongsTv.text = "${DownloadUtil.getDownloadCount()}"
private suspend fun fragmentOnResume() {
//过滤只有size大于0的才计数
val offlineBeans = App.appOfflineDBManager.getAllOfflineBeans()
val count = offlineBeans.count { it.bytesDownloaded?.let { bytes -> bytes > 0 } == true }
binding.offlineSongsTv.text = "$count"
}
override fun onResume() {

View File

@ -37,6 +37,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import relax.offline.music.App
import java.net.CookieHandler
import java.net.CookieManager
import java.net.CookiePolicy
@ -161,7 +162,7 @@ object DownloadUtil {
.use { cursor ->
while (cursor.moveToNext()) {
val download = cursor.download
if (download.request.id == id) {
if (download.request.id == id && download.state == Download.STATE_COMPLETED) {
isExist = true
}
}
@ -245,7 +246,17 @@ object DownloadUtil {
val notification: Notification = when (download.state) {
Download.STATE_COMPLETED -> {
insertOfflineData(download)
//更新大小
CoroutineScope(Dispatchers.IO).launch {
val offlineBean =
App.appOfflineDBManager.getOfflineBeanByID(download.request.id)
if (offlineBean != null) {
offlineBean.size =
FileSizeConverter(download.bytesDownloaded).formattedSize()
offlineBean.bytesDownloaded = download.bytesDownloaded
App.appOfflineDBManager.updateOfflineBean(offlineBean)
}
}
notificationHelper.buildDownloadCompletedNotification(
context,
@ -271,24 +282,4 @@ object DownloadUtil {
NotificationUtil.setNotification(context, nextNotificationId++, notification)
}
}
private fun insertOfflineData(download: Download) {
val meController = MediaControllerManager.getController()
if (meController != null && meController.currentMediaItem != null) {
val currentMediaItem = meController.currentMediaItem
val mediaMetadata = currentMediaItem?.mediaMetadata
CoroutineScope(Dispatchers.IO).launch {
val bean = OfflineBean(
videoId = download.request.id,
title = mediaMetadata?.title.toString(),
name = mediaMetadata?.artist.toString(),
thumbnail = mediaMetadata?.artworkUri.toString(),
size = FileSizeConverter(download.bytesDownloaded).formattedSize(),
isOffline = true
)
LogTag.LogD(TAG, "insertOfflineBean bean->${bean}")
relax.offline.music.App.appOfflineDBManager.insertOfflineBean(bean)
}
}
}
}

View File

@ -3,18 +3,35 @@ package relax.offline.music.util
import android.net.Uri
import android.os.Build
import android.text.format.DateUtils
import androidx.annotation.OptIn
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.util.UnstableApi
import relax.offline.music.bean.OfflineBean
import relax.offline.music.innertube.Innertube
import relax.offline.music.innertube.models.bodies.ContinuationBody
import relax.offline.music.innertube.requests.playlistPage
import relax.offline.music.innertube.utils.plus
val OfflineBean.asMediaItem: MediaItem
@OptIn(UnstableApi::class)
get() = MediaItem.Builder()
.setMediaId(videoId)
.setUri(videoId)
.setCustomCacheKey(videoId)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(title)
.setArtist(name)
.setArtworkUri(thumbnail?.toUri())
.build()
)
.build()
val Innertube.SongItem.asMediaItem: MediaItem
@UnstableApi
@OptIn(UnstableApi::class)
get() = MediaItem.Builder()
.setMediaId(key)
.setUri(key)
@ -29,7 +46,8 @@ val Innertube.SongItem.asMediaItem: MediaItem
bundleOf(
"albumId" to album?.endpoint?.browseId,
"durationText" to durationText,
"artistNames" to authors?.filter { it.endpoint != null }?.mapNotNull { it.name },
"artistNames" to authors?.filter { it.endpoint != null }
?.mapNotNull { it.name },
"artistIds" to authors?.mapNotNull { it.endpoint?.browseId },
)
)
@ -56,7 +74,8 @@ suspend fun Result<Innertube.PlaylistOrAlbumPage>.completed(): Result<Innertube.
while (playlistPage.songsPage?.continuation != null) {
val continuation = playlistPage.songsPage?.continuation!!
val otherPlaylistPageResult = Innertube.playlistPage(ContinuationBody(continuation = continuation)) ?: break
val otherPlaylistPageResult =
Innertube.playlistPage(ContinuationBody(continuation = continuation)) ?: break
if (otherPlaylistPageResult.isFailure) break

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF666666"
android:pathData="M2.623,8.779C3.339,5.724 5.724,3.339 8.779,2.623C10.898,2.126 13.102,2.126 15.221,2.623C18.276,3.339 20.661,5.724 21.377,8.779C21.874,10.898 21.874,13.102 21.377,15.221C20.661,18.276 18.276,20.661 15.221,21.377C13.102,21.874 10.898,21.874 8.779,21.377C5.724,20.661 3.339,18.276 2.623,15.221C2.126,13.102 2.126,10.898 2.623,8.779L4.083,9.122C3.639,11.015 3.639,12.985 4.083,14.878C4.669,17.378 6.622,19.33 9.122,19.917C11.015,20.361 12.985,20.361 14.878,19.917C17.378,19.33 19.33,17.378 19.917,14.878C20.361,12.985 20.361,11.015 19.917,9.122C19.33,6.622 17.378,4.669 14.878,4.083C12.985,3.639 11.015,3.639 9.122,4.083C6.622,4.669 4.669,6.622 4.083,9.122L2.623,8.779Z" />
<path
android:fillColor="#00000000"
android:pathData="M12,8.5V15.5M12,15.5L14.5,13M12,15.5L9.5,13"
android:strokeWidth="1.5"
android:strokeColor="#FF666666"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

View File

@ -94,13 +94,14 @@
<RelativeLayout
android:id="@+id/downloadBtn"
android:layout_width="40dp"
android:visibility="gone"
android:layout_height="40dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/download_icon" />
android:src="@drawable/download_gray_icon" />
</RelativeLayout>
</LinearLayout>