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("org.jetbrains.kotlin.android")
|
||||
id("kotlin-kapt")
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
}
|
||||
|
||||
android {
|
||||
@ -13,7 +14,7 @@ android {
|
||||
minSdk = 24
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0.1"
|
||||
versionName = "1.0.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@ -65,4 +66,13 @@ dependencies {
|
||||
implementation("androidx.media3:media3-exoplayer:1.3.1")
|
||||
implementation("androidx.media3:media3-ui: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
|
||||
android:name=".activity.MainActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".activity.PrimaryActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".activity.PlayDetailsActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
@ -45,8 +48,6 @@
|
||||
android:name=".activity.AboutActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<service android:name=".service.AudioPlayerService" />
|
||||
|
||||
<service
|
||||
android:name=".service.PlaybackService"
|
||||
android:exported="true"
|
||||
|
||||
@ -41,7 +41,7 @@ class LaunchActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun toMainActivity() {
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
startActivity(Intent(this, PrimaryActivity::class.java))
|
||||
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 {
|
||||
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.plugin.serialization") version "1.7.20" apply false
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user