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