update
This commit is contained in:
parent
c0b1731e01
commit
70e1ef66c8
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="AndroidLintUnsafeImplicitIntentLaunch" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
@ -2,6 +2,7 @@ plugins {
|
|||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("kotlin-kapt")
|
id("kotlin-kapt")
|
||||||
|
id("org.jetbrains.kotlin.plugin.serialization")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -13,7 +14,7 @@ android {
|
|||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0.1"
|
versionName = "1.0.2"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@ -65,4 +66,13 @@ dependencies {
|
|||||||
implementation("androidx.media3:media3-exoplayer:1.3.1")
|
implementation("androidx.media3:media3-exoplayer:1.3.1")
|
||||||
implementation("androidx.media3:media3-ui:1.3.1")
|
implementation("androidx.media3:media3-ui:1.3.1")
|
||||||
implementation("androidx.media3:media3-common:1.3.1")
|
implementation("androidx.media3:media3-common:1.3.1")
|
||||||
|
|
||||||
|
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||||
|
implementation("io.ktor:ktor-client-core:2.3.8")
|
||||||
|
implementation("io.ktor:ktor-client-okhttp:2.3.8")
|
||||||
|
implementation("io.ktor:ktor-client-content-negotiation:2.3.8")
|
||||||
|
implementation("io.ktor:ktor-client-encoding:2.3.8")
|
||||||
|
implementation("io.ktor:ktor-client-serialization:2.1.2")
|
||||||
|
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.8")
|
||||||
|
implementation("org.brotli:dec:0.1.2")
|
||||||
}
|
}
|
||||||
@ -35,6 +35,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".activity.MainActivity"
|
android:name=".activity.MainActivity"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name=".activity.PrimaryActivity"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.PlayDetailsActivity"
|
android:name=".activity.PlayDetailsActivity"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
@ -45,8 +48,6 @@
|
|||||||
android:name=".activity.AboutActivity"
|
android:name=".activity.AboutActivity"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
<service android:name=".service.AudioPlayerService" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.PlaybackService"
|
android:name=".service.PlaybackService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@ -41,7 +41,7 @@ class LaunchActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun toMainActivity() {
|
private fun toMainActivity() {
|
||||||
startActivity(Intent(this, MainActivity::class.java))
|
startActivity(Intent(this, PrimaryActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
package com.player.musicoo.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.player.musicoo.sp.AppStore
|
||||||
|
import com.player.musicoo.util.LogTag
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
||||||
|
|
||||||
|
enum class Event {
|
||||||
|
ActivityStart,
|
||||||
|
ActivityStop,
|
||||||
|
ActivityOnResume
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val TAG = LogTag.VO_ACT_LOG
|
||||||
|
protected val appStore by lazy { AppStore(this) }
|
||||||
|
protected val events = Channel<Event>(Channel.UNLIMITED)
|
||||||
|
|
||||||
|
protected abstract suspend fun main()
|
||||||
|
private var defer: suspend () -> Unit = {}
|
||||||
|
private var deferRunning = false
|
||||||
|
|
||||||
|
fun defer(operation: suspend () -> Unit) {
|
||||||
|
this.defer = operation
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
launch {
|
||||||
|
main()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
events.trySend(Event.ActivityOnResume)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
events.trySend(Event.ActivityStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
events.trySend(Event.ActivityStop)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
if (deferRunning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deferRunning = true
|
||||||
|
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
defer()
|
||||||
|
} finally {
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
super.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
package com.player.musicoo.activity
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentTransaction
|
||||||
|
import com.player.musicoo.R
|
||||||
|
import com.player.musicoo.databinding.ActivityPrimaryBinding
|
||||||
|
import com.player.musicoo.fragment.ImportFragment
|
||||||
|
import com.player.musicoo.fragment.MoHomeFragment
|
||||||
|
|
||||||
|
class PrimaryActivity : MoBaseActivity() {
|
||||||
|
/**
|
||||||
|
* musicResponsiveListItemRenderer
|
||||||
|
* musicTwoRowItemRenderer
|
||||||
|
*/
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityPrimaryBinding
|
||||||
|
private val mFragments: MutableList<Fragment> = ArrayList()
|
||||||
|
private var currentIndex: Int = 0
|
||||||
|
private var mCurrentFragment: Fragment? = null
|
||||||
|
|
||||||
|
override suspend fun main() {
|
||||||
|
binding = ActivityPrimaryBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
initView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
initClick()
|
||||||
|
initFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initClick() {
|
||||||
|
binding.homeBtn.setOnClickListener {
|
||||||
|
changeFragment(0)
|
||||||
|
updateBtnState(0)
|
||||||
|
}
|
||||||
|
binding.importBtn.setOnClickListener {
|
||||||
|
changeFragment(1)
|
||||||
|
updateBtnState(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initFragment() {
|
||||||
|
mFragments.clear()
|
||||||
|
mFragments.add(MoHomeFragment())
|
||||||
|
mFragments.add(ImportFragment())
|
||||||
|
changeFragment(0)
|
||||||
|
updateBtnState(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeFragment(index: Int) {
|
||||||
|
currentIndex = index
|
||||||
|
val ft: FragmentTransaction = supportFragmentManager.beginTransaction()
|
||||||
|
if (null != mCurrentFragment) {
|
||||||
|
ft.hide(mCurrentFragment!!)
|
||||||
|
}
|
||||||
|
var fragment = supportFragmentManager.findFragmentByTag(
|
||||||
|
mFragments[currentIndex].javaClass.name
|
||||||
|
)
|
||||||
|
if (null == fragment) {
|
||||||
|
fragment = mFragments[index]
|
||||||
|
}
|
||||||
|
mCurrentFragment = fragment
|
||||||
|
|
||||||
|
if (!fragment.isAdded) {
|
||||||
|
ft.add(R.id.frame_layout, fragment, fragment.javaClass.name)
|
||||||
|
} else {
|
||||||
|
ft.show(fragment)
|
||||||
|
}
|
||||||
|
ft.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateBtnState(index: Int) {
|
||||||
|
binding.apply {
|
||||||
|
homeImg.setImageResource(
|
||||||
|
when (index) {
|
||||||
|
0 -> R.drawable.home_select_icon
|
||||||
|
else -> R.drawable.home_unselect_icon
|
||||||
|
}
|
||||||
|
)
|
||||||
|
importImg.setImageResource(
|
||||||
|
when (index) {
|
||||||
|
1 -> R.drawable.import_select_icon
|
||||||
|
else -> R.drawable.import_unselect_icon
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
package com.player.musicoo.adapter
|
||||||
|
|
||||||
|
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 com.player.musicoo.App
|
||||||
|
import com.player.musicoo.R
|
||||||
|
import com.player.musicoo.activity.PlayDetailsActivity
|
||||||
|
import com.player.musicoo.bean.Audio
|
||||||
|
import com.player.musicoo.databinding.MusicResponsiveItemBinding
|
||||||
|
import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
|
||||||
|
import com.player.musicoo.databinding.SoundsOfNatureLayoutBinding
|
||||||
|
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
|
||||||
|
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
|
||||||
|
import com.player.musicoo.util.getAudioDurationFromAssets
|
||||||
|
|
||||||
|
class ResponsiveListAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private val list: List<MusicCarouselShelfRenderer.Content>,
|
||||||
|
) :
|
||||||
|
RecyclerView.Adapter<ResponsiveListAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding =
|
||||||
|
MusicResponsiveItemBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||||
|
return ViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val bean = list[position]
|
||||||
|
holder.bind(bean)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = list.size
|
||||||
|
|
||||||
|
inner class ViewHolder(private val binding: MusicResponsiveItemBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(content: MusicCarouselShelfRenderer.Content) {
|
||||||
|
val url = content.musicResponsiveListItemRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.musicThumbnailRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.thumbnails
|
||||||
|
?.let { it.getOrNull(1) ?: it.getOrNull(0) }
|
||||||
|
?.url
|
||||||
|
val name = content.musicResponsiveListItemRenderer
|
||||||
|
?.flexColumns?.get(0)
|
||||||
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
|
?.text
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text
|
||||||
|
val desc = content.musicResponsiveListItemRenderer
|
||||||
|
?.flexColumns?.get(1)
|
||||||
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
|
?.text
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text
|
||||||
|
binding.apply {
|
||||||
|
Glide.with(context)
|
||||||
|
.load(url)
|
||||||
|
.into(image)
|
||||||
|
nameTv.text = name
|
||||||
|
descTv.text = desc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var itemClickListener: OnItemClickListener? = null
|
||||||
|
|
||||||
|
fun setOnItemClickListener(listener: OnItemClickListener) {
|
||||||
|
itemClickListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnItemClickListener {
|
||||||
|
fun onItemClick(position: Int)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
package com.player.musicoo.adapter
|
||||||
|
|
||||||
|
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 com.player.musicoo.App
|
||||||
|
import com.player.musicoo.R
|
||||||
|
import com.player.musicoo.activity.PlayDetailsActivity
|
||||||
|
import com.player.musicoo.bean.Audio
|
||||||
|
import com.player.musicoo.databinding.MusicResponsiveItemBinding
|
||||||
|
import com.player.musicoo.databinding.MusicTowRowItemBinding
|
||||||
|
import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
|
||||||
|
import com.player.musicoo.databinding.SoundsOfNatureLayoutBinding
|
||||||
|
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
|
||||||
|
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
|
||||||
|
import com.player.musicoo.util.getAudioDurationFromAssets
|
||||||
|
|
||||||
|
class TowRowListAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private val list: List<MusicCarouselShelfRenderer.Content>,
|
||||||
|
) :
|
||||||
|
RecyclerView.Adapter<TowRowListAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding =
|
||||||
|
MusicTowRowItemBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||||
|
return ViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val bean = list[position]
|
||||||
|
holder.bind(bean)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = list.size
|
||||||
|
|
||||||
|
inner class ViewHolder(private val binding: MusicTowRowItemBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(content: MusicCarouselShelfRenderer.Content) {
|
||||||
|
val url = content.musicTwoRowItemRenderer
|
||||||
|
?.thumbnailRenderer
|
||||||
|
?.musicThumbnailRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.thumbnails
|
||||||
|
?.let { it.getOrNull(1) ?: it.getOrNull(0) }
|
||||||
|
?.url
|
||||||
|
val name = content.musicTwoRowItemRenderer
|
||||||
|
?.title
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text
|
||||||
|
val desc = content.musicTwoRowItemRenderer
|
||||||
|
?.subtitle
|
||||||
|
?.runs
|
||||||
|
?.map { it.text }
|
||||||
|
?.joinToString("")
|
||||||
|
binding.apply {
|
||||||
|
Glide.with(context)
|
||||||
|
.load(url)
|
||||||
|
.into(image)
|
||||||
|
nameTv.text = name
|
||||||
|
descTv.text = desc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var itemClickListener: OnItemClickListener? = null
|
||||||
|
|
||||||
|
fun setOnItemClickListener(listener: OnItemClickListener) {
|
||||||
|
itemClickListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnItemClickListener {
|
||||||
|
fun onItemClick(position: Int)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.player.musicoo.fragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.player.musicoo.databinding.FragmentMoHomeBinding
|
||||||
|
import com.player.musicoo.sp.AppStore
|
||||||
|
import com.player.musicoo.util.LogTag
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
abstract class MoBaseFragment<T : ViewBinding> : Fragment(), CoroutineScope by MainScope() {
|
||||||
|
|
||||||
|
protected abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> T
|
||||||
|
protected lateinit var binding: T
|
||||||
|
protected val TAG = LogTag.VO_FRAGMENT_LOG
|
||||||
|
protected val appStore by lazy { AppStore(requireContext()) }
|
||||||
|
|
||||||
|
protected abstract suspend fun onViewCreated()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
binding = bindingInflater(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
launch {
|
||||||
|
onViewCreated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.player.musicoo.fragment
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.gyf.immersionbar.ktx.immersionBar
|
||||||
|
import com.player.musicoo.databinding.FragmentMoHomeBinding
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
|
||||||
|
import com.player.musicoo.innertube.requests.homePage
|
||||||
|
import com.player.musicoo.view.MusicResponsiveListView
|
||||||
|
import com.player.musicoo.view.MusicTowRowListView
|
||||||
|
|
||||||
|
class MoHomeFragment : MoBaseFragment<FragmentMoHomeBinding>() {
|
||||||
|
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentMoHomeBinding
|
||||||
|
get() = FragmentMoHomeBinding::inflate
|
||||||
|
|
||||||
|
override suspend fun onViewCreated() {
|
||||||
|
initView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initImmersionBar() {
|
||||||
|
immersionBar {
|
||||||
|
statusBarDarkFont(false)
|
||||||
|
statusBarView(binding.view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun initView() {
|
||||||
|
Innertube.homePage()?.onFailure {
|
||||||
|
Log.d("ocean", "onFailure->$it")
|
||||||
|
}
|
||||||
|
?.onSuccess {
|
||||||
|
for (home: Innertube.HomePage in it) {
|
||||||
|
for (content: MusicCarouselShelfRenderer.Content in home.contents) {
|
||||||
|
if (content.musicResponsiveListItemRenderer != null) {
|
||||||
|
binding.contentLayout.addView(
|
||||||
|
MusicResponsiveListView(
|
||||||
|
requireActivity(),
|
||||||
|
home
|
||||||
|
)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (content.musicTwoRowItemRenderer != null) {
|
||||||
|
binding.contentLayout.addView(
|
||||||
|
MusicTowRowListView(
|
||||||
|
requireActivity(),
|
||||||
|
home
|
||||||
|
)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
initImmersionBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHiddenChanged(hidden: Boolean) {
|
||||||
|
super.onHiddenChanged(hidden)
|
||||||
|
if (!hidden) {
|
||||||
|
initImmersionBar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
209
app/src/main/java/com/player/musicoo/innertube/Innertube.kt
Normal file
209
app/src/main/java/com/player/musicoo/innertube/Innertube.kt
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
package com.player.musicoo.innertube
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
|
import io.ktor.client.plugins.BrowserUserAgent
|
||||||
|
import io.ktor.client.plugins.compression.ContentEncoding
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.client.plugins.defaultRequest
|
||||||
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import com.player.musicoo.innertube.models.NavigationEndpoint
|
||||||
|
import com.player.musicoo.innertube.models.Runs
|
||||||
|
import com.player.musicoo.innertube.models.Thumbnail
|
||||||
|
import com.player.musicoo.innertube.utils.brotli
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
object Innertube {
|
||||||
|
val client = HttpClient(OkHttp) {
|
||||||
|
BrowserUserAgent()
|
||||||
|
|
||||||
|
expectSuccess = true
|
||||||
|
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
json(Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
explicitNulls = false
|
||||||
|
encodeDefaults = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
install(ContentEncoding) {
|
||||||
|
brotli()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultRequest {
|
||||||
|
url(scheme = "https", host ="music.youtube.com") {
|
||||||
|
headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
headers.append("X-Goog-Api-Key", "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8")
|
||||||
|
parameters.append("prettyPrint", "false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const val browse = "/youtubei/v1/browse"
|
||||||
|
internal const val next = "/youtubei/v1/next"
|
||||||
|
internal const val player = "/youtubei/v1/player"
|
||||||
|
internal const val queue = "/youtubei/v1/music/get_queue"
|
||||||
|
internal const val search = "/youtubei/v1/search"
|
||||||
|
internal const val searchSuggestions = "/youtubei/v1/music/get_search_suggestions"
|
||||||
|
|
||||||
|
internal const val musicResponsiveListItemRendererMask = "musicResponsiveListItemRenderer(flexColumns,fixedColumns,thumbnail,navigationEndpoint)"
|
||||||
|
internal const val musicTwoRowItemRendererMask = "musicTwoRowItemRenderer(thumbnailRenderer,title,subtitle,navigationEndpoint)"
|
||||||
|
const val playlistPanelVideoRendererMask = "playlistPanelVideoRenderer(title,navigationEndpoint,longBylineText,shortBylineText,thumbnail,lengthText)"
|
||||||
|
|
||||||
|
internal fun HttpRequestBuilder.mask(value: String = "*") =
|
||||||
|
header("X-Goog-FieldMask", value)
|
||||||
|
|
||||||
|
data class Info<T : NavigationEndpoint.Endpoint>(
|
||||||
|
val name: String?,
|
||||||
|
val endpoint: T?
|
||||||
|
) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
constructor(run: Runs.Run) : this(
|
||||||
|
name = run.text,
|
||||||
|
endpoint = run.navigationEndpoint?.endpoint as T?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class SearchFilter(val value: String) {
|
||||||
|
companion object {
|
||||||
|
val Song = SearchFilter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D")
|
||||||
|
val Video = SearchFilter("EgWKAQIQAWoKEAkQChAFEAMQBA%3D%3D")
|
||||||
|
val Album = SearchFilter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D")
|
||||||
|
val Artist = SearchFilter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D")
|
||||||
|
val CommunityPlaylist = SearchFilter("EgeKAQQoAEABagoQAxAEEAoQCRAF")
|
||||||
|
val FeaturedPlaylist = SearchFilter("EgeKAQQoADgBagwQDhAKEAMQBRAJEAQ%3D")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Item {
|
||||||
|
abstract val thumbnail: Thumbnail?
|
||||||
|
abstract val key: String
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SongItem(
|
||||||
|
val info: Info<NavigationEndpoint.Endpoint.Watch>?,
|
||||||
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
||||||
|
val album: Info<NavigationEndpoint.Endpoint.Browse>?,
|
||||||
|
val durationText: String?,
|
||||||
|
override val thumbnail: Thumbnail?
|
||||||
|
) : Item() {
|
||||||
|
override val key get() = info!!.endpoint!!.videoId!!
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
||||||
|
data class VideoItem(
|
||||||
|
val info: Info<NavigationEndpoint.Endpoint.Watch>?,
|
||||||
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
||||||
|
val viewsText: String?,
|
||||||
|
val durationText: String?,
|
||||||
|
override val thumbnail: Thumbnail?
|
||||||
|
) : Item() {
|
||||||
|
override val key get() = info!!.endpoint!!.videoId!!
|
||||||
|
|
||||||
|
val isOfficialMusicVideo: Boolean
|
||||||
|
get() = info
|
||||||
|
?.endpoint
|
||||||
|
?.watchEndpointMusicSupportedConfigs
|
||||||
|
?.watchEndpointMusicConfig
|
||||||
|
?.musicVideoType == "MUSIC_VIDEO_TYPE_OMV"
|
||||||
|
|
||||||
|
val isUserGeneratedContent: Boolean
|
||||||
|
get() = info
|
||||||
|
?.endpoint
|
||||||
|
?.watchEndpointMusicSupportedConfigs
|
||||||
|
?.watchEndpointMusicConfig
|
||||||
|
?.musicVideoType == "MUSIC_VIDEO_TYPE_UGC"
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AlbumItem(
|
||||||
|
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
|
||||||
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
||||||
|
val year: String?,
|
||||||
|
override val thumbnail: Thumbnail?
|
||||||
|
) : Item() {
|
||||||
|
override val key get() = info!!.endpoint!!.browseId!!
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ArtistItem(
|
||||||
|
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
|
||||||
|
val subscribersCountText: String?,
|
||||||
|
override val thumbnail: Thumbnail?
|
||||||
|
) : Item() {
|
||||||
|
override val key get() = info!!.endpoint!!.browseId!!
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PlaylistItem(
|
||||||
|
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
|
||||||
|
val channel: Info<NavigationEndpoint.Endpoint.Browse>?,
|
||||||
|
val songCount: Int?,
|
||||||
|
override val thumbnail: Thumbnail?
|
||||||
|
) : Item() {
|
||||||
|
override val key get() = info!!.endpoint!!.browseId!!
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
||||||
|
data class HomePage(
|
||||||
|
val title: String?,
|
||||||
|
val contents: List<MusicCarouselShelfRenderer.Content>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ArtistPage(
|
||||||
|
val name: String?,
|
||||||
|
val description: String?,
|
||||||
|
val thumbnail: Thumbnail?,
|
||||||
|
val shuffleEndpoint: NavigationEndpoint.Endpoint.Watch?,
|
||||||
|
val radioEndpoint: NavigationEndpoint.Endpoint.Watch?,
|
||||||
|
val songs: List<SongItem>?,
|
||||||
|
val songsEndpoint: NavigationEndpoint.Endpoint.Browse?,
|
||||||
|
val albums: List<AlbumItem>?,
|
||||||
|
val albumsEndpoint: NavigationEndpoint.Endpoint.Browse?,
|
||||||
|
val singles: List<AlbumItem>?,
|
||||||
|
val singlesEndpoint: NavigationEndpoint.Endpoint.Browse?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PlaylistOrAlbumPage(
|
||||||
|
val title: String?,
|
||||||
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
||||||
|
val year: String?,
|
||||||
|
val thumbnail: Thumbnail?,
|
||||||
|
val url: String?,
|
||||||
|
val songsPage: ItemsPage<SongItem>?,
|
||||||
|
val otherVersions: List<AlbumItem>?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class NextPage(
|
||||||
|
val itemsPage: ItemsPage<SongItem>?,
|
||||||
|
val playlistId: String?,
|
||||||
|
val params: String? = null,
|
||||||
|
val playlistSetVideoId: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RelatedPage(
|
||||||
|
val songs: List<SongItem>? = null,
|
||||||
|
val playlists: List<PlaylistItem>? = null,
|
||||||
|
val albums: List<AlbumItem>? = null,
|
||||||
|
val artists: List<ArtistItem>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ItemsPage<T : Item>(
|
||||||
|
val items: List<T>?,
|
||||||
|
val continuation: String?
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BrowseResponse(
|
||||||
|
val contents: Contents?,
|
||||||
|
val header: Header?,
|
||||||
|
val microformat: Microformat?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Contents(
|
||||||
|
val singleColumnBrowseResultsRenderer: Tabs?,
|
||||||
|
val sectionListRenderer: SectionListRenderer?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Header @OptIn(ExperimentalSerializationApi::class) constructor(
|
||||||
|
@JsonNames("musicVisualHeaderRenderer")
|
||||||
|
val musicImmersiveHeaderRenderer: MusicImmersiveHeaderRenderer?,
|
||||||
|
val musicDetailHeaderRenderer: MusicDetailHeaderRenderer?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class MusicDetailHeaderRenderer(
|
||||||
|
val title: Runs?,
|
||||||
|
val subtitle: Runs?,
|
||||||
|
val secondSubtitle: Runs?,
|
||||||
|
val thumbnail: ThumbnailRenderer?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MusicImmersiveHeaderRenderer(
|
||||||
|
val description: Runs?,
|
||||||
|
val playButton: PlayButton?,
|
||||||
|
val startRadioButton: StartRadioButton?,
|
||||||
|
val thumbnail: ThumbnailRenderer?,
|
||||||
|
val foregroundThumbnail: ThumbnailRenderer?,
|
||||||
|
val title: Runs?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class PlayButton(
|
||||||
|
val buttonRenderer: ButtonRenderer?
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class StartRadioButton(
|
||||||
|
val buttonRenderer: ButtonRenderer?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Microformat(
|
||||||
|
val microformatDataRenderer: MicroformatDataRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class MicroformatDataRenderer(
|
||||||
|
val urlCanonical: String?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ButtonRenderer(
|
||||||
|
val navigationEndpoint: NavigationEndpoint?
|
||||||
|
)
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Context(
|
||||||
|
val client: Client,
|
||||||
|
val thirdParty: ThirdParty? = null,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Client(
|
||||||
|
val clientName: String,
|
||||||
|
val clientVersion: String,
|
||||||
|
val platform: String,
|
||||||
|
val hl: String = "zh",
|
||||||
|
// val visitorData: String = "CgtEUlRINDFjdm1YayjX1pSaBg%3D%3D",
|
||||||
|
val androidSdkVersion: Int? = null,
|
||||||
|
val userAgent: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ThirdParty(
|
||||||
|
val embedUrl: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DefaultWeb = Context(
|
||||||
|
client = Client(
|
||||||
|
clientName = "WEB_REMIX",
|
||||||
|
clientVersion = "1.20220918",
|
||||||
|
platform = "DESKTOP",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val DefaultAndroid = Context(
|
||||||
|
client = Client(
|
||||||
|
clientName = "ANDROID_MUSIC",
|
||||||
|
clientVersion = "5.28.1",
|
||||||
|
platform = "MOBILE",
|
||||||
|
androidSdkVersion = 30,
|
||||||
|
userAgent = "com.google.android.apps.youtube.music/5.28.1 (Linux; U; Android 11) gzip"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val DefaultAgeRestrictionBypass = Context(
|
||||||
|
client = Client(
|
||||||
|
clientName = "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
|
||||||
|
clientVersion = "2.0",
|
||||||
|
platform = "TV"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@Serializable
|
||||||
|
data class Continuation(
|
||||||
|
@JsonNames("nextContinuationData", "nextRadioContinuationData")
|
||||||
|
val nextContinuationData: Data?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Data(
|
||||||
|
val continuation: String?
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@Serializable
|
||||||
|
data class ContinuationResponse(
|
||||||
|
val continuationContents: ContinuationContents?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class ContinuationContents(
|
||||||
|
@JsonNames("musicPlaylistShelfContinuation")
|
||||||
|
val musicShelfContinuation: MusicShelfRenderer?,
|
||||||
|
val playlistPanelContinuation: NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer?,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GetQueueResponse(
|
||||||
|
val queueDatas: List<QueueData>?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class QueueData(
|
||||||
|
val content: NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer.Content?
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GridRenderer(
|
||||||
|
val items: List<Item>?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Item(
|
||||||
|
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MusicCarouselShelfRenderer(
|
||||||
|
val header: Header?,
|
||||||
|
val contents: List<Content>?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Content(
|
||||||
|
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,
|
||||||
|
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Header(
|
||||||
|
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,
|
||||||
|
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
|
||||||
|
val musicCarouselShelfBasicHeaderRenderer: MusicCarouselShelfBasicHeaderRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class MusicCarouselShelfBasicHeaderRenderer(
|
||||||
|
val moreContentButton: MoreContentButton?,
|
||||||
|
val title: Runs?,
|
||||||
|
val strapline: Runs?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class MoreContentButton(
|
||||||
|
val buttonRenderer: ButtonRenderer?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@Serializable
|
||||||
|
data class MusicResponsiveListItemRenderer(
|
||||||
|
val fixedColumns: List<FlexColumn>?,
|
||||||
|
val flexColumns: List<FlexColumn>,
|
||||||
|
val thumbnail: ThumbnailRenderer?,
|
||||||
|
val navigationEndpoint: NavigationEndpoint?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class FlexColumn(
|
||||||
|
@JsonNames("musicResponsiveListItemFixedColumnRenderer")
|
||||||
|
val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class MusicResponsiveListItemFlexColumnRenderer(
|
||||||
|
val text: Runs?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MusicShelfRenderer(
|
||||||
|
val bottomEndpoint: NavigationEndpoint?,
|
||||||
|
val contents: List<Content>?,
|
||||||
|
val continuations: List<Continuation>?,
|
||||||
|
val title: Runs?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Content(
|
||||||
|
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
|
||||||
|
) {
|
||||||
|
val runs: Pair<List<Runs.Run>, List<List<Runs.Run>>>
|
||||||
|
get() = (musicResponsiveListItemRenderer
|
||||||
|
?.flexColumns
|
||||||
|
?.firstOrNull()
|
||||||
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
|
?.text
|
||||||
|
?.runs
|
||||||
|
?: emptyList()) to
|
||||||
|
(musicResponsiveListItemRenderer
|
||||||
|
?.flexColumns
|
||||||
|
?.lastOrNull()
|
||||||
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
|
?.text
|
||||||
|
?.splitBySeparator()
|
||||||
|
?: emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
val thumbnail: Thumbnail?
|
||||||
|
get() = musicResponsiveListItemRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.musicThumbnailRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.thumbnails
|
||||||
|
?.firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MusicTwoRowItemRenderer(
|
||||||
|
val navigationEndpoint: NavigationEndpoint?,
|
||||||
|
val thumbnailRenderer: ThumbnailRenderer?,
|
||||||
|
val title: Runs?,
|
||||||
|
val subtitle: Runs?,
|
||||||
|
)
|
||||||
@ -0,0 +1,203 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* watchPlaylistEndpoint: params, playlistId
|
||||||
|
* watchEndpoint: params, playlistId, videoId, index
|
||||||
|
* browseEndpoint: params, browseId
|
||||||
|
* searchEndpoint: params, query
|
||||||
|
*/
|
||||||
|
//@Serializable
|
||||||
|
//data class NavigationEndpoint(
|
||||||
|
// @JsonNames("watchEndpoint", "watchPlaylistEndpoint", "navigationEndpoint", "browseEndpoint", "searchEndpoint")
|
||||||
|
// val endpoint: Endpoint
|
||||||
|
//) {
|
||||||
|
// @Serializable
|
||||||
|
// data class Endpoint(
|
||||||
|
// val params: String?,
|
||||||
|
// val playlistId: String?,
|
||||||
|
// val videoId: String?,
|
||||||
|
// val index: Int?,
|
||||||
|
// val browseId: String?,
|
||||||
|
// val query: String?,
|
||||||
|
// val watchEndpointMusicSupportedConfigs: WatchEndpointMusicSupportedConfigs?,
|
||||||
|
// val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs?,
|
||||||
|
// ) {
|
||||||
|
// @Serializable
|
||||||
|
// data class WatchEndpointMusicSupportedConfigs(
|
||||||
|
// val watchEndpointMusicConfig: WatchEndpointMusicConfig
|
||||||
|
// ) {
|
||||||
|
// @Serializable
|
||||||
|
// data class WatchEndpointMusicConfig(
|
||||||
|
// val musicVideoType: String
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Serializable
|
||||||
|
// data class BrowseEndpointContextSupportedConfigs(
|
||||||
|
// val browseEndpointContextMusicConfig: BrowseEndpointContextMusicConfig
|
||||||
|
// ) {
|
||||||
|
// @Serializable
|
||||||
|
// data class BrowseEndpointContextMusicConfig(
|
||||||
|
// val pageType: String
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class NavigationEndpoint(
|
||||||
|
val watchEndpoint: Endpoint.Watch?,
|
||||||
|
val watchPlaylistEndpoint: Endpoint.WatchPlaylist?,
|
||||||
|
val browseEndpoint: Endpoint.Browse?,
|
||||||
|
val searchEndpoint: Endpoint.Search?,
|
||||||
|
) {
|
||||||
|
val endpoint: Endpoint?
|
||||||
|
get() = watchEndpoint ?: browseEndpoint ?: watchPlaylistEndpoint ?: searchEndpoint
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class Endpoint {
|
||||||
|
@Serializable
|
||||||
|
data class Watch(
|
||||||
|
val params: String? = null,
|
||||||
|
val playlistId: String? = null,
|
||||||
|
val videoId: String? = null,
|
||||||
|
val index: Int? = null,
|
||||||
|
val playlistSetVideoId: String? = null,
|
||||||
|
val watchEndpointMusicSupportedConfigs: WatchEndpointMusicSupportedConfigs? = null,
|
||||||
|
) : Endpoint() {
|
||||||
|
val type: String?
|
||||||
|
get() = watchEndpointMusicSupportedConfigs
|
||||||
|
?.watchEndpointMusicConfig
|
||||||
|
?.musicVideoType
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class WatchEndpointMusicSupportedConfigs(
|
||||||
|
val watchEndpointMusicConfig: WatchEndpointMusicConfig?
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class WatchEndpointMusicConfig(
|
||||||
|
val musicVideoType: String?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class WatchPlaylist(
|
||||||
|
val params: String?,
|
||||||
|
val playlistId: String?,
|
||||||
|
) : Endpoint()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Browse(
|
||||||
|
val params: String? = null,
|
||||||
|
val browseId: String? = null,
|
||||||
|
val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs? = null,
|
||||||
|
) : Endpoint() {
|
||||||
|
val type: String?
|
||||||
|
get() = browseEndpointContextSupportedConfigs
|
||||||
|
?.browseEndpointContextMusicConfig
|
||||||
|
?.pageType
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BrowseEndpointContextSupportedConfigs(
|
||||||
|
val browseEndpointContextMusicConfig: BrowseEndpointContextMusicConfig
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BrowseEndpointContextMusicConfig(
|
||||||
|
val pageType: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Search(
|
||||||
|
val params: String?,
|
||||||
|
val query: String,
|
||||||
|
) : Endpoint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//@Serializable(with = NavigationEndpoint.Serializer::class)
|
||||||
|
//sealed class NavigationEndpoint {
|
||||||
|
// @Serializable
|
||||||
|
// data class Watch(
|
||||||
|
// val watchEndpoint: Data
|
||||||
|
// ) : NavigationEndpoint() {
|
||||||
|
// @Serializable
|
||||||
|
// data class Data(
|
||||||
|
// val params: String?,
|
||||||
|
// val playlistId: String,
|
||||||
|
// val videoId: String,
|
||||||
|
//// val index: Int?
|
||||||
|
// val watchEndpointMusicSupportedConfigs: WatchEndpointMusicSupportedConfigs,
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// @Serializable
|
||||||
|
// data class WatchEndpointMusicSupportedConfigs(
|
||||||
|
// val watchEndpointMusicConfig: WatchEndpointMusicConfig
|
||||||
|
// ) {
|
||||||
|
// @Serializable
|
||||||
|
// data class WatchEndpointMusicConfig(
|
||||||
|
// val musicVideoType: String
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Serializable
|
||||||
|
// data class WatchPlaylist(
|
||||||
|
// val watchPlaylistEndpoint: Data
|
||||||
|
// ) : NavigationEndpoint() {
|
||||||
|
// @Serializable
|
||||||
|
// data class Data(
|
||||||
|
// val params: String?,
|
||||||
|
// val playlistId: String,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Serializable
|
||||||
|
// data class Browse(
|
||||||
|
// val browseEndpoint: Data
|
||||||
|
// ) : NavigationEndpoint() {
|
||||||
|
// @Serializable
|
||||||
|
// data class Data(
|
||||||
|
// val params: String?,
|
||||||
|
// val browseId: String,
|
||||||
|
// val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs,
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// @Serializable
|
||||||
|
// data class BrowseEndpointContextSupportedConfigs(
|
||||||
|
// val browseEndpointContextMusicConfig: BrowseEndpointContextMusicConfig
|
||||||
|
// ) {
|
||||||
|
// @Serializable
|
||||||
|
// data class BrowseEndpointContextMusicConfig(
|
||||||
|
// val pageType: String
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Serializable
|
||||||
|
// data class Search(
|
||||||
|
// val searchEndpoint: Data
|
||||||
|
// ) : NavigationEndpoint() {
|
||||||
|
// @Serializable
|
||||||
|
// data class Data(
|
||||||
|
// val params: String?,
|
||||||
|
// val query: String,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// object Serializer : JsonContentPolymorphicSerializer<NavigationEndpoint>(NavigationEndpoint::class) {
|
||||||
|
// override fun selectDeserializer(element: JsonElement) = when {
|
||||||
|
// "watchEndpoint" in element.jsonObject -> Watch.serializer()
|
||||||
|
// "watchPlaylistEndpoint" in element.jsonObject -> WatchPlaylist.serializer()
|
||||||
|
// "browseEndpoint" in element.jsonObject -> Browse.serializer()
|
||||||
|
// "searchEndpoint" in element.jsonObject -> Search.serializer()
|
||||||
|
// else -> TODO()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@Serializable
|
||||||
|
data class NextResponse(
|
||||||
|
val contents: Contents?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class MusicQueueRenderer(
|
||||||
|
val content: Content?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Content(
|
||||||
|
@JsonNames("playlistPanelContinuation")
|
||||||
|
val playlistPanelRenderer: PlaylistPanelRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class PlaylistPanelRenderer(
|
||||||
|
val contents: List<Content>?,
|
||||||
|
val continuations: List<Continuation>?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Content(
|
||||||
|
val playlistPanelVideoRenderer: PlaylistPanelVideoRenderer?,
|
||||||
|
val automixPreviewVideoRenderer: AutomixPreviewVideoRenderer?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AutomixPreviewVideoRenderer(
|
||||||
|
val content: Content?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Content(
|
||||||
|
val automixPlaylistVideoRenderer: AutomixPlaylistVideoRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class AutomixPlaylistVideoRenderer(
|
||||||
|
val navigationEndpoint: NavigationEndpoint?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Contents(
|
||||||
|
val singleColumnMusicWatchNextResultsRenderer: SingleColumnMusicWatchNextResultsRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class SingleColumnMusicWatchNextResultsRenderer(
|
||||||
|
val tabbedRenderer: TabbedRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class TabbedRenderer(
|
||||||
|
val watchNextTabbedResultsRenderer: WatchNextTabbedResultsRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class WatchNextTabbedResultsRenderer(
|
||||||
|
val tabs: List<Tab>?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Tab(
|
||||||
|
val tabRenderer: TabRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class TabRenderer(
|
||||||
|
val content: Content?,
|
||||||
|
val endpoint: NavigationEndpoint?,
|
||||||
|
val title: String?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Content(
|
||||||
|
val musicQueueRenderer: MusicQueueRenderer?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerResponse(
|
||||||
|
val playabilityStatus: PlayabilityStatus?,
|
||||||
|
val playerConfig: PlayerConfig?,
|
||||||
|
val streamingData: StreamingData?,
|
||||||
|
val videoDetails: VideoDetails?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class PlayabilityStatus(
|
||||||
|
val status: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerConfig(
|
||||||
|
val audioConfig: AudioConfig?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class AudioConfig(
|
||||||
|
private val loudnessDb: Double?
|
||||||
|
) {
|
||||||
|
// For music clients only
|
||||||
|
val normalizedLoudnessDb: Float?
|
||||||
|
get() = loudnessDb?.plus(7)?.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class StreamingData(
|
||||||
|
val adaptiveFormats: List<AdaptiveFormat>?
|
||||||
|
) {
|
||||||
|
val highestQualityFormat: AdaptiveFormat?
|
||||||
|
get() = adaptiveFormats?.findLast { it.itag == 251 || it.itag == 140 }
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AdaptiveFormat(
|
||||||
|
val itag: Int,
|
||||||
|
val mimeType: String,
|
||||||
|
val bitrate: Long?,
|
||||||
|
val averageBitrate: Long?,
|
||||||
|
val contentLength: Long?,
|
||||||
|
val audioQuality: String?,
|
||||||
|
val approxDurationMs: Long?,
|
||||||
|
val lastModified: Long?,
|
||||||
|
val loudnessDb: Double?,
|
||||||
|
val audioSampleRate: Int?,
|
||||||
|
val url: String?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class VideoDetails(
|
||||||
|
val videoId: String?
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlaylistPanelVideoRenderer(
|
||||||
|
val title: Runs?,
|
||||||
|
val longBylineText: Runs?,
|
||||||
|
val shortBylineText: Runs?,
|
||||||
|
val lengthText: Runs?,
|
||||||
|
val navigationEndpoint: NavigationEndpoint?,
|
||||||
|
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail?,
|
||||||
|
)
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Runs(
|
||||||
|
val runs: List<Run> = listOf()
|
||||||
|
) {
|
||||||
|
val text: String
|
||||||
|
get() = runs.joinToString("") { it.text ?: "" }
|
||||||
|
|
||||||
|
fun splitBySeparator(): List<List<Run>> {
|
||||||
|
return runs.flatMapIndexed { index, run ->
|
||||||
|
when {
|
||||||
|
index == 0 || index == runs.lastIndex -> listOf(index)
|
||||||
|
run.text == " • " -> listOf(index - 1, index + 1)
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
}.windowed(size = 2, step = 2) { (from, to) -> runs.slice(from..to) }.let {
|
||||||
|
it.ifEmpty {
|
||||||
|
listOf(runs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Run(
|
||||||
|
val text: String?,
|
||||||
|
val navigationEndpoint: NavigationEndpoint?,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SearchResponse(
|
||||||
|
val contents: Contents?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Contents(
|
||||||
|
val tabbedSearchResultsRenderer: Tabs?
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SearchSuggestionsResponse(
|
||||||
|
val contents: List<Content>?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Content(
|
||||||
|
val searchSuggestionsSectionRenderer: SearchSuggestionsSectionRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class SearchSuggestionsSectionRenderer(
|
||||||
|
val contents: List<Content>?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Content(
|
||||||
|
val searchSuggestionRenderer: SearchSuggestionRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class SearchSuggestionRenderer(
|
||||||
|
val navigationEndpoint: NavigationEndpoint?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@Serializable
|
||||||
|
data class SectionListRenderer(
|
||||||
|
val contents: List<Content>?,
|
||||||
|
val continuations: List<Continuation>?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Content(
|
||||||
|
@JsonNames("musicImmersiveCarouselShelfRenderer")
|
||||||
|
val musicCarouselShelfRenderer: MusicCarouselShelfRenderer?,
|
||||||
|
@JsonNames("musicPlaylistShelfRenderer")
|
||||||
|
val musicShelfRenderer: MusicShelfRenderer?,
|
||||||
|
val gridRenderer: GridRenderer?,
|
||||||
|
val musicDescriptionShelfRenderer: MusicDescriptionShelfRenderer?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MusicDescriptionShelfRenderer(
|
||||||
|
val description: Runs?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Tabs(
|
||||||
|
val tabs: List<Tab>?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Tab(
|
||||||
|
val tabRenderer: TabRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class TabRenderer(
|
||||||
|
val content: Content?,
|
||||||
|
val title: String?,
|
||||||
|
val tabIdentifier: String?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Content(
|
||||||
|
val sectionListRenderer: SectionListRenderer?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Thumbnail(
|
||||||
|
val url: String,
|
||||||
|
val height: Int?,
|
||||||
|
val width: Int?
|
||||||
|
) {
|
||||||
|
val isResizable: Boolean
|
||||||
|
get() = !url.startsWith("https://i.ytimg.com")
|
||||||
|
|
||||||
|
fun size(size: Int): String {
|
||||||
|
return when {
|
||||||
|
url.startsWith("https://lh3.googleusercontent.com") -> "$url-w$size-h$size"
|
||||||
|
url.startsWith("https://yt3.ggpht.com") -> "$url-s$size"
|
||||||
|
else -> url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.player.musicoo.innertube.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@Serializable
|
||||||
|
data class ThumbnailRenderer(
|
||||||
|
@JsonNames("croppedSquareThumbnailRenderer")
|
||||||
|
val musicThumbnailRenderer: MusicThumbnailRenderer?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class MusicThumbnailRenderer(
|
||||||
|
val thumbnail: Thumbnail?
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Thumbnail(
|
||||||
|
val thumbnails: List<com.player.musicoo.innertube.models.Thumbnail>?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.player.musicoo.innertube.models.bodies
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.models.Context
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BrowseBody(
|
||||||
|
val context: Context = Context.DefaultWeb,
|
||||||
|
val browseId: String,
|
||||||
|
val params: String? = null
|
||||||
|
)
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.player.musicoo.innertube.models.bodies
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.models.Context
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ContinuationBody(
|
||||||
|
val context: Context = Context.DefaultWeb,
|
||||||
|
val continuation: String,
|
||||||
|
)
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.player.musicoo.innertube.models.bodies
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.models.Context
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class NextBody(
|
||||||
|
val context: Context = Context.DefaultWeb,
|
||||||
|
val videoId: String?,
|
||||||
|
val isAudioOnly: Boolean = true,
|
||||||
|
val playlistId: String? = null,
|
||||||
|
val tunerSettingValue: String = "AUTOMIX_SETTING_NORMAL",
|
||||||
|
val index: Int? = null,
|
||||||
|
val params: String? = null,
|
||||||
|
val playlistSetVideoId: String? = null,
|
||||||
|
val watchEndpointMusicSupportedConfigs: WatchEndpointMusicSupportedConfigs = WatchEndpointMusicSupportedConfigs(
|
||||||
|
musicVideoType = "MUSIC_VIDEO_TYPE_ATV"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class WatchEndpointMusicSupportedConfigs(
|
||||||
|
val musicVideoType: String
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.player.musicoo.innertube.models.bodies
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.models.Context
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerBody(
|
||||||
|
val context: Context = Context.DefaultAndroid,
|
||||||
|
val videoId: String,
|
||||||
|
val playlistId: String? = null
|
||||||
|
)
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.player.musicoo.innertube.models.bodies
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.models.Context
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class QueueBody(
|
||||||
|
val context: Context = Context.DefaultWeb,
|
||||||
|
val videoIds: List<String>? = null,
|
||||||
|
val playlistId: String? = null,
|
||||||
|
)
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.player.musicoo.innertube.models.bodies
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.models.Context
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SearchBody(
|
||||||
|
val context: Context = Context.DefaultWeb,
|
||||||
|
val query: String,
|
||||||
|
val params: String
|
||||||
|
)
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.player.musicoo.innertube.models.bodies
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.models.Context
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SearchSuggestionsBody(
|
||||||
|
val context: Context = Context.DefaultWeb,
|
||||||
|
val input: String
|
||||||
|
)
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import io.ktor.http.Url
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.NavigationEndpoint
|
||||||
|
import com.player.musicoo.innertube.models.bodies.BrowseBody
|
||||||
|
|
||||||
|
suspend fun Innertube.albumPage(body: BrowseBody): Result<Innertube.PlaylistOrAlbumPage>? {
|
||||||
|
return playlistPage(body)?.map { album ->
|
||||||
|
album.url?.let { Url(it).parameters["list"] }?.let { playlistId ->
|
||||||
|
playlistPage(BrowseBody(browseId = "VL$playlistId"))?.getOrNull()?.let { playlist ->
|
||||||
|
album.copy(songsPage = playlist.songsPage)
|
||||||
|
}
|
||||||
|
} ?: album
|
||||||
|
}?.map { album ->
|
||||||
|
val albumInfo = Innertube.Info(
|
||||||
|
name = album.title,
|
||||||
|
endpoint = NavigationEndpoint.Endpoint.Browse(
|
||||||
|
browseId = body.browseId,
|
||||||
|
params = body.params
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
album.copy(
|
||||||
|
songsPage = album.songsPage?.copy(
|
||||||
|
items = album.songsPage.items?.map { song ->
|
||||||
|
song.copy(
|
||||||
|
authors = song.authors ?: album.authors,
|
||||||
|
album = albumInfo,
|
||||||
|
thumbnail = album.thumbnail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.BrowseResponse
|
||||||
|
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
|
||||||
|
import com.player.musicoo.innertube.models.MusicShelfRenderer
|
||||||
|
import com.player.musicoo.innertube.models.SectionListRenderer
|
||||||
|
import com.player.musicoo.innertube.models.bodies.BrowseBody
|
||||||
|
import com.player.musicoo.innertube.utils.findSectionByTitle
|
||||||
|
import com.player.musicoo.innertube.utils.from
|
||||||
|
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
|
||||||
|
|
||||||
|
suspend fun Innertube.artistPage(body: BrowseBody): Result<Innertube.ArtistPage>? =
|
||||||
|
runCatchingNonCancellable {
|
||||||
|
val response = client.post(browse) {
|
||||||
|
setBody(body)
|
||||||
|
mask("contents,header")
|
||||||
|
}.body<BrowseResponse>()
|
||||||
|
|
||||||
|
fun findSectionByTitle(text: String): SectionListRenderer.Content? {
|
||||||
|
return response
|
||||||
|
.contents
|
||||||
|
?.singleColumnBrowseResultsRenderer
|
||||||
|
?.tabs
|
||||||
|
?.get(0)
|
||||||
|
?.tabRenderer
|
||||||
|
?.content
|
||||||
|
?.sectionListRenderer
|
||||||
|
?.findSectionByTitle(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
val songsSection = findSectionByTitle("Songs")?.musicShelfRenderer
|
||||||
|
val albumsSection = findSectionByTitle("Albums")?.musicCarouselShelfRenderer
|
||||||
|
val singlesSection = findSectionByTitle("Singles")?.musicCarouselShelfRenderer
|
||||||
|
|
||||||
|
Innertube.ArtistPage(
|
||||||
|
name = response
|
||||||
|
.header
|
||||||
|
?.musicImmersiveHeaderRenderer
|
||||||
|
?.title
|
||||||
|
?.text,
|
||||||
|
description = response
|
||||||
|
.header
|
||||||
|
?.musicImmersiveHeaderRenderer
|
||||||
|
?.description
|
||||||
|
?.text,
|
||||||
|
thumbnail = (response
|
||||||
|
.header
|
||||||
|
?.musicImmersiveHeaderRenderer
|
||||||
|
?.foregroundThumbnail
|
||||||
|
?: response
|
||||||
|
.header
|
||||||
|
?.musicImmersiveHeaderRenderer
|
||||||
|
?.thumbnail)
|
||||||
|
?.musicThumbnailRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.thumbnails
|
||||||
|
?.getOrNull(0),
|
||||||
|
shuffleEndpoint = response
|
||||||
|
.header
|
||||||
|
?.musicImmersiveHeaderRenderer
|
||||||
|
?.playButton
|
||||||
|
?.buttonRenderer
|
||||||
|
?.navigationEndpoint
|
||||||
|
?.watchEndpoint,
|
||||||
|
radioEndpoint = response
|
||||||
|
.header
|
||||||
|
?.musicImmersiveHeaderRenderer
|
||||||
|
?.startRadioButton
|
||||||
|
?.buttonRenderer
|
||||||
|
?.navigationEndpoint
|
||||||
|
?.watchEndpoint,
|
||||||
|
songs = songsSection
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
||||||
|
?.mapNotNull(Innertube.SongItem::from),
|
||||||
|
songsEndpoint = songsSection
|
||||||
|
?.bottomEndpoint
|
||||||
|
?.browseEndpoint,
|
||||||
|
albums = albumsSection
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
|
||||||
|
?.mapNotNull(Innertube.AlbumItem::from),
|
||||||
|
albumsEndpoint = albumsSection
|
||||||
|
?.header
|
||||||
|
?.musicCarouselShelfBasicHeaderRenderer
|
||||||
|
?.moreContentButton
|
||||||
|
?.buttonRenderer
|
||||||
|
?.navigationEndpoint
|
||||||
|
?.browseEndpoint,
|
||||||
|
singles = singlesSection
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
|
||||||
|
?.mapNotNull(Innertube.AlbumItem::from),
|
||||||
|
singlesEndpoint = singlesSection
|
||||||
|
?.header
|
||||||
|
?.musicCarouselShelfBasicHeaderRenderer
|
||||||
|
?.moreContentButton
|
||||||
|
?.buttonRenderer
|
||||||
|
?.navigationEndpoint
|
||||||
|
?.browseEndpoint,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.BrowseResponse
|
||||||
|
import com.player.musicoo.innertube.models.Context
|
||||||
|
import com.player.musicoo.innertube.models.SectionListRenderer
|
||||||
|
import com.player.musicoo.innertube.models.bodies.BrowseBody
|
||||||
|
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
|
||||||
|
//
|
||||||
|
suspend fun Innertube.homePage(): Result<List<Innertube.HomePage>>? =
|
||||||
|
runCatchingNonCancellable {
|
||||||
|
|
||||||
|
val response = client.post(browse) {
|
||||||
|
setBody(BrowseBody(Context.DefaultWeb, "FEmusic_home"))
|
||||||
|
}.body<BrowseResponse>()
|
||||||
|
|
||||||
|
val sectionListRenderer = response
|
||||||
|
.contents
|
||||||
|
?.singleColumnBrowseResultsRenderer
|
||||||
|
?.tabs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.tabRenderer
|
||||||
|
?.content
|
||||||
|
?.sectionListRenderer
|
||||||
|
|
||||||
|
val contents = sectionListRenderer?.contents
|
||||||
|
Log.d("ocean","contents->$contents")
|
||||||
|
|
||||||
|
val searchList: MutableList<Innertube.HomePage> = mutableListOf()
|
||||||
|
|
||||||
|
if (contents != null) {
|
||||||
|
for (content: SectionListRenderer.Content in contents) {
|
||||||
|
val text = content.musicCarouselShelfRenderer
|
||||||
|
?.header
|
||||||
|
?.musicCarouselShelfBasicHeaderRenderer
|
||||||
|
?.title
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text
|
||||||
|
|
||||||
|
val homePage =
|
||||||
|
content.musicCarouselShelfRenderer?.contents?.let {
|
||||||
|
Innertube.HomePage(text,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (homePage != null) {
|
||||||
|
searchList.add(homePage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searchList
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.BrowseResponse
|
||||||
|
import com.player.musicoo.innertube.models.ContinuationResponse
|
||||||
|
import com.player.musicoo.innertube.models.GridRenderer
|
||||||
|
import com.player.musicoo.innertube.models.MusicResponsiveListItemRenderer
|
||||||
|
import com.player.musicoo.innertube.models.MusicShelfRenderer
|
||||||
|
import com.player.musicoo.innertube.models.MusicTwoRowItemRenderer
|
||||||
|
import com.player.musicoo.innertube.models.bodies.BrowseBody
|
||||||
|
import com.player.musicoo.innertube.models.bodies.ContinuationBody
|
||||||
|
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
|
||||||
|
|
||||||
|
suspend fun <T : Innertube.Item> Innertube.itemsPage(
|
||||||
|
body: BrowseBody,
|
||||||
|
fromMusicResponsiveListItemRenderer: (MusicResponsiveListItemRenderer) -> T? = { null },
|
||||||
|
fromMusicTwoRowItemRenderer: (MusicTwoRowItemRenderer) -> T? = { null },
|
||||||
|
) = runCatchingNonCancellable {
|
||||||
|
val response = client.post(browse) {
|
||||||
|
setBody(body)
|
||||||
|
// mask("contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents(musicPlaylistShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask),gridRenderer(continuations,items.$musicTwoRowItemRendererMask))")
|
||||||
|
}.body<BrowseResponse>()
|
||||||
|
|
||||||
|
val sectionListRendererContent = response
|
||||||
|
.contents
|
||||||
|
?.singleColumnBrowseResultsRenderer
|
||||||
|
?.tabs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.tabRenderer
|
||||||
|
?.content
|
||||||
|
?.sectionListRenderer
|
||||||
|
?.contents
|
||||||
|
?.firstOrNull()
|
||||||
|
|
||||||
|
itemsPageFromMusicShelRendererOrGridRenderer(
|
||||||
|
musicShelfRenderer = sectionListRendererContent
|
||||||
|
?.musicShelfRenderer,
|
||||||
|
gridRenderer = sectionListRendererContent
|
||||||
|
?.gridRenderer,
|
||||||
|
fromMusicResponsiveListItemRenderer = fromMusicResponsiveListItemRenderer,
|
||||||
|
fromMusicTwoRowItemRenderer = fromMusicTwoRowItemRenderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T : Innertube.Item> Innertube.itemsPage(
|
||||||
|
body: ContinuationBody,
|
||||||
|
fromMusicResponsiveListItemRenderer: (MusicResponsiveListItemRenderer) -> T? = { null },
|
||||||
|
fromMusicTwoRowItemRenderer: (MusicTwoRowItemRenderer) -> T? = { null },
|
||||||
|
) = runCatchingNonCancellable {
|
||||||
|
val response = client.post(browse) {
|
||||||
|
setBody(body)
|
||||||
|
// mask("contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents(musicPlaylistShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask),gridRenderer(continuations,items.$musicTwoRowItemRendererMask))")
|
||||||
|
}.body<ContinuationResponse>()
|
||||||
|
|
||||||
|
itemsPageFromMusicShelRendererOrGridRenderer(
|
||||||
|
musicShelfRenderer = response
|
||||||
|
.continuationContents
|
||||||
|
?.musicShelfContinuation,
|
||||||
|
gridRenderer = null,
|
||||||
|
fromMusicResponsiveListItemRenderer = fromMusicResponsiveListItemRenderer,
|
||||||
|
fromMusicTwoRowItemRenderer = fromMusicTwoRowItemRenderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T : Innertube.Item> itemsPageFromMusicShelRendererOrGridRenderer(
|
||||||
|
musicShelfRenderer: MusicShelfRenderer?,
|
||||||
|
gridRenderer: GridRenderer?,
|
||||||
|
fromMusicResponsiveListItemRenderer: (MusicResponsiveListItemRenderer) -> T?,
|
||||||
|
fromMusicTwoRowItemRenderer: (MusicTwoRowItemRenderer) -> T?,
|
||||||
|
): Innertube.ItemsPage<T>? {
|
||||||
|
return if (musicShelfRenderer != null) {
|
||||||
|
Innertube.ItemsPage(
|
||||||
|
continuation = musicShelfRenderer
|
||||||
|
.continuations
|
||||||
|
?.firstOrNull()
|
||||||
|
?.nextContinuationData
|
||||||
|
?.continuation,
|
||||||
|
items = musicShelfRenderer
|
||||||
|
.contents
|
||||||
|
?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
||||||
|
?.mapNotNull(fromMusicResponsiveListItemRenderer)
|
||||||
|
)
|
||||||
|
} else if (gridRenderer != null) {
|
||||||
|
Innertube.ItemsPage(
|
||||||
|
continuation = null,
|
||||||
|
items = gridRenderer
|
||||||
|
.items
|
||||||
|
?.mapNotNull(GridRenderer.Item::musicTwoRowItemRenderer)
|
||||||
|
?.mapNotNull(fromMusicTwoRowItemRenderer)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.BrowseResponse
|
||||||
|
import com.player.musicoo.innertube.models.NextResponse
|
||||||
|
import com.player.musicoo.innertube.models.bodies.BrowseBody
|
||||||
|
import com.player.musicoo.innertube.models.bodies.NextBody
|
||||||
|
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
|
||||||
|
|
||||||
|
suspend fun Innertube.lyrics(body: NextBody): Result<String?>? = runCatchingNonCancellable {
|
||||||
|
val nextResponse = client.post(next) {
|
||||||
|
setBody(body)
|
||||||
|
mask("contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer(endpoint,title)")
|
||||||
|
}.body<NextResponse>()
|
||||||
|
|
||||||
|
val browseId = nextResponse
|
||||||
|
.contents
|
||||||
|
?.singleColumnMusicWatchNextResultsRenderer
|
||||||
|
?.tabbedRenderer
|
||||||
|
?.watchNextTabbedResultsRenderer
|
||||||
|
?.tabs
|
||||||
|
?.getOrNull(1)
|
||||||
|
?.tabRenderer
|
||||||
|
?.endpoint
|
||||||
|
?.browseEndpoint
|
||||||
|
?.browseId
|
||||||
|
?: return@runCatchingNonCancellable null
|
||||||
|
|
||||||
|
val response = client.post(browse) {
|
||||||
|
setBody(BrowseBody(browseId = browseId))
|
||||||
|
mask("contents.sectionListRenderer.contents.musicDescriptionShelfRenderer.description")
|
||||||
|
}.body<BrowseResponse>()
|
||||||
|
|
||||||
|
response.contents
|
||||||
|
?.sectionListRenderer
|
||||||
|
?.contents
|
||||||
|
?.firstOrNull()
|
||||||
|
?.musicDescriptionShelfRenderer
|
||||||
|
?.description
|
||||||
|
?.text
|
||||||
|
}
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.ContinuationResponse
|
||||||
|
import com.player.musicoo.innertube.models.NextResponse
|
||||||
|
import com.player.musicoo.innertube.models.bodies.ContinuationBody
|
||||||
|
import com.player.musicoo.innertube.models.bodies.NextBody
|
||||||
|
import com.player.musicoo.innertube.utils.from
|
||||||
|
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun Innertube.nextPage(body: NextBody): Result<Innertube.NextPage>? =
|
||||||
|
runCatchingNonCancellable {
|
||||||
|
val response = client.post(next) {
|
||||||
|
setBody(body)
|
||||||
|
mask("contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer.content.musicQueueRenderer.content.playlistPanelRenderer(continuations,contents(automixPreviewVideoRenderer,$playlistPanelVideoRendererMask))")
|
||||||
|
}.body<NextResponse>()
|
||||||
|
|
||||||
|
val tabs = response
|
||||||
|
.contents
|
||||||
|
?.singleColumnMusicWatchNextResultsRenderer
|
||||||
|
?.tabbedRenderer
|
||||||
|
?.watchNextTabbedResultsRenderer
|
||||||
|
?.tabs
|
||||||
|
|
||||||
|
val playlistPanelRenderer = tabs
|
||||||
|
?.getOrNull(0)
|
||||||
|
?.tabRenderer
|
||||||
|
?.content
|
||||||
|
?.musicQueueRenderer
|
||||||
|
?.content
|
||||||
|
?.playlistPanelRenderer
|
||||||
|
|
||||||
|
if (body.playlistId == null) {
|
||||||
|
val endpoint = playlistPanelRenderer
|
||||||
|
?.contents
|
||||||
|
?.lastOrNull()
|
||||||
|
?.automixPreviewVideoRenderer
|
||||||
|
?.content
|
||||||
|
?.automixPlaylistVideoRenderer
|
||||||
|
?.navigationEndpoint
|
||||||
|
?.watchPlaylistEndpoint
|
||||||
|
|
||||||
|
if (endpoint != null) {
|
||||||
|
return nextPage(
|
||||||
|
body.copy(
|
||||||
|
playlistId = endpoint.playlistId,
|
||||||
|
params = endpoint.params
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Innertube.NextPage(
|
||||||
|
playlistId = body.playlistId,
|
||||||
|
playlistSetVideoId = body.playlistSetVideoId,
|
||||||
|
params = body.params,
|
||||||
|
itemsPage = playlistPanelRenderer
|
||||||
|
?.toSongsPage()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun Innertube.nextPage(body: ContinuationBody) = runCatchingNonCancellable {
|
||||||
|
val response = client.post(next) {
|
||||||
|
setBody(body)
|
||||||
|
mask("continuationContents.playlistPanelContinuation(continuations,contents.$playlistPanelVideoRendererMask)")
|
||||||
|
}.body<ContinuationResponse>()
|
||||||
|
|
||||||
|
response
|
||||||
|
.continuationContents
|
||||||
|
?.playlistPanelContinuation
|
||||||
|
?.toSongsPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer?.toSongsPage() =
|
||||||
|
Innertube.ItemsPage(
|
||||||
|
items = this
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer.Content::playlistPanelVideoRenderer)
|
||||||
|
?.mapNotNull(Innertube.SongItem::from),
|
||||||
|
continuation = this
|
||||||
|
?.continuations
|
||||||
|
?.firstOrNull()
|
||||||
|
?.nextContinuationData
|
||||||
|
?.continuation
|
||||||
|
)
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.contentType
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.Context
|
||||||
|
import com.player.musicoo.innertube.models.PlayerResponse
|
||||||
|
import com.player.musicoo.innertube.models.bodies.PlayerBody
|
||||||
|
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
suspend fun Innertube.player(body: PlayerBody) = runCatchingNonCancellable {
|
||||||
|
val response = client.post(player) {
|
||||||
|
setBody(body)
|
||||||
|
mask("playabilityStatus.status,playerConfig.audioConfig,streamingData.adaptiveFormats,videoDetails.videoId")
|
||||||
|
}.body<PlayerResponse>()
|
||||||
|
|
||||||
|
if (response.playabilityStatus?.status == "OK") {
|
||||||
|
response
|
||||||
|
} else {
|
||||||
|
@Serializable
|
||||||
|
data class AudioStream(
|
||||||
|
val url: String,
|
||||||
|
val bitrate: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PipedResponse(
|
||||||
|
val audioStreams: List<AudioStream>
|
||||||
|
)
|
||||||
|
|
||||||
|
val safePlayerResponse = client.post(player) {
|
||||||
|
setBody(
|
||||||
|
body.copy(
|
||||||
|
context = Context.DefaultAgeRestrictionBypass.copy(
|
||||||
|
thirdParty = Context.ThirdParty(
|
||||||
|
embedUrl = "https://www.youtube.com/watch?v=${body.videoId}"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
mask("playabilityStatus.status,playerConfig.audioConfig,streamingData.adaptiveFormats,videoDetails.videoId")
|
||||||
|
}.body<PlayerResponse>()
|
||||||
|
|
||||||
|
if (safePlayerResponse.playabilityStatus?.status != "OK") {
|
||||||
|
return@runCatchingNonCancellable response
|
||||||
|
}
|
||||||
|
|
||||||
|
val audioStreams = client.get("https://watchapi.whatever.social/streams/${body.videoId}") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
}.body<PipedResponse>().audioStreams
|
||||||
|
|
||||||
|
safePlayerResponse.copy(
|
||||||
|
streamingData = safePlayerResponse.streamingData?.copy(
|
||||||
|
adaptiveFormats = safePlayerResponse.streamingData.adaptiveFormats?.map { adaptiveFormat ->
|
||||||
|
adaptiveFormat.copy(
|
||||||
|
url = audioStreams.find { it.bitrate == adaptiveFormat.bitrate }?.url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.BrowseResponse
|
||||||
|
import com.player.musicoo.innertube.models.ContinuationResponse
|
||||||
|
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
|
||||||
|
import com.player.musicoo.innertube.models.MusicShelfRenderer
|
||||||
|
import com.player.musicoo.innertube.models.bodies.BrowseBody
|
||||||
|
import com.player.musicoo.innertube.models.bodies.ContinuationBody
|
||||||
|
import com.player.musicoo.innertube.utils.from
|
||||||
|
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
|
||||||
|
|
||||||
|
suspend fun Innertube.playlistPage(body: BrowseBody) = runCatchingNonCancellable {
|
||||||
|
val response = client.post(browse) {
|
||||||
|
setBody(body)
|
||||||
|
mask("contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents(musicPlaylistShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask),musicCarouselShelfRenderer.contents.$musicTwoRowItemRendererMask),header.musicDetailHeaderRenderer(title,subtitle,thumbnail),microformat")
|
||||||
|
}.body<BrowseResponse>()
|
||||||
|
|
||||||
|
val musicDetailHeaderRenderer = response
|
||||||
|
.header
|
||||||
|
?.musicDetailHeaderRenderer
|
||||||
|
|
||||||
|
val sectionListRendererContents = response
|
||||||
|
.contents
|
||||||
|
?.singleColumnBrowseResultsRenderer
|
||||||
|
?.tabs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.tabRenderer
|
||||||
|
?.content
|
||||||
|
?.sectionListRenderer
|
||||||
|
?.contents
|
||||||
|
|
||||||
|
val musicShelfRenderer = sectionListRendererContents
|
||||||
|
?.firstOrNull()
|
||||||
|
?.musicShelfRenderer
|
||||||
|
|
||||||
|
val musicCarouselShelfRenderer = sectionListRendererContents
|
||||||
|
?.getOrNull(1)
|
||||||
|
?.musicCarouselShelfRenderer
|
||||||
|
|
||||||
|
Innertube.PlaylistOrAlbumPage(
|
||||||
|
title = musicDetailHeaderRenderer
|
||||||
|
?.title
|
||||||
|
?.text,
|
||||||
|
thumbnail = musicDetailHeaderRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.musicThumbnailRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.thumbnails
|
||||||
|
?.firstOrNull(),
|
||||||
|
authors = musicDetailHeaderRenderer
|
||||||
|
?.subtitle
|
||||||
|
?.splitBySeparator()
|
||||||
|
?.getOrNull(1)
|
||||||
|
?.map(Innertube::Info),
|
||||||
|
year = musicDetailHeaderRenderer
|
||||||
|
?.subtitle
|
||||||
|
?.splitBySeparator()
|
||||||
|
?.getOrNull(2)
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text,
|
||||||
|
url = response
|
||||||
|
.microformat
|
||||||
|
?.microformatDataRenderer
|
||||||
|
?.urlCanonical,
|
||||||
|
songsPage = musicShelfRenderer
|
||||||
|
?.toSongsPage(),
|
||||||
|
otherVersions = musicCarouselShelfRenderer
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
|
||||||
|
?.mapNotNull(Innertube.AlbumItem::from)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun Innertube.playlistPage(body: ContinuationBody) = runCatchingNonCancellable {
|
||||||
|
val response = client.post(browse) {
|
||||||
|
setBody(body)
|
||||||
|
mask("continuationContents.musicPlaylistShelfContinuation(continuations,contents.$musicResponsiveListItemRendererMask)")
|
||||||
|
}.body<ContinuationResponse>()
|
||||||
|
|
||||||
|
response
|
||||||
|
.continuationContents
|
||||||
|
?.musicShelfContinuation
|
||||||
|
?.toSongsPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MusicShelfRenderer?.toSongsPage() =
|
||||||
|
Innertube.ItemsPage(
|
||||||
|
items = this
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
||||||
|
?.mapNotNull(Innertube.SongItem::from),
|
||||||
|
continuation = this
|
||||||
|
?.continuations
|
||||||
|
?.firstOrNull()
|
||||||
|
?.nextContinuationData
|
||||||
|
?.continuation
|
||||||
|
)
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.GetQueueResponse
|
||||||
|
import com.player.musicoo.innertube.models.bodies.QueueBody
|
||||||
|
import com.player.musicoo.innertube.utils.from
|
||||||
|
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
|
||||||
|
|
||||||
|
suspend fun Innertube.queue(body: QueueBody) = runCatchingNonCancellable {
|
||||||
|
val response = client.post(queue) {
|
||||||
|
setBody(body)
|
||||||
|
mask("queueDatas.content.$playlistPanelVideoRendererMask")
|
||||||
|
}.body<GetQueueResponse>()
|
||||||
|
|
||||||
|
response
|
||||||
|
.queueDatas
|
||||||
|
?.mapNotNull { queueData ->
|
||||||
|
queueData
|
||||||
|
.content
|
||||||
|
?.playlistPanelVideoRenderer
|
||||||
|
?.let(Innertube.SongItem::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun Innertube.song(videoId: String): Result<Innertube.SongItem?>? =
|
||||||
|
queue(QueueBody(videoIds = listOf(videoId)))?.map { it?.firstOrNull() }
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.BrowseResponse
|
||||||
|
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
|
||||||
|
import com.player.musicoo.innertube.models.NextResponse
|
||||||
|
import com.player.musicoo.innertube.models.bodies.BrowseBody
|
||||||
|
import com.player.musicoo.innertube.models.bodies.NextBody
|
||||||
|
import com.player.musicoo.innertube.utils.findSectionByStrapline
|
||||||
|
import com.player.musicoo.innertube.utils.findSectionByTitle
|
||||||
|
import com.player.musicoo.innertube.utils.from
|
||||||
|
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
|
||||||
|
|
||||||
|
suspend fun Innertube.relatedPage(body: NextBody) = runCatchingNonCancellable {
|
||||||
|
val nextResponse = client.post(next) {
|
||||||
|
setBody(body)
|
||||||
|
mask("contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer(endpoint,title)")
|
||||||
|
}.body<NextResponse>()
|
||||||
|
|
||||||
|
val browseId = nextResponse
|
||||||
|
.contents
|
||||||
|
?.singleColumnMusicWatchNextResultsRenderer
|
||||||
|
?.tabbedRenderer
|
||||||
|
?.watchNextTabbedResultsRenderer
|
||||||
|
?.tabs
|
||||||
|
?.getOrNull(2)
|
||||||
|
?.tabRenderer
|
||||||
|
?.endpoint
|
||||||
|
?.browseEndpoint
|
||||||
|
?.browseId
|
||||||
|
?: return@runCatchingNonCancellable null
|
||||||
|
|
||||||
|
val response = client.post(browse) {
|
||||||
|
setBody(BrowseBody(browseId = browseId))
|
||||||
|
mask("contents.sectionListRenderer.contents.musicCarouselShelfRenderer(header.musicCarouselShelfBasicHeaderRenderer(title,strapline),contents($musicResponsiveListItemRendererMask,$musicTwoRowItemRendererMask))")
|
||||||
|
}.body<BrowseResponse>()
|
||||||
|
|
||||||
|
val sectionListRenderer = response
|
||||||
|
.contents
|
||||||
|
?.sectionListRenderer
|
||||||
|
|
||||||
|
Innertube.RelatedPage(
|
||||||
|
songs = sectionListRenderer
|
||||||
|
?.findSectionByTitle("You might also like")
|
||||||
|
?.musicCarouselShelfRenderer
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicResponsiveListItemRenderer)
|
||||||
|
?.mapNotNull(Innertube.SongItem::from),
|
||||||
|
playlists = sectionListRenderer
|
||||||
|
?.findSectionByTitle("Recommended playlists")
|
||||||
|
?.musicCarouselShelfRenderer
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
|
||||||
|
?.mapNotNull(Innertube.PlaylistItem::from)
|
||||||
|
?.sortedByDescending { it.channel?.name == "YouTube Music" },
|
||||||
|
albums = sectionListRenderer
|
||||||
|
?.findSectionByStrapline("MORE FROM")
|
||||||
|
?.musicCarouselShelfRenderer
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
|
||||||
|
?.mapNotNull(Innertube.AlbumItem::from),
|
||||||
|
artists = sectionListRenderer
|
||||||
|
?.findSectionByTitle("Similar artists")
|
||||||
|
?.musicCarouselShelfRenderer
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
|
||||||
|
?.mapNotNull(Innertube.ArtistItem::from),
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.ContinuationResponse
|
||||||
|
import com.player.musicoo.innertube.models.MusicShelfRenderer
|
||||||
|
import com.player.musicoo.innertube.models.SearchResponse
|
||||||
|
import com.player.musicoo.innertube.models.bodies.ContinuationBody
|
||||||
|
import com.player.musicoo.innertube.models.bodies.SearchBody
|
||||||
|
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
|
||||||
|
|
||||||
|
suspend fun <T : Innertube.Item> Innertube.searchPage(
|
||||||
|
body: SearchBody,
|
||||||
|
fromMusicShelfRendererContent: (MusicShelfRenderer.Content) -> T?
|
||||||
|
) = runCatchingNonCancellable {
|
||||||
|
val response = client.post(search) {
|
||||||
|
setBody(body)
|
||||||
|
mask("contents.tabbedSearchResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents.musicShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask)")
|
||||||
|
}.body<SearchResponse>()
|
||||||
|
|
||||||
|
response
|
||||||
|
.contents
|
||||||
|
?.tabbedSearchResultsRenderer
|
||||||
|
?.tabs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.tabRenderer
|
||||||
|
?.content
|
||||||
|
?.sectionListRenderer
|
||||||
|
?.contents
|
||||||
|
?.lastOrNull()
|
||||||
|
?.musicShelfRenderer
|
||||||
|
?.toItemsPage(fromMusicShelfRendererContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T : Innertube.Item> Innertube.searchPage(
|
||||||
|
body: ContinuationBody,
|
||||||
|
fromMusicShelfRendererContent: (MusicShelfRenderer.Content) -> T?
|
||||||
|
) = runCatchingNonCancellable {
|
||||||
|
val response = client.post(search) {
|
||||||
|
setBody(body)
|
||||||
|
mask("continuationContents.musicShelfContinuation(continuations,contents.$musicResponsiveListItemRendererMask)")
|
||||||
|
}.body<ContinuationResponse>()
|
||||||
|
|
||||||
|
response
|
||||||
|
.continuationContents
|
||||||
|
?.musicShelfContinuation
|
||||||
|
?.toItemsPage(fromMusicShelfRendererContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T : Innertube.Item> MusicShelfRenderer?.toItemsPage(mapper: (MusicShelfRenderer.Content) -> T?) =
|
||||||
|
Innertube.ItemsPage(
|
||||||
|
items = this
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(mapper),
|
||||||
|
continuation = this
|
||||||
|
?.continuations
|
||||||
|
?.firstOrNull()
|
||||||
|
?.nextContinuationData
|
||||||
|
?.continuation
|
||||||
|
)
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.player.musicoo.innertube.requests
|
||||||
|
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.SearchSuggestionsResponse
|
||||||
|
import com.player.musicoo.innertube.models.bodies.SearchSuggestionsBody
|
||||||
|
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
|
||||||
|
|
||||||
|
suspend fun Innertube.searchSuggestions(body: SearchSuggestionsBody) = runCatchingNonCancellable {
|
||||||
|
val response = client.post(searchSuggestions) {
|
||||||
|
setBody(body)
|
||||||
|
mask("contents.searchSuggestionsSectionRenderer.contents.searchSuggestionRenderer.navigationEndpoint.searchEndpoint.query")
|
||||||
|
}.body<SearchSuggestionsResponse>()
|
||||||
|
|
||||||
|
response
|
||||||
|
.contents
|
||||||
|
?.firstOrNull()
|
||||||
|
?.searchSuggestionsSectionRenderer
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull { content ->
|
||||||
|
content
|
||||||
|
.searchSuggestionRenderer
|
||||||
|
?.navigationEndpoint
|
||||||
|
?.searchEndpoint
|
||||||
|
?.query
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.player.musicoo.innertube.utils
|
||||||
|
|
||||||
|
import io.ktor.client.plugins.compression.ContentEncoder
|
||||||
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
|
import io.ktor.utils.io.jvm.javaio.toByteReadChannel
|
||||||
|
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import org.brotli.dec.BrotliInputStream
|
||||||
|
|
||||||
|
internal object BrotliEncoder : ContentEncoder {
|
||||||
|
override val name: String = "br"
|
||||||
|
|
||||||
|
override fun CoroutineScope.encode(source: ByteReadChannel) =
|
||||||
|
error("BrotliOutputStream not available (https://github.com/google/brotli/issues/715)")
|
||||||
|
|
||||||
|
override fun CoroutineScope.decode(source: ByteReadChannel): ByteReadChannel =
|
||||||
|
BrotliInputStream(source.toInputStream()).toByteReadChannel()
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package com.player.musicoo.innertube.utils
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.MusicResponsiveListItemRenderer
|
||||||
|
import com.player.musicoo.innertube.models.NavigationEndpoint
|
||||||
|
import com.player.musicoo.innertube.models.Runs
|
||||||
|
|
||||||
|
fun Innertube.SongItem.Companion.from(renderer: MusicResponsiveListItemRenderer): Innertube.SongItem? {
|
||||||
|
return Innertube.SongItem(
|
||||||
|
info = renderer
|
||||||
|
.flexColumns
|
||||||
|
.getOrNull(0)
|
||||||
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
|
?.text
|
||||||
|
?.runs
|
||||||
|
?.getOrNull(0)
|
||||||
|
?.let(Innertube::Info),
|
||||||
|
authors = renderer
|
||||||
|
.flexColumns
|
||||||
|
.getOrNull(1)
|
||||||
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
|
?.text
|
||||||
|
?.runs
|
||||||
|
?.map<Runs.Run, Innertube.Info<NavigationEndpoint.Endpoint.Browse>>(Innertube::Info)
|
||||||
|
?.takeIf(List<Any>::isNotEmpty),
|
||||||
|
durationText = renderer
|
||||||
|
.fixedColumns
|
||||||
|
?.getOrNull(0)
|
||||||
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
|
?.text
|
||||||
|
?.runs
|
||||||
|
?.getOrNull(0)
|
||||||
|
?.text,
|
||||||
|
album = renderer
|
||||||
|
.flexColumns
|
||||||
|
.getOrNull(2)
|
||||||
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
|
?.text
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.let(Innertube::Info),
|
||||||
|
thumbnail = renderer
|
||||||
|
.thumbnail
|
||||||
|
?.musicThumbnailRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.thumbnails
|
||||||
|
?.firstOrNull()
|
||||||
|
).takeIf { it.info?.endpoint?.videoId != null }
|
||||||
|
}
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
package com.player.musicoo.innertube.utils
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.MusicShelfRenderer
|
||||||
|
import com.player.musicoo.innertube.models.NavigationEndpoint
|
||||||
|
|
||||||
|
fun Innertube.SongItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.SongItem? {
|
||||||
|
val (mainRuns, otherRuns) = content.runs
|
||||||
|
|
||||||
|
// Possible configurations:
|
||||||
|
// "song" • author(s) • album • duration
|
||||||
|
// "song" • author(s) • duration
|
||||||
|
// author(s) • album • duration
|
||||||
|
// author(s) • duration
|
||||||
|
|
||||||
|
val album: Innertube.Info<NavigationEndpoint.Endpoint.Browse>? = otherRuns
|
||||||
|
.getOrNull(otherRuns.lastIndex - 1)
|
||||||
|
?.firstOrNull()
|
||||||
|
?.takeIf { run ->
|
||||||
|
run
|
||||||
|
.navigationEndpoint
|
||||||
|
?.browseEndpoint
|
||||||
|
?.type == "MUSIC_PAGE_TYPE_ALBUM"
|
||||||
|
}
|
||||||
|
?.let(Innertube::Info)
|
||||||
|
|
||||||
|
return Innertube.SongItem(
|
||||||
|
info = mainRuns
|
||||||
|
.firstOrNull()
|
||||||
|
?.let(Innertube::Info),
|
||||||
|
authors = otherRuns
|
||||||
|
.getOrNull(otherRuns.lastIndex - if (album == null) 1 else 2)
|
||||||
|
?.map(Innertube::Info),
|
||||||
|
album = album,
|
||||||
|
durationText = otherRuns
|
||||||
|
.lastOrNull()
|
||||||
|
?.firstOrNull()?.text,
|
||||||
|
thumbnail = content
|
||||||
|
.thumbnail
|
||||||
|
).takeIf { it.info?.endpoint?.videoId != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Innertube.VideoItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.VideoItem? {
|
||||||
|
val (mainRuns, otherRuns) = content.runs
|
||||||
|
|
||||||
|
return Innertube.VideoItem(
|
||||||
|
info = mainRuns
|
||||||
|
.firstOrNull()
|
||||||
|
?.let(Innertube::Info),
|
||||||
|
authors = otherRuns
|
||||||
|
.getOrNull(otherRuns.lastIndex - 2)
|
||||||
|
?.map(Innertube::Info),
|
||||||
|
viewsText = otherRuns
|
||||||
|
.getOrNull(otherRuns.lastIndex - 1)
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text,
|
||||||
|
durationText = otherRuns
|
||||||
|
.getOrNull(otherRuns.lastIndex)
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text,
|
||||||
|
thumbnail = content
|
||||||
|
.thumbnail
|
||||||
|
).takeIf { it.info?.endpoint?.videoId != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Innertube.AlbumItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.AlbumItem? {
|
||||||
|
val (mainRuns, otherRuns) = content.runs
|
||||||
|
|
||||||
|
return Innertube.AlbumItem(
|
||||||
|
info = Innertube.Info(
|
||||||
|
name = mainRuns
|
||||||
|
.firstOrNull()
|
||||||
|
?.text,
|
||||||
|
endpoint = content
|
||||||
|
.musicResponsiveListItemRenderer
|
||||||
|
?.navigationEndpoint
|
||||||
|
?.browseEndpoint
|
||||||
|
),
|
||||||
|
authors = otherRuns
|
||||||
|
.getOrNull(otherRuns.lastIndex - 1)
|
||||||
|
?.map(Innertube::Info),
|
||||||
|
year = otherRuns
|
||||||
|
.getOrNull(otherRuns.lastIndex)
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text,
|
||||||
|
thumbnail = content
|
||||||
|
.thumbnail
|
||||||
|
).takeIf { it.info?.endpoint?.browseId != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Innertube.ArtistItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.ArtistItem? {
|
||||||
|
val (mainRuns, otherRuns) = content.runs
|
||||||
|
|
||||||
|
return Innertube.ArtistItem(
|
||||||
|
info = Innertube.Info(
|
||||||
|
name = mainRuns
|
||||||
|
.firstOrNull()
|
||||||
|
?.text,
|
||||||
|
endpoint = content
|
||||||
|
.musicResponsiveListItemRenderer
|
||||||
|
?.navigationEndpoint
|
||||||
|
?.browseEndpoint
|
||||||
|
),
|
||||||
|
subscribersCountText = otherRuns
|
||||||
|
.lastOrNull()
|
||||||
|
?.last()
|
||||||
|
?.text,
|
||||||
|
thumbnail = content
|
||||||
|
.thumbnail
|
||||||
|
).takeIf { it.info?.endpoint?.browseId != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Innertube.PlaylistItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.PlaylistItem? {
|
||||||
|
val (mainRuns, otherRuns) = content.runs
|
||||||
|
|
||||||
|
return Innertube.PlaylistItem(
|
||||||
|
info = Innertube.Info(
|
||||||
|
name = mainRuns
|
||||||
|
.firstOrNull()
|
||||||
|
?.text,
|
||||||
|
endpoint = content
|
||||||
|
.musicResponsiveListItemRenderer
|
||||||
|
?.navigationEndpoint
|
||||||
|
?.browseEndpoint
|
||||||
|
),
|
||||||
|
channel = otherRuns
|
||||||
|
.firstOrNull()
|
||||||
|
?.firstOrNull()
|
||||||
|
?.let(Innertube::Info),
|
||||||
|
songCount = otherRuns
|
||||||
|
.lastOrNull()
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text
|
||||||
|
?.split(' ')
|
||||||
|
?.firstOrNull()
|
||||||
|
?.toIntOrNull(),
|
||||||
|
thumbnail = content
|
||||||
|
.thumbnail
|
||||||
|
).takeIf { it.info?.endpoint?.browseId != null }
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
package com.player.musicoo.innertube.utils
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.MusicTwoRowItemRenderer
|
||||||
|
|
||||||
|
fun Innertube.AlbumItem.Companion.from(renderer: MusicTwoRowItemRenderer): Innertube.AlbumItem? {
|
||||||
|
return Innertube.AlbumItem(
|
||||||
|
info = renderer
|
||||||
|
.title
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.let(Innertube::Info),
|
||||||
|
authors = null,
|
||||||
|
year = renderer
|
||||||
|
.subtitle
|
||||||
|
?.runs
|
||||||
|
?.lastOrNull()
|
||||||
|
?.text,
|
||||||
|
thumbnail = renderer
|
||||||
|
.thumbnailRenderer
|
||||||
|
?.musicThumbnailRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.thumbnails
|
||||||
|
?.firstOrNull()
|
||||||
|
).takeIf { it.info?.endpoint?.browseId != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Innertube.ArtistItem.Companion.from(renderer: MusicTwoRowItemRenderer): Innertube.ArtistItem? {
|
||||||
|
return Innertube.ArtistItem(
|
||||||
|
info = renderer
|
||||||
|
.title
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.let(Innertube::Info),
|
||||||
|
subscribersCountText = renderer
|
||||||
|
.subtitle
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text,
|
||||||
|
thumbnail = renderer
|
||||||
|
.thumbnailRenderer
|
||||||
|
?.musicThumbnailRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.thumbnails
|
||||||
|
?.firstOrNull()
|
||||||
|
).takeIf { it.info?.endpoint?.browseId != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Innertube.PlaylistItem.Companion.from(renderer: MusicTwoRowItemRenderer): Innertube.PlaylistItem? {
|
||||||
|
return Innertube.PlaylistItem(
|
||||||
|
info = renderer
|
||||||
|
.title
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.let(Innertube::Info),
|
||||||
|
channel = renderer
|
||||||
|
.subtitle
|
||||||
|
?.runs
|
||||||
|
?.getOrNull(2)
|
||||||
|
?.let(Innertube::Info),
|
||||||
|
songCount = renderer
|
||||||
|
.subtitle
|
||||||
|
?.runs
|
||||||
|
?.getOrNull(4)
|
||||||
|
?.text
|
||||||
|
?.split(' ')
|
||||||
|
?.firstOrNull()
|
||||||
|
?.toIntOrNull(),
|
||||||
|
thumbnail = renderer
|
||||||
|
.thumbnailRenderer
|
||||||
|
?.musicThumbnailRenderer
|
||||||
|
?.thumbnail
|
||||||
|
?.thumbnails
|
||||||
|
?.firstOrNull()
|
||||||
|
).takeIf { it.info?.endpoint?.browseId != null }
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package com.player.musicoo.innertube.utils
|
||||||
|
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.PlaylistPanelVideoRenderer
|
||||||
|
|
||||||
|
fun Innertube.SongItem.Companion.from(renderer: PlaylistPanelVideoRenderer): Innertube.SongItem? {
|
||||||
|
return Innertube.SongItem(
|
||||||
|
info = Innertube.Info(
|
||||||
|
name = renderer
|
||||||
|
.title
|
||||||
|
?.text,
|
||||||
|
endpoint = renderer
|
||||||
|
.navigationEndpoint
|
||||||
|
?.watchEndpoint
|
||||||
|
),
|
||||||
|
authors = renderer
|
||||||
|
.longBylineText
|
||||||
|
?.splitBySeparator()
|
||||||
|
?.getOrNull(0)
|
||||||
|
?.map(Innertube::Info),
|
||||||
|
album = renderer
|
||||||
|
.longBylineText
|
||||||
|
?.splitBySeparator()
|
||||||
|
?.getOrNull(1)
|
||||||
|
?.getOrNull(0)
|
||||||
|
?.let(Innertube::Info),
|
||||||
|
thumbnail = renderer
|
||||||
|
.thumbnail
|
||||||
|
?.thumbnails
|
||||||
|
?.getOrNull(0),
|
||||||
|
durationText = renderer
|
||||||
|
.lengthText
|
||||||
|
?.text
|
||||||
|
).takeIf { it.info?.endpoint?.videoId != null }
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
package com.player.musicoo.innertube.utils
|
||||||
|
|
||||||
|
import io.ktor.utils.io.CancellationException
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.innertube.models.SectionListRenderer
|
||||||
|
|
||||||
|
internal fun SectionListRenderer.findSectionByTitle(text: String): SectionListRenderer.Content? {
|
||||||
|
return contents?.find { content ->
|
||||||
|
val title = content
|
||||||
|
.musicCarouselShelfRenderer
|
||||||
|
?.header
|
||||||
|
?.musicCarouselShelfBasicHeaderRenderer
|
||||||
|
?.title
|
||||||
|
?: content
|
||||||
|
.musicShelfRenderer
|
||||||
|
?.title
|
||||||
|
|
||||||
|
title
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text == text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun SectionListRenderer.findSectionByStrapline(text: String): SectionListRenderer.Content? {
|
||||||
|
return contents?.find { content ->
|
||||||
|
content
|
||||||
|
.musicCarouselShelfRenderer
|
||||||
|
?.header
|
||||||
|
?.musicCarouselShelfBasicHeaderRenderer
|
||||||
|
?.strapline
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text == text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun <R> runCatchingNonCancellable(block: () -> R): Result<R>? {
|
||||||
|
val result = runCatching(block)
|
||||||
|
return when (result.exceptionOrNull()) {
|
||||||
|
is CancellationException -> null
|
||||||
|
else -> result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
infix operator fun <T : Innertube.Item> Innertube.ItemsPage<T>?.plus(other: Innertube.ItemsPage<T>) =
|
||||||
|
other.copy(
|
||||||
|
items = (this?.items?.plus(other.items ?: emptyList())
|
||||||
|
?: other.items)?.distinctBy(Innertube.Item::key)
|
||||||
|
)
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.player.musicoo.innertube.utils
|
||||||
|
|
||||||
|
import io.ktor.client.plugins.compression.ContentEncoding
|
||||||
|
|
||||||
|
fun ContentEncoding.Config.brotli(quality: Float? = null) {
|
||||||
|
customEncoder(BrotliEncoder, quality)
|
||||||
|
}
|
||||||
@ -1,94 +0,0 @@
|
|||||||
package com.player.musicoo.service
|
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.media.AudioAttributes
|
|
||||||
import android.media.MediaPlayer
|
|
||||||
import android.os.Binder
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.player.musicoo.R
|
|
||||||
import com.player.musicoo.activity.MainActivity
|
|
||||||
|
|
||||||
class AudioPlayerService : Service() {
|
|
||||||
|
|
||||||
private var mediaPlayer: MediaPlayer? = null
|
|
||||||
private val binder = AudioPlayerBinder()
|
|
||||||
|
|
||||||
inner class AudioPlayerBinder : Binder() {
|
|
||||||
fun getService(): AudioPlayerService = this@AudioPlayerService
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
|
||||||
return binder
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
fun playAudio(audioUri: String) {
|
|
||||||
mediaPlayer?.let {
|
|
||||||
it.stop()
|
|
||||||
it.reset() // 重置 MediaPlayer,确保处于空闲状态
|
|
||||||
it.release()
|
|
||||||
}
|
|
||||||
mediaPlayer = MediaPlayer().apply {
|
|
||||||
setAudioAttributes(
|
|
||||||
AudioAttributes.Builder()
|
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
val assetFileDescriptor = assets.openFd(audioUri)
|
|
||||||
setDataSource(
|
|
||||||
assetFileDescriptor.fileDescriptor,
|
|
||||||
assetFileDescriptor.startOffset,
|
|
||||||
assetFileDescriptor.length
|
|
||||||
)
|
|
||||||
prepareAsync()
|
|
||||||
setOnPreparedListener { start() }
|
|
||||||
isLooping = true // 开启重复播放
|
|
||||||
}
|
|
||||||
startForegroundService()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pauseAudio() {
|
|
||||||
mediaPlayer?.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
mediaPlayer?.release()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startForegroundService() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val channelId = "audio_player_channel"
|
|
||||||
val channelName = "Audio Player"
|
|
||||||
val notificationManager =
|
|
||||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
|
|
||||||
val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
|
|
||||||
val notificationIntent = Intent(this, MainActivity::class.java)
|
|
||||||
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent,
|
|
||||||
PendingIntent.FLAG_IMMUTABLE)
|
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(this, channelId)
|
|
||||||
.setContentTitle("正在播放音频")
|
|
||||||
.setContentText("点击以返回应用")
|
|
||||||
.setSmallIcon(R.mipmap.musicoo_logo_img)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
startForeground(1, notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
app/src/main/java/com/player/musicoo/sp/AppStore.kt
Normal file
19
app/src/main/java/com/player/musicoo/sp/AppStore.kt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package com.player.musicoo.sp
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.player.musicoo.sp.store.Store
|
||||||
|
import com.player.musicoo.sp.store.asStoreProvider
|
||||||
|
|
||||||
|
class AppStore(context: Context) {
|
||||||
|
private val store = Store(
|
||||||
|
context
|
||||||
|
.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
|
||||||
|
.asStoreProvider()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FILE_NAME = "music_oo_app"
|
||||||
|
}
|
||||||
|
}
|
||||||
60
app/src/main/java/com/player/musicoo/sp/store/Providers.kt
Normal file
60
app/src/main/java/com/player/musicoo/sp/store/Providers.kt
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package com.player.musicoo.sp.store
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
|
class SharedPreferenceProvider(private val preferences: SharedPreferences) : StoreProvider {
|
||||||
|
override fun getInt(key: String, defaultValue: Int): Int {
|
||||||
|
return preferences.getInt(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setInt(key: String, value: Int) {
|
||||||
|
preferences.edit {
|
||||||
|
putInt(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLong(key: String, defaultValue: Long): Long {
|
||||||
|
return preferences.getLong(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLong(key: String, value: Long) {
|
||||||
|
preferences.edit {
|
||||||
|
putLong(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(key: String, defaultValue: String): String {
|
||||||
|
return preferences.getString(key, defaultValue)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setString(key: String, value: String) {
|
||||||
|
preferences.edit {
|
||||||
|
putString(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStringSet(key: String, defaultValue: Set<String>): Set<String> {
|
||||||
|
return preferences.getStringSet(key, defaultValue)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setStringSet(key: String, value: Set<String>) {
|
||||||
|
preferences.edit {
|
||||||
|
putStringSet(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
|
||||||
|
return preferences.getBoolean(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setBoolean(key: String, value: Boolean) {
|
||||||
|
preferences.edit {
|
||||||
|
putBoolean(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SharedPreferences.asStoreProvider(): StoreProvider {
|
||||||
|
return SharedPreferenceProvider(this)
|
||||||
|
}
|
||||||
98
app/src/main/java/com/player/musicoo/sp/store/Store.kt
Normal file
98
app/src/main/java/com/player/musicoo/sp/store/Store.kt
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package com.player.musicoo.sp.store
|
||||||
|
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
class Store(val provider: StoreProvider) {
|
||||||
|
interface Delegate<T> {
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>): T
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun int(key: String, defaultValue: Int): Delegate<Int> {
|
||||||
|
return object : Delegate<Int> {
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
|
||||||
|
return provider.getInt(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
|
||||||
|
provider.setInt(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun long(key: String, defaultValue: Long): Delegate<Long> {
|
||||||
|
return object : Delegate<Long> {
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): Long {
|
||||||
|
return provider.getLong(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) {
|
||||||
|
provider.setLong(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun string(key: String, defaultValue: String): Delegate<String> {
|
||||||
|
return object : Delegate<String> {
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
|
||||||
|
return provider.getString(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
|
||||||
|
provider.setString(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stringSet(key: String, defaultValue: Set<String>): Delegate<Set<String>> {
|
||||||
|
return object : Delegate<Set<String>> {
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): Set<String> {
|
||||||
|
return provider.getStringSet(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set<String>) {
|
||||||
|
provider.setStringSet(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun boolean(key: String, defaultValue: Boolean): Delegate<Boolean> {
|
||||||
|
return object : Delegate<Boolean> {
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean {
|
||||||
|
return provider.getBoolean(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
|
||||||
|
provider.setBoolean(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : Enum<T>> enum(key: String, defaultValue: T, values: Array<T>): Delegate<T> {
|
||||||
|
return object : Delegate<T> {
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||||
|
val name = provider.getString(key, defaultValue.name)
|
||||||
|
|
||||||
|
return values.find { name == it.name } ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||||
|
provider.setString(key, value.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> typedString(key: String, from: (String) -> T?, to: (T?) -> String): Delegate<T?> {
|
||||||
|
return object : Delegate<T?> {
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): T? {
|
||||||
|
val value = provider.getString(key, to(null))
|
||||||
|
|
||||||
|
return from(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
|
||||||
|
provider.setString(key, to(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.player.musicoo.sp.store
|
||||||
|
|
||||||
|
interface StoreProvider {
|
||||||
|
fun getInt(key: String, defaultValue: Int): Int
|
||||||
|
fun setInt(key: String, value: Int)
|
||||||
|
|
||||||
|
fun getLong(key: String, defaultValue: Long): Long
|
||||||
|
fun setLong(key: String, value: Long)
|
||||||
|
|
||||||
|
fun getString(key: String, defaultValue: String): String
|
||||||
|
fun setString(key: String, value: String)
|
||||||
|
|
||||||
|
fun getStringSet(key: String, defaultValue: Set<String>): Set<String>
|
||||||
|
fun setStringSet(key: String, value: Set<String>)
|
||||||
|
|
||||||
|
fun getBoolean(key: String, defaultValue: Boolean): Boolean
|
||||||
|
fun setBoolean(key: String, value: Boolean)
|
||||||
|
}
|
||||||
6
app/src/main/java/com/player/musicoo/util/LogTag.kt
Normal file
6
app/src/main/java/com/player/musicoo/util/LogTag.kt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package com.player.musicoo.util
|
||||||
|
|
||||||
|
object LogTag {
|
||||||
|
const val VO_ACT_LOG = "vo-act—log"
|
||||||
|
const val VO_FRAGMENT_LOG = "vo_fragment_log"
|
||||||
|
}
|
||||||
19
app/src/main/java/com/player/musicoo/view/ModuleView.kt
Normal file
19
app/src/main/java/com/player/musicoo/view/ModuleView.kt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package com.player.musicoo.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
|
||||||
|
open class ModuleView : FrameLayout {
|
||||||
|
protected var contentView: View? = null
|
||||||
|
constructor(context: Context) : super(context)
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleAttr
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.player.musicoo.view
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.player.musicoo.App
|
||||||
|
import com.player.musicoo.R
|
||||||
|
import com.player.musicoo.adapter.ResponsiveListAdapter
|
||||||
|
import com.player.musicoo.adapter.SoundsOfNatureAdapter
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
import com.player.musicoo.util.GridSpacingItemDecoration
|
||||||
|
|
||||||
|
@SuppressLint("ViewConstructor")
|
||||||
|
class MusicResponsiveListView(context: Context, homePage: Innertube.HomePage) :
|
||||||
|
ModuleView(context) {
|
||||||
|
init {
|
||||||
|
contentView = inflate(getContext(), R.layout.music_list_layout, this)
|
||||||
|
val title = contentView?.findViewById<TextView>(R.id.title)
|
||||||
|
title?.text = homePage.title
|
||||||
|
|
||||||
|
val rv = contentView?.findViewById<RecyclerView>(R.id.rv)
|
||||||
|
|
||||||
|
val adapter = ResponsiveListAdapter(context, homePage.contents)
|
||||||
|
rv?.layoutManager = GridLayoutManager(context, 3, GridLayoutManager.HORIZONTAL, false)
|
||||||
|
rv?.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.player.musicoo.view
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.player.musicoo.R
|
||||||
|
import com.player.musicoo.adapter.ResponsiveListAdapter
|
||||||
|
import com.player.musicoo.adapter.TowRowListAdapter
|
||||||
|
import com.player.musicoo.innertube.Innertube
|
||||||
|
|
||||||
|
@SuppressLint("ViewConstructor")
|
||||||
|
class MusicTowRowListView(context: Context, homePage: Innertube.HomePage) : ModuleView(context) {
|
||||||
|
init {
|
||||||
|
contentView = inflate(getContext(), R.layout.music_list_layout, this)
|
||||||
|
val title = contentView?.findViewById<TextView>(R.id.title)
|
||||||
|
title?.text = homePage.title
|
||||||
|
|
||||||
|
val rv = contentView?.findViewById<RecyclerView>(R.id.rv)
|
||||||
|
|
||||||
|
val adapter = TowRowListAdapter(context, homePage.contents)
|
||||||
|
rv?.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
rv?.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
180
app/src/main/res/layout/activity_primary.xml
Normal file
180
app/src/main/res/layout/activity_primary.xml
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/main_bg_color">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/frame_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_above="@+id/bottom_layout"
|
||||||
|
android:layout_below="@+id/view" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/bottom_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.lihang.ShadowLayout
|
||||||
|
android:id="@+id/playing_status_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
app:hl_cornerRadius="36dp"
|
||||||
|
app:hl_layoutBackground="#FF80F988"
|
||||||
|
app:hl_shadowColor="#40040604"
|
||||||
|
app:hl_shadowOffsetX="0dp"
|
||||||
|
app:hl_shadowOffsetY="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="78dp"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:elevation="0dp"
|
||||||
|
app:cardCornerRadius="24dp"
|
||||||
|
app:cardElevation="0dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/audio_img"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:src="@mipmap/breathe" />
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<com.player.musicoo.view.CircularProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="54dp"
|
||||||
|
android:layout_height="54dp"
|
||||||
|
android:layout_centerInParent="true" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.player.musicoo.view.MarqueeTextView
|
||||||
|
android:id="@+id/name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:fontFamily="@font/medium_font"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
<com.player.musicoo.view.MarqueeTextView
|
||||||
|
android:id="@+id/desc"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textColor="@color/black_60"
|
||||||
|
android:textSize="12dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/alarm_clock_btn"
|
||||||
|
android:layout_width="42dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="34dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
android:src="@drawable/alarm_clock_icon" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/play_black_btn"
|
||||||
|
android:layout_width="42dp"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/play_status_img"
|
||||||
|
android:layout_width="34dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
android:src="@drawable/play_black_icon" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.lihang.ShadowLayout>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/tab_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="72dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/drw_main_bottom_bg"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/home_btn"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/home_img"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/home_select_icon" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/import_btn"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/import_img"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/import_unselect_icon" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
57
app/src/main/res/layout/fragment_mo_home.xml
Normal file
57
app/src/main/res/layout/fragment_mo_home.xml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/main_bg_color">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@mipmap/main_bg_img" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp" />
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_below="@+id/view"
|
||||||
|
android:layout_marginTop="-2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:fontFamily="@font/medium_font"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="20dp" />
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/content_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
</RelativeLayout>
|
||||||
23
app/src/main/res/layout/music_list_layout.xml
Normal file
23
app/src/main/res/layout/music_list_layout.xml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:ignore="MissingDefaultResource">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/medium_font"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="18dp" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rv"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
</LinearLayout>
|
||||||
63
app/src/main/res/layout/music_responsive_item.xml
Normal file
63
app/src/main/res/layout/music_responsive_item.xml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="320dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:ignore="MissingDefaultResource">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingTop="12dp">
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:elevation="0dp"
|
||||||
|
app:cardCornerRadius="4dp"
|
||||||
|
app:cardElevation="0dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@mipmap/musicoo_logo_img" />
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.player.musicoo.view.MarqueeTextView
|
||||||
|
android:id="@+id/name_tv"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:fontFamily="@font/regular_font"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="14dp" />
|
||||||
|
|
||||||
|
<com.player.musicoo.view.MarqueeTextView
|
||||||
|
android:id="@+id/desc_tv"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:fontFamily="@font/regular_font"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textColor="@color/white_60"
|
||||||
|
android:textSize="12dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
63
app/src/main/res/layout/music_tow_row_item.xml
Normal file
63
app/src/main/res/layout/music_tow_row_item.xml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:ignore="MissingDefaultResource">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingTop="12dp">
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:elevation="0dp"
|
||||||
|
app:cardCornerRadius="4dp"
|
||||||
|
app:cardElevation="0dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="148dp"
|
||||||
|
android:layout_height="148dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@mipmap/musicoo_logo_img" />
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.player.musicoo.view.MarqueeTextView
|
||||||
|
android:id="@+id/name_tv"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:fontFamily="@font/regular_font"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="14dp" />
|
||||||
|
|
||||||
|
<com.player.musicoo.view.MarqueeTextView
|
||||||
|
android:id="@+id/desc_tv"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/regular_font"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textColor="@color/white_60"
|
||||||
|
android:textSize="12dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
@ -2,4 +2,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.2.1" apply false
|
id("com.android.application") version "8.2.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
||||||
|
id("org.jetbrains.kotlin.plugin.serialization") version "1.7.20" apply false
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user