diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..c8f004d
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index de4cae1..822560e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d08809e..6e6f31c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -35,6 +35,9 @@
+
@@ -45,8 +48,6 @@
android:name=".activity.AboutActivity"
android:screenOrientation="portrait" />
-
-
(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()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/activity/PrimaryActivity.kt b/app/src/main/java/com/player/musicoo/activity/PrimaryActivity.kt
new file mode 100644
index 0000000..2cbd060
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/activity/PrimaryActivity.kt
@@ -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 = 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
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/adapter/ResponsiveListAdapter.kt b/app/src/main/java/com/player/musicoo/adapter/ResponsiveListAdapter.kt
new file mode 100644
index 0000000..201924e
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/adapter/ResponsiveListAdapter.kt
@@ -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,
+) :
+ RecyclerView.Adapter() {
+
+ 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/adapter/TowRowListAdapter.kt b/app/src/main/java/com/player/musicoo/adapter/TowRowListAdapter.kt
new file mode 100644
index 0000000..a7f9996
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/adapter/TowRowListAdapter.kt
@@ -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,
+) :
+ RecyclerView.Adapter() {
+
+ 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/fragment/MoBaseFragment.kt b/app/src/main/java/com/player/musicoo/fragment/MoBaseFragment.kt
new file mode 100644
index 0000000..916034c
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/fragment/MoBaseFragment.kt
@@ -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 : 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()
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/fragment/MoHomeFragment.kt b/app/src/main/java/com/player/musicoo/fragment/MoHomeFragment.kt
new file mode 100644
index 0000000..0e20327
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/fragment/MoHomeFragment.kt
@@ -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() {
+ 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()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/innertube/Innertube.kt b/app/src/main/java/com/player/musicoo/innertube/Innertube.kt
new file mode 100644
index 0000000..d960ff7
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/Innertube.kt
@@ -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(
+ 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?,
+ val authors: List>?,
+ val album: Info?,
+ val durationText: String?,
+ override val thumbnail: Thumbnail?
+ ) : Item() {
+ override val key get() = info!!.endpoint!!.videoId!!
+
+ companion object
+ }
+
+ data class VideoItem(
+ val info: Info?,
+ val authors: List>?,
+ 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?,
+ val authors: List>?,
+ val year: String?,
+ override val thumbnail: Thumbnail?
+ ) : Item() {
+ override val key get() = info!!.endpoint!!.browseId!!
+
+ companion object
+ }
+
+ data class ArtistItem(
+ val info: Info?,
+ val subscribersCountText: String?,
+ override val thumbnail: Thumbnail?
+ ) : Item() {
+ override val key get() = info!!.endpoint!!.browseId!!
+
+ companion object
+ }
+
+ data class PlaylistItem(
+ val info: Info?,
+ val channel: Info?,
+ 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
+ )
+
+ 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?,
+ val songsEndpoint: NavigationEndpoint.Endpoint.Browse?,
+ val albums: List?,
+ val albumsEndpoint: NavigationEndpoint.Endpoint.Browse?,
+ val singles: List?,
+ val singlesEndpoint: NavigationEndpoint.Endpoint.Browse?,
+ )
+
+ data class PlaylistOrAlbumPage(
+ val title: String?,
+ val authors: List>?,
+ val year: String?,
+ val thumbnail: Thumbnail?,
+ val url: String?,
+ val songsPage: ItemsPage?,
+ val otherVersions: List?
+ )
+
+ data class NextPage(
+ val itemsPage: ItemsPage?,
+ val playlistId: String?,
+ val params: String? = null,
+ val playlistSetVideoId: String? = null
+ )
+
+ data class RelatedPage(
+ val songs: List? = null,
+ val playlists: List? = null,
+ val albums: List? = null,
+ val artists: List? = null,
+ )
+
+ data class ItemsPage(
+ val items: List?,
+ val continuation: String?
+ )
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/BrowseResponse.kt b/app/src/main/java/com/player/musicoo/innertube/models/BrowseResponse.kt
new file mode 100644
index 0000000..43291dc
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/BrowseResponse.kt
@@ -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?
+ )
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/ButtonRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/models/ButtonRenderer.kt
new file mode 100644
index 0000000..6ae0f91
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/ButtonRenderer.kt
@@ -0,0 +1,8 @@
+package com.player.musicoo.innertube.models
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ButtonRenderer(
+ val navigationEndpoint: NavigationEndpoint?
+)
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/Context.kt b/app/src/main/java/com/player/musicoo/innertube/models/Context.kt
new file mode 100644
index 0000000..6e67c58
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/Context.kt
@@ -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"
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/Continuation.kt b/app/src/main/java/com/player/musicoo/innertube/models/Continuation.kt
new file mode 100644
index 0000000..63ef42c
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/Continuation.kt
@@ -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?
+ )
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/ContinuationResponse.kt b/app/src/main/java/com/player/musicoo/innertube/models/ContinuationResponse.kt
new file mode 100644
index 0000000..6fb2141
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/ContinuationResponse.kt
@@ -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?,
+ )
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/GetQueueResponse.kt b/app/src/main/java/com/player/musicoo/innertube/models/GetQueueResponse.kt
new file mode 100644
index 0000000..e84b3d3
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/GetQueueResponse.kt
@@ -0,0 +1,13 @@
+package com.player.musicoo.innertube.models
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class GetQueueResponse(
+ val queueDatas: List?,
+) {
+ @Serializable
+ data class QueueData(
+ val content: NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer.Content?
+ )
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/GridRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/models/GridRenderer.kt
new file mode 100644
index 0000000..ea769ce
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/GridRenderer.kt
@@ -0,0 +1,13 @@
+package com.player.musicoo.innertube.models
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class GridRenderer(
+ val items: List- ?,
+) {
+ @Serializable
+ data class Item(
+ val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?
+ )
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/MusicCarouselShelfRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/models/MusicCarouselShelfRenderer.kt
new file mode 100644
index 0000000..86ef678
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/MusicCarouselShelfRenderer.kt
@@ -0,0 +1,34 @@
+package com.player.musicoo.innertube.models
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MusicCarouselShelfRenderer(
+ val header: Header?,
+ val contents: List?,
+) {
+ @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?
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/MusicResponsiveListItemRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/models/MusicResponsiveListItemRenderer.kt
new file mode 100644
index 0000000..90b964d
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/MusicResponsiveListItemRenderer.kt
@@ -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?,
+ val flexColumns: List,
+ val thumbnail: ThumbnailRenderer?,
+ val navigationEndpoint: NavigationEndpoint?,
+) {
+ @Serializable
+ data class FlexColumn(
+ @JsonNames("musicResponsiveListItemFixedColumnRenderer")
+ val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer?
+ ) {
+ @Serializable
+ data class MusicResponsiveListItemFlexColumnRenderer(
+ val text: Runs?
+ )
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/MusicShelfRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/models/MusicShelfRenderer.kt
new file mode 100644
index 0000000..ac864ef
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/MusicShelfRenderer.kt
@@ -0,0 +1,41 @@
+package com.player.musicoo.innertube.models
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MusicShelfRenderer(
+ val bottomEndpoint: NavigationEndpoint?,
+ val contents: List?,
+ val continuations: List?,
+ val title: Runs?
+) {
+ @Serializable
+ data class Content(
+ val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
+ ) {
+ val runs: Pair
, List>>
+ 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()
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/MusicTwoRowItemRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/models/MusicTwoRowItemRenderer.kt
new file mode 100644
index 0000000..2d6ea15
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/MusicTwoRowItemRenderer.kt
@@ -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?,
+)
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/NavigationEndpoint.kt b/app/src/main/java/com/player/musicoo/innertube/models/NavigationEndpoint.kt
new file mode 100644
index 0000000..1ef7fbd
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/NavigationEndpoint.kt
@@ -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::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()
+// }
+// }
+//}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/NextResponse.kt b/app/src/main/java/com/player/musicoo/innertube/models/NextResponse.kt
new file mode 100644
index 0000000..70062b2
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/NextResponse.kt
@@ -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?,
+ val continuations: List?,
+ ) {
+ @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?
+ ) {
+ @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?
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/PlayerResponse.kt b/app/src/main/java/com/player/musicoo/innertube/models/PlayerResponse.kt
new file mode 100644
index 0000000..599937e
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/PlayerResponse.kt
@@ -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?
+ ) {
+ 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?
+ )
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/PlaylistPanelVideoRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/models/PlaylistPanelVideoRenderer.kt
new file mode 100644
index 0000000..b9dcaff
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/PlaylistPanelVideoRenderer.kt
@@ -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?,
+)
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/Runs.kt b/app/src/main/java/com/player/musicoo/innertube/models/Runs.kt
new file mode 100644
index 0000000..7996287
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/Runs.kt
@@ -0,0 +1,31 @@
+package com.player.musicoo.innertube.models
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Runs(
+ val runs: List = listOf()
+) {
+ val text: String
+ get() = runs.joinToString("") { it.text ?: "" }
+
+ fun splitBySeparator(): List> {
+ 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?,
+ )
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/SearchResponse.kt b/app/src/main/java/com/player/musicoo/innertube/models/SearchResponse.kt
new file mode 100644
index 0000000..e650209
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/SearchResponse.kt
@@ -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?
+ )
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/SearchSuggestionsResponse.kt b/app/src/main/java/com/player/musicoo/innertube/models/SearchSuggestionsResponse.kt
new file mode 100644
index 0000000..9ceff22
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/SearchSuggestionsResponse.kt
@@ -0,0 +1,28 @@
+package com.player.musicoo.innertube.models
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SearchSuggestionsResponse(
+ val contents: List?
+) {
+ @Serializable
+ data class Content(
+ val searchSuggestionsSectionRenderer: SearchSuggestionsSectionRenderer?
+ ) {
+ @Serializable
+ data class SearchSuggestionsSectionRenderer(
+ val contents: List?
+ ) {
+ @Serializable
+ data class Content(
+ val searchSuggestionRenderer: SearchSuggestionRenderer?
+ ) {
+ @Serializable
+ data class SearchSuggestionRenderer(
+ val navigationEndpoint: NavigationEndpoint?,
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/SectionListRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/models/SectionListRenderer.kt
new file mode 100644
index 0000000..9050c5e
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/SectionListRenderer.kt
@@ -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?,
+ val continuations: List?
+) {
+ @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?,
+ )
+ }
+
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/Tabs.kt b/app/src/main/java/com/player/musicoo/innertube/models/Tabs.kt
new file mode 100644
index 0000000..0d98941
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/Tabs.kt
@@ -0,0 +1,25 @@
+package com.player.musicoo.innertube.models
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Tabs(
+ val tabs: List?
+) {
+ @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?,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/Thumbnail.kt b/app/src/main/java/com/player/musicoo/innertube/models/Thumbnail.kt
new file mode 100644
index 0000000..e39b936
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/Thumbnail.kt
@@ -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
+ }
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/ThumbnailRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/models/ThumbnailRenderer.kt
new file mode 100644
index 0000000..73f2f1d
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/ThumbnailRenderer.kt
@@ -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?
+ )
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/bodies/BrowseBody.kt b/app/src/main/java/com/player/musicoo/innertube/models/bodies/BrowseBody.kt
new file mode 100644
index 0000000..09e5760
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/bodies/BrowseBody.kt
@@ -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
+)
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/bodies/ContinuationBody.kt b/app/src/main/java/com/player/musicoo/innertube/models/bodies/ContinuationBody.kt
new file mode 100644
index 0000000..8350ccd
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/bodies/ContinuationBody.kt
@@ -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,
+)
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/bodies/NextBody.kt b/app/src/main/java/com/player/musicoo/innertube/models/bodies/NextBody.kt
new file mode 100644
index 0000000..14d490a
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/bodies/NextBody.kt
@@ -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
+ )
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/bodies/PlayerBody.kt b/app/src/main/java/com/player/musicoo/innertube/models/bodies/PlayerBody.kt
new file mode 100644
index 0000000..4b1ab33
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/bodies/PlayerBody.kt
@@ -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
+)
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/bodies/QueueBody.kt b/app/src/main/java/com/player/musicoo/innertube/models/bodies/QueueBody.kt
new file mode 100644
index 0000000..a04b24a
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/bodies/QueueBody.kt
@@ -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? = null,
+ val playlistId: String? = null,
+)
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/bodies/SearchBody.kt b/app/src/main/java/com/player/musicoo/innertube/models/bodies/SearchBody.kt
new file mode 100644
index 0000000..3d68dff
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/bodies/SearchBody.kt
@@ -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
+)
diff --git a/app/src/main/java/com/player/musicoo/innertube/models/bodies/SearchSuggestionsBody.kt b/app/src/main/java/com/player/musicoo/innertube/models/bodies/SearchSuggestionsBody.kt
new file mode 100644
index 0000000..9868ca5
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/models/bodies/SearchSuggestionsBody.kt
@@ -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
+)
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/AlbumPage.kt b/app/src/main/java/com/player/musicoo/innertube/requests/AlbumPage.kt
new file mode 100644
index 0000000..0b72749
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/AlbumPage.kt
@@ -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? {
+ 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
+ )
+ }
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/ArtistPage.kt b/app/src/main/java/com/player/musicoo/innertube/requests/ArtistPage.kt
new file mode 100644
index 0000000..5e678a0
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/ArtistPage.kt
@@ -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? =
+ runCatchingNonCancellable {
+ val response = client.post(browse) {
+ setBody(body)
+ mask("contents,header")
+ }.body()
+
+ 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,
+ )
+ }
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/HomePage.kt b/app/src/main/java/com/player/musicoo/innertube/requests/HomePage.kt
new file mode 100644
index 0000000..898d48e
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/HomePage.kt
@@ -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>? =
+ runCatchingNonCancellable {
+
+ val response = client.post(browse) {
+ setBody(BrowseBody(Context.DefaultWeb, "FEmusic_home"))
+ }.body()
+
+ val sectionListRenderer = response
+ .contents
+ ?.singleColumnBrowseResultsRenderer
+ ?.tabs
+ ?.firstOrNull()
+ ?.tabRenderer
+ ?.content
+ ?.sectionListRenderer
+
+ val contents = sectionListRenderer?.contents
+ Log.d("ocean","contents->$contents")
+
+ val searchList: MutableList = 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
+ }
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/ItemsPage.kt b/app/src/main/java/com/player/musicoo/innertube/requests/ItemsPage.kt
new file mode 100644
index 0000000..6a0973c
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/ItemsPage.kt
@@ -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 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()
+
+ 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 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()
+
+ itemsPageFromMusicShelRendererOrGridRenderer(
+ musicShelfRenderer = response
+ .continuationContents
+ ?.musicShelfContinuation,
+ gridRenderer = null,
+ fromMusicResponsiveListItemRenderer = fromMusicResponsiveListItemRenderer,
+ fromMusicTwoRowItemRenderer = fromMusicTwoRowItemRenderer,
+ )
+}
+
+private fun itemsPageFromMusicShelRendererOrGridRenderer(
+ musicShelfRenderer: MusicShelfRenderer?,
+ gridRenderer: GridRenderer?,
+ fromMusicResponsiveListItemRenderer: (MusicResponsiveListItemRenderer) -> T?,
+ fromMusicTwoRowItemRenderer: (MusicTwoRowItemRenderer) -> T?,
+): Innertube.ItemsPage? {
+ 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
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/Lyrics.kt b/app/src/main/java/com/player/musicoo/innertube/requests/Lyrics.kt
new file mode 100644
index 0000000..8c7f036
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/Lyrics.kt
@@ -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? = runCatchingNonCancellable {
+ val nextResponse = client.post(next) {
+ setBody(body)
+ mask("contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer(endpoint,title)")
+ }.body()
+
+ 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()
+
+ response.contents
+ ?.sectionListRenderer
+ ?.contents
+ ?.firstOrNull()
+ ?.musicDescriptionShelfRenderer
+ ?.description
+ ?.text
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/NextPage.kt b/app/src/main/java/com/player/musicoo/innertube/requests/NextPage.kt
new file mode 100644
index 0000000..0ee99ba
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/NextPage.kt
@@ -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? =
+ runCatchingNonCancellable {
+ val response = client.post(next) {
+ setBody(body)
+ mask("contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer.content.musicQueueRenderer.content.playlistPanelRenderer(continuations,contents(automixPreviewVideoRenderer,$playlistPanelVideoRendererMask))")
+ }.body()
+
+ 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()
+
+ 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
+ )
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/Player.kt b/app/src/main/java/com/player/musicoo/innertube/requests/Player.kt
new file mode 100644
index 0000000..5d7f153
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/Player.kt
@@ -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()
+
+ if (response.playabilityStatus?.status == "OK") {
+ response
+ } else {
+ @Serializable
+ data class AudioStream(
+ val url: String,
+ val bitrate: Long
+ )
+
+ @Serializable
+ data class PipedResponse(
+ val audioStreams: List
+ )
+
+ 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()
+
+ if (safePlayerResponse.playabilityStatus?.status != "OK") {
+ return@runCatchingNonCancellable response
+ }
+
+ val audioStreams = client.get("https://watchapi.whatever.social/streams/${body.videoId}") {
+ contentType(ContentType.Application.Json)
+ }.body().audioStreams
+
+ safePlayerResponse.copy(
+ streamingData = safePlayerResponse.streamingData?.copy(
+ adaptiveFormats = safePlayerResponse.streamingData.adaptiveFormats?.map { adaptiveFormat ->
+ adaptiveFormat.copy(
+ url = audioStreams.find { it.bitrate == adaptiveFormat.bitrate }?.url
+ )
+ }
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/PlaylistPage.kt b/app/src/main/java/com/player/musicoo/innertube/requests/PlaylistPage.kt
new file mode 100644
index 0000000..bd8c699
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/PlaylistPage.kt
@@ -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()
+
+ 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()
+
+ 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
+ )
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/Queue.kt b/app/src/main/java/com/player/musicoo/innertube/requests/Queue.kt
new file mode 100644
index 0000000..470cb28
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/Queue.kt
@@ -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()
+
+ response
+ .queueDatas
+ ?.mapNotNull { queueData ->
+ queueData
+ .content
+ ?.playlistPanelVideoRenderer
+ ?.let(Innertube.SongItem::from)
+ }
+}
+
+suspend fun Innertube.song(videoId: String): Result? =
+ queue(QueueBody(videoIds = listOf(videoId)))?.map { it?.firstOrNull() }
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/RelatedPage.kt b/app/src/main/java/com/player/musicoo/innertube/requests/RelatedPage.kt
new file mode 100644
index 0000000..6273a38
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/RelatedPage.kt
@@ -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()
+
+ 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()
+
+ 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),
+ )
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/SearchPage.kt b/app/src/main/java/com/player/musicoo/innertube/requests/SearchPage.kt
new file mode 100644
index 0000000..3498983
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/SearchPage.kt
@@ -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 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()
+
+ response
+ .contents
+ ?.tabbedSearchResultsRenderer
+ ?.tabs
+ ?.firstOrNull()
+ ?.tabRenderer
+ ?.content
+ ?.sectionListRenderer
+ ?.contents
+ ?.lastOrNull()
+ ?.musicShelfRenderer
+ ?.toItemsPage(fromMusicShelfRendererContent)
+}
+
+suspend fun Innertube.searchPage(
+ body: ContinuationBody,
+ fromMusicShelfRendererContent: (MusicShelfRenderer.Content) -> T?
+) = runCatchingNonCancellable {
+ val response = client.post(search) {
+ setBody(body)
+ mask("continuationContents.musicShelfContinuation(continuations,contents.$musicResponsiveListItemRendererMask)")
+ }.body()
+
+ response
+ .continuationContents
+ ?.musicShelfContinuation
+ ?.toItemsPage(fromMusicShelfRendererContent)
+}
+
+private fun MusicShelfRenderer?.toItemsPage(mapper: (MusicShelfRenderer.Content) -> T?) =
+ Innertube.ItemsPage(
+ items = this
+ ?.contents
+ ?.mapNotNull(mapper),
+ continuation = this
+ ?.continuations
+ ?.firstOrNull()
+ ?.nextContinuationData
+ ?.continuation
+ )
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/SearchSuggestions.kt b/app/src/main/java/com/player/musicoo/innertube/requests/SearchSuggestions.kt
new file mode 100644
index 0000000..79ce129
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/SearchSuggestions.kt
@@ -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()
+
+ response
+ .contents
+ ?.firstOrNull()
+ ?.searchSuggestionsSectionRenderer
+ ?.contents
+ ?.mapNotNull { content ->
+ content
+ .searchSuggestionRenderer
+ ?.navigationEndpoint
+ ?.searchEndpoint
+ ?.query
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/utils/BrotliEncoder.kt b/app/src/main/java/com/player/musicoo/innertube/utils/BrotliEncoder.kt
new file mode 100644
index 0000000..ce217a7
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/utils/BrotliEncoder.kt
@@ -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()
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicResponsiveListItemRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicResponsiveListItemRenderer.kt
new file mode 100644
index 0000000..e6c260b
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicResponsiveListItemRenderer.kt
@@ -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>(Innertube::Info)
+ ?.takeIf(List::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 }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicShelfRendererContent.kt b/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicShelfRendererContent.kt
new file mode 100644
index 0000000..1396542
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicShelfRendererContent.kt
@@ -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? = 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 }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicTwoRowItemRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicTwoRowItemRenderer.kt
new file mode 100644
index 0000000..0e1cf77
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicTwoRowItemRenderer.kt
@@ -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 }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/utils/FromPlaylistPanelVideoRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/utils/FromPlaylistPanelVideoRenderer.kt
new file mode 100644
index 0000000..6a9fbd5
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/utils/FromPlaylistPanelVideoRenderer.kt
@@ -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 }
+}
diff --git a/app/src/main/java/com/player/musicoo/innertube/utils/Utils.kt b/app/src/main/java/com/player/musicoo/innertube/utils/Utils.kt
new file mode 100644
index 0000000..7ce5fc7
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/utils/Utils.kt
@@ -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 runCatchingNonCancellable(block: () -> R): Result? {
+ val result = runCatching(block)
+ return when (result.exceptionOrNull()) {
+ is CancellationException -> null
+ else -> result
+ }
+}
+
+infix operator fun Innertube.ItemsPage?.plus(other: Innertube.ItemsPage) =
+ other.copy(
+ items = (this?.items?.plus(other.items ?: emptyList())
+ ?: other.items)?.distinctBy(Innertube.Item::key)
+ )
diff --git a/app/src/main/java/com/player/musicoo/innertube/utils/brotli.kt b/app/src/main/java/com/player/musicoo/innertube/utils/brotli.kt
new file mode 100644
index 0000000..14d8212
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/utils/brotli.kt
@@ -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)
+}
diff --git a/app/src/main/java/com/player/musicoo/service/AudioPlayerService.kt b/app/src/main/java/com/player/musicoo/service/AudioPlayerService.kt
deleted file mode 100644
index c566263..0000000
--- a/app/src/main/java/com/player/musicoo/service/AudioPlayerService.kt
+++ /dev/null
@@ -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)
- }
- }
-}
diff --git a/app/src/main/java/com/player/musicoo/sp/AppStore.kt b/app/src/main/java/com/player/musicoo/sp/AppStore.kt
new file mode 100644
index 0000000..8ce1962
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/sp/AppStore.kt
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/sp/store/Providers.kt b/app/src/main/java/com/player/musicoo/sp/store/Providers.kt
new file mode 100644
index 0000000..9749ecc
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/sp/store/Providers.kt
@@ -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): Set {
+ return preferences.getStringSet(key, defaultValue)!!
+ }
+
+ override fun setStringSet(key: String, value: Set) {
+ 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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/sp/store/Store.kt b/app/src/main/java/com/player/musicoo/sp/store/Store.kt
new file mode 100644
index 0000000..6fe63a1
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/sp/store/Store.kt
@@ -0,0 +1,98 @@
+package com.player.musicoo.sp.store
+
+import kotlin.reflect.KProperty
+
+class Store(val provider: StoreProvider) {
+ interface Delegate {
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): T
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
+ }
+
+ fun int(key: String, defaultValue: Int): Delegate {
+ return object : Delegate {
+ 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 {
+ return object : Delegate {
+ 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 {
+ return object : Delegate {
+ 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): Delegate> {
+ return object : Delegate> {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Set {
+ return provider.getStringSet(key, defaultValue)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set) {
+ provider.setStringSet(key, value)
+ }
+ }
+ }
+
+ fun boolean(key: String, defaultValue: Boolean): Delegate {
+ return object : Delegate {
+ 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 > enum(key: String, defaultValue: T, values: Array): Delegate {
+ return object : Delegate {
+ 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 typedString(key: String, from: (String) -> T?, to: (T?) -> String): Delegate {
+ return object : Delegate {
+ 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))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/sp/store/StoreProvider.kt b/app/src/main/java/com/player/musicoo/sp/store/StoreProvider.kt
new file mode 100644
index 0000000..d33c2b4
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/sp/store/StoreProvider.kt
@@ -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): Set
+ fun setStringSet(key: String, value: Set)
+
+ fun getBoolean(key: String, defaultValue: Boolean): Boolean
+ fun setBoolean(key: String, value: Boolean)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/util/LogTag.kt b/app/src/main/java/com/player/musicoo/util/LogTag.kt
new file mode 100644
index 0000000..be1e689
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/util/LogTag.kt
@@ -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"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/view/ModuleView.kt b/app/src/main/java/com/player/musicoo/view/ModuleView.kt
new file mode 100644
index 0000000..0de72b7
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/view/ModuleView.kt
@@ -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
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/view/MusicResponsiveListView.kt b/app/src/main/java/com/player/musicoo/view/MusicResponsiveListView.kt
new file mode 100644
index 0000000..5689bc1
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/view/MusicResponsiveListView.kt
@@ -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(R.id.title)
+ title?.text = homePage.title
+
+ val rv = contentView?.findViewById(R.id.rv)
+
+ val adapter = ResponsiveListAdapter(context, homePage.contents)
+ rv?.layoutManager = GridLayoutManager(context, 3, GridLayoutManager.HORIZONTAL, false)
+ rv?.adapter = adapter
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/view/MusicTowRowListView.kt b/app/src/main/java/com/player/musicoo/view/MusicTowRowListView.kt
new file mode 100644
index 0000000..8c1d6d7
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/view/MusicTowRowListView.kt
@@ -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(R.id.title)
+ title?.text = homePage.title
+
+ val rv = contentView?.findViewById(R.id.rv)
+
+ val adapter = TowRowListAdapter(context, homePage.contents)
+ rv?.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ rv?.adapter = adapter
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_primary.xml b/app/src/main/res/layout/activity_primary.xml
new file mode 100644
index 0000000..53e863a
--- /dev/null
+++ b/app/src/main/res/layout/activity_primary.xml
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_mo_home.xml b/app/src/main/res/layout/fragment_mo_home.xml
new file mode 100644
index 0000000..881a002
--- /dev/null
+++ b/app/src/main/res/layout/fragment_mo_home.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/music_list_layout.xml b/app/src/main/res/layout/music_list_layout.xml
new file mode 100644
index 0000000..d10459e
--- /dev/null
+++ b/app/src/main/res/layout/music_list_layout.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/music_responsive_item.xml b/app/src/main/res/layout/music_responsive_item.xml
new file mode 100644
index 0000000..e62d9ac
--- /dev/null
+++ b/app/src/main/res/layout/music_responsive_item.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/music_tow_row_item.xml b/app/src/main/res/layout/music_tow_row_item.xml
new file mode 100644
index 0000000..e548b08
--- /dev/null
+++ b/app/src/main/res/layout/music_tow_row_item.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index a6711ae..3b28948 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -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
}
\ No newline at end of file