This commit is contained in:
ocean 2024-04-22 18:26:48 +08:00
parent c0b1731e01
commit 70e1ef66c8
74 changed files with 3422 additions and 98 deletions

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AndroidLintUnsafeImplicitIntentLaunch" enabled="false" level="ERROR" enabled_by_default="false" />
</profile>
</component>

View File

@ -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")
}

View File

@ -35,6 +35,9 @@
<activity
android:name=".activity.MainActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activity.PrimaryActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activity.PlayDetailsActivity"
android:screenOrientation="portrait" />
@ -45,8 +48,6 @@
android:name=".activity.AboutActivity"
android:screenOrientation="portrait" />
<service android:name=".service.AudioPlayerService" />
<service
android:name=".service.PlaybackService"
android:exported="true"

View File

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

View File

@ -0,0 +1,73 @@
package com.player.musicoo.activity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.player.musicoo.sp.AppStore
import com.player.musicoo.util.LogTag
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope() {
enum class Event {
ActivityStart,
ActivityStop,
ActivityOnResume
}
protected val TAG = LogTag.VO_ACT_LOG
protected val appStore by lazy { AppStore(this) }
protected val events = Channel<Event>(Channel.UNLIMITED)
protected abstract suspend fun main()
private var defer: suspend () -> Unit = {}
private var deferRunning = false
fun defer(operation: suspend () -> Unit) {
this.defer = operation
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launch {
main()
}
}
override fun onResume() {
super.onResume()
events.trySend(Event.ActivityOnResume)
}
override fun onStart() {
super.onStart()
events.trySend(Event.ActivityStart)
}
override fun onStop() {
super.onStop()
events.trySend(Event.ActivityStop)
}
override fun finish() {
if (deferRunning) {
return
}
deferRunning = true
launch {
try {
defer()
} finally {
withContext(NonCancellable) {
super.finish()
}
}
}
}
}

View File

@ -0,0 +1,89 @@
package com.player.musicoo.activity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import com.player.musicoo.R
import com.player.musicoo.databinding.ActivityPrimaryBinding
import com.player.musicoo.fragment.ImportFragment
import com.player.musicoo.fragment.MoHomeFragment
class PrimaryActivity : MoBaseActivity() {
/**
* musicResponsiveListItemRenderer
* musicTwoRowItemRenderer
*/
private lateinit var binding: ActivityPrimaryBinding
private val mFragments: MutableList<Fragment> = ArrayList()
private var currentIndex: Int = 0
private var mCurrentFragment: Fragment? = null
override suspend fun main() {
binding = ActivityPrimaryBinding.inflate(layoutInflater)
setContentView(binding.root)
initView()
}
private fun initView() {
initClick()
initFragment()
}
private fun initClick() {
binding.homeBtn.setOnClickListener {
changeFragment(0)
updateBtnState(0)
}
binding.importBtn.setOnClickListener {
changeFragment(1)
updateBtnState(1)
}
}
private fun initFragment() {
mFragments.clear()
mFragments.add(MoHomeFragment())
mFragments.add(ImportFragment())
changeFragment(0)
updateBtnState(0)
}
private fun changeFragment(index: Int) {
currentIndex = index
val ft: FragmentTransaction = supportFragmentManager.beginTransaction()
if (null != mCurrentFragment) {
ft.hide(mCurrentFragment!!)
}
var fragment = supportFragmentManager.findFragmentByTag(
mFragments[currentIndex].javaClass.name
)
if (null == fragment) {
fragment = mFragments[index]
}
mCurrentFragment = fragment
if (!fragment.isAdded) {
ft.add(R.id.frame_layout, fragment, fragment.javaClass.name)
} else {
ft.show(fragment)
}
ft.commit()
}
private fun updateBtnState(index: Int) {
binding.apply {
homeImg.setImageResource(
when (index) {
0 -> R.drawable.home_select_icon
else -> R.drawable.home_unselect_icon
}
)
importImg.setImageResource(
when (index) {
1 -> R.drawable.import_select_icon
else -> R.drawable.import_unselect_icon
}
)
}
}
}

View File

@ -0,0 +1,84 @@
package com.player.musicoo.adapter
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.activity.PlayDetailsActivity
import com.player.musicoo.bean.Audio
import com.player.musicoo.databinding.MusicResponsiveItemBinding
import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
import com.player.musicoo.databinding.SoundsOfNatureLayoutBinding
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import com.player.musicoo.util.getAudioDurationFromAssets
class ResponsiveListAdapter(
private val context: Context,
private val list: List<MusicCarouselShelfRenderer.Content>,
) :
RecyclerView.Adapter<ResponsiveListAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding =
MusicResponsiveItemBinding.inflate(LayoutInflater.from(context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val bean = list[position]
holder.bind(bean)
}
override fun getItemCount(): Int = list.size
inner class ViewHolder(private val binding: MusicResponsiveItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(content: MusicCarouselShelfRenderer.Content) {
val url = content.musicResponsiveListItemRenderer
?.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.let { it.getOrNull(1) ?: it.getOrNull(0) }
?.url
val name = content.musicResponsiveListItemRenderer
?.flexColumns?.get(0)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.firstOrNull()
?.text
val desc = content.musicResponsiveListItemRenderer
?.flexColumns?.get(1)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.firstOrNull()
?.text
binding.apply {
Glide.with(context)
.load(url)
.into(image)
nameTv.text = name
descTv.text = desc
}
}
}
private var itemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) {
itemClickListener = listener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
}

View File

@ -0,0 +1,81 @@
package com.player.musicoo.adapter
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.activity.PlayDetailsActivity
import com.player.musicoo.bean.Audio
import com.player.musicoo.databinding.MusicResponsiveItemBinding
import com.player.musicoo.databinding.MusicTowRowItemBinding
import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
import com.player.musicoo.databinding.SoundsOfNatureLayoutBinding
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import com.player.musicoo.util.getAudioDurationFromAssets
class TowRowListAdapter(
private val context: Context,
private val list: List<MusicCarouselShelfRenderer.Content>,
) :
RecyclerView.Adapter<TowRowListAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding =
MusicTowRowItemBinding.inflate(LayoutInflater.from(context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val bean = list[position]
holder.bind(bean)
}
override fun getItemCount(): Int = list.size
inner class ViewHolder(private val binding: MusicTowRowItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(content: MusicCarouselShelfRenderer.Content) {
val url = content.musicTwoRowItemRenderer
?.thumbnailRenderer
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.let { it.getOrNull(1) ?: it.getOrNull(0) }
?.url
val name = content.musicTwoRowItemRenderer
?.title
?.runs
?.firstOrNull()
?.text
val desc = content.musicTwoRowItemRenderer
?.subtitle
?.runs
?.map { it.text }
?.joinToString("")
binding.apply {
Glide.with(context)
.load(url)
.into(image)
nameTv.text = name
descTv.text = desc
}
}
}
private var itemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) {
itemClickListener = listener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
}

View File

@ -0,0 +1,47 @@
package com.player.musicoo.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import com.player.musicoo.databinding.FragmentMoHomeBinding
import com.player.musicoo.sp.AppStore
import com.player.musicoo.util.LogTag
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
abstract class MoBaseFragment<T : ViewBinding> : Fragment(), CoroutineScope by MainScope() {
protected abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> T
protected lateinit var binding: T
protected val TAG = LogTag.VO_FRAGMENT_LOG
protected val appStore by lazy { AppStore(requireContext()) }
protected abstract suspend fun onViewCreated()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = bindingInflater(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
launch {
onViewCreated()
}
}
override fun onDestroyView() {
super.onDestroyView()
cancel()
}
override fun onDestroy() {
super.onDestroy()
cancel()
}
}

View File

@ -0,0 +1,72 @@
package com.player.musicoo.fragment
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.databinding.FragmentMoHomeBinding
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
import com.player.musicoo.innertube.requests.homePage
import com.player.musicoo.view.MusicResponsiveListView
import com.player.musicoo.view.MusicTowRowListView
class MoHomeFragment : MoBaseFragment<FragmentMoHomeBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentMoHomeBinding
get() = FragmentMoHomeBinding::inflate
override suspend fun onViewCreated() {
initView()
}
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
private suspend fun initView() {
Innertube.homePage()?.onFailure {
Log.d("ocean", "onFailure->$it")
}
?.onSuccess {
for (home: Innertube.HomePage in it) {
for (content: MusicCarouselShelfRenderer.Content in home.contents) {
if (content.musicResponsiveListItemRenderer != null) {
binding.contentLayout.addView(
MusicResponsiveListView(
requireActivity(),
home
)
)
break
}
if (content.musicTwoRowItemRenderer != null) {
binding.contentLayout.addView(
MusicTowRowListView(
requireActivity(),
home
)
)
break
}
}
}
}
}
override fun onResume() {
super.onResume()
initImmersionBar()
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (!hidden) {
initImmersionBar()
}
}
}

View File

@ -0,0 +1,209 @@
package com.player.musicoo.innertube
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.BrowserUserAgent
import io.ktor.client.plugins.compression.ContentEncoding
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.header
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.serialization.kotlinx.json.json
import com.player.musicoo.innertube.models.NavigationEndpoint
import com.player.musicoo.innertube.models.Runs
import com.player.musicoo.innertube.models.Thumbnail
import com.player.musicoo.innertube.utils.brotli
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
object Innertube {
val client = HttpClient(OkHttp) {
BrowserUserAgent()
expectSuccess = true
install(ContentNegotiation) {
@OptIn(ExperimentalSerializationApi::class)
json(Json {
ignoreUnknownKeys = true
explicitNulls = false
encodeDefaults = true
})
}
install(ContentEncoding) {
brotli()
}
defaultRequest {
url(scheme = "https", host ="music.youtube.com") {
headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
headers.append("X-Goog-Api-Key", "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8")
parameters.append("prettyPrint", "false")
}
}
}
internal const val browse = "/youtubei/v1/browse"
internal const val next = "/youtubei/v1/next"
internal const val player = "/youtubei/v1/player"
internal const val queue = "/youtubei/v1/music/get_queue"
internal const val search = "/youtubei/v1/search"
internal const val searchSuggestions = "/youtubei/v1/music/get_search_suggestions"
internal const val musicResponsiveListItemRendererMask = "musicResponsiveListItemRenderer(flexColumns,fixedColumns,thumbnail,navigationEndpoint)"
internal const val musicTwoRowItemRendererMask = "musicTwoRowItemRenderer(thumbnailRenderer,title,subtitle,navigationEndpoint)"
const val playlistPanelVideoRendererMask = "playlistPanelVideoRenderer(title,navigationEndpoint,longBylineText,shortBylineText,thumbnail,lengthText)"
internal fun HttpRequestBuilder.mask(value: String = "*") =
header("X-Goog-FieldMask", value)
data class Info<T : NavigationEndpoint.Endpoint>(
val name: String?,
val endpoint: T?
) {
@Suppress("UNCHECKED_CAST")
constructor(run: Runs.Run) : this(
name = run.text,
endpoint = run.navigationEndpoint?.endpoint as T?
)
}
@JvmInline
value class SearchFilter(val value: String) {
companion object {
val Song = SearchFilter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D")
val Video = SearchFilter("EgWKAQIQAWoKEAkQChAFEAMQBA%3D%3D")
val Album = SearchFilter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D")
val Artist = SearchFilter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D")
val CommunityPlaylist = SearchFilter("EgeKAQQoAEABagoQAxAEEAoQCRAF")
val FeaturedPlaylist = SearchFilter("EgeKAQQoADgBagwQDhAKEAMQBRAJEAQ%3D")
}
}
sealed class Item {
abstract val thumbnail: Thumbnail?
abstract val key: String
}
data class SongItem(
val info: Info<NavigationEndpoint.Endpoint.Watch>?,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
val album: Info<NavigationEndpoint.Endpoint.Browse>?,
val durationText: String?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.videoId!!
companion object
}
data class VideoItem(
val info: Info<NavigationEndpoint.Endpoint.Watch>?,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
val viewsText: String?,
val durationText: String?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.videoId!!
val isOfficialMusicVideo: Boolean
get() = info
?.endpoint
?.watchEndpointMusicSupportedConfigs
?.watchEndpointMusicConfig
?.musicVideoType == "MUSIC_VIDEO_TYPE_OMV"
val isUserGeneratedContent: Boolean
get() = info
?.endpoint
?.watchEndpointMusicSupportedConfigs
?.watchEndpointMusicConfig
?.musicVideoType == "MUSIC_VIDEO_TYPE_UGC"
companion object
}
data class AlbumItem(
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
val year: String?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.browseId!!
companion object
}
data class ArtistItem(
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
val subscribersCountText: String?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.browseId!!
companion object
}
data class PlaylistItem(
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
val channel: Info<NavigationEndpoint.Endpoint.Browse>?,
val songCount: Int?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.browseId!!
companion object
}
data class HomePage(
val title: String?,
val contents: List<MusicCarouselShelfRenderer.Content>
)
data class ArtistPage(
val name: String?,
val description: String?,
val thumbnail: Thumbnail?,
val shuffleEndpoint: NavigationEndpoint.Endpoint.Watch?,
val radioEndpoint: NavigationEndpoint.Endpoint.Watch?,
val songs: List<SongItem>?,
val songsEndpoint: NavigationEndpoint.Endpoint.Browse?,
val albums: List<AlbumItem>?,
val albumsEndpoint: NavigationEndpoint.Endpoint.Browse?,
val singles: List<AlbumItem>?,
val singlesEndpoint: NavigationEndpoint.Endpoint.Browse?,
)
data class PlaylistOrAlbumPage(
val title: String?,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
val year: String?,
val thumbnail: Thumbnail?,
val url: String?,
val songsPage: ItemsPage<SongItem>?,
val otherVersions: List<AlbumItem>?
)
data class NextPage(
val itemsPage: ItemsPage<SongItem>?,
val playlistId: String?,
val params: String? = null,
val playlistSetVideoId: String? = null
)
data class RelatedPage(
val songs: List<SongItem>? = null,
val playlists: List<PlaylistItem>? = null,
val albums: List<AlbumItem>? = null,
val artists: List<ArtistItem>? = null,
)
data class ItemsPage<T : Item>(
val items: List<T>?,
val continuation: String?
)
}

View File

@ -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?
)
}
}

View File

@ -0,0 +1,8 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.Serializable
@Serializable
data class ButtonRenderer(
val navigationEndpoint: NavigationEndpoint?
)

View File

@ -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"
)
)
}
}

View File

@ -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?
)
}

View File

@ -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?,
)
}

View File

@ -0,0 +1,13 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.Serializable
@Serializable
data class GetQueueResponse(
val queueDatas: List<QueueData>?,
) {
@Serializable
data class QueueData(
val content: NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer.Content?
)
}

View File

@ -0,0 +1,13 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.Serializable
@Serializable
data class GridRenderer(
val items: List<Item>?,
) {
@Serializable
data class Item(
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?
)
}

View File

@ -0,0 +1,34 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.Serializable
@Serializable
data class MusicCarouselShelfRenderer(
val header: Header?,
val contents: List<Content>?,
) {
@Serializable
data class Content(
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
)
@Serializable
data class Header(
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
val musicCarouselShelfBasicHeaderRenderer: MusicCarouselShelfBasicHeaderRenderer?
) {
@Serializable
data class MusicCarouselShelfBasicHeaderRenderer(
val moreContentButton: MoreContentButton?,
val title: Runs?,
val strapline: Runs?,
) {
@Serializable
data class MoreContentButton(
val buttonRenderer: ButtonRenderer?
)
}
}
}

View File

@ -0,0 +1,25 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class MusicResponsiveListItemRenderer(
val fixedColumns: List<FlexColumn>?,
val flexColumns: List<FlexColumn>,
val thumbnail: ThumbnailRenderer?,
val navigationEndpoint: NavigationEndpoint?,
) {
@Serializable
data class FlexColumn(
@JsonNames("musicResponsiveListItemFixedColumnRenderer")
val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer?
) {
@Serializable
data class MusicResponsiveListItemFlexColumnRenderer(
val text: Runs?
)
}
}

View File

@ -0,0 +1,41 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.Serializable
@Serializable
data class MusicShelfRenderer(
val bottomEndpoint: NavigationEndpoint?,
val contents: List<Content>?,
val continuations: List<Continuation>?,
val title: Runs?
) {
@Serializable
data class Content(
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
) {
val runs: Pair<List<Runs.Run>, List<List<Runs.Run>>>
get() = (musicResponsiveListItemRenderer
?.flexColumns
?.firstOrNull()
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?: emptyList()) to
(musicResponsiveListItemRenderer
?.flexColumns
?.lastOrNull()
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.splitBySeparator()
?: emptyList()
)
val thumbnail: Thumbnail?
get() = musicResponsiveListItemRenderer
?.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.firstOrNull()
}
}

View File

@ -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?,
)

View File

@ -0,0 +1,203 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.Serializable
/**
* watchPlaylistEndpoint: params, playlistId
* watchEndpoint: params, playlistId, videoId, index
* browseEndpoint: params, browseId
* searchEndpoint: params, query
*/
//@Serializable
//data class NavigationEndpoint(
// @JsonNames("watchEndpoint", "watchPlaylistEndpoint", "navigationEndpoint", "browseEndpoint", "searchEndpoint")
// val endpoint: Endpoint
//) {
// @Serializable
// data class Endpoint(
// val params: String?,
// val playlistId: String?,
// val videoId: String?,
// val index: Int?,
// val browseId: String?,
// val query: String?,
// val watchEndpointMusicSupportedConfigs: WatchEndpointMusicSupportedConfigs?,
// val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs?,
// ) {
// @Serializable
// data class WatchEndpointMusicSupportedConfigs(
// val watchEndpointMusicConfig: WatchEndpointMusicConfig
// ) {
// @Serializable
// data class WatchEndpointMusicConfig(
// val musicVideoType: String
// )
// }
//
// @Serializable
// data class BrowseEndpointContextSupportedConfigs(
// val browseEndpointContextMusicConfig: BrowseEndpointContextMusicConfig
// ) {
// @Serializable
// data class BrowseEndpointContextMusicConfig(
// val pageType: String
// )
// }
// }
//}
@Serializable
data class NavigationEndpoint(
val watchEndpoint: Endpoint.Watch?,
val watchPlaylistEndpoint: Endpoint.WatchPlaylist?,
val browseEndpoint: Endpoint.Browse?,
val searchEndpoint: Endpoint.Search?,
) {
val endpoint: Endpoint?
get() = watchEndpoint ?: browseEndpoint ?: watchPlaylistEndpoint ?: searchEndpoint
@Serializable
sealed class Endpoint {
@Serializable
data class Watch(
val params: String? = null,
val playlistId: String? = null,
val videoId: String? = null,
val index: Int? = null,
val playlistSetVideoId: String? = null,
val watchEndpointMusicSupportedConfigs: WatchEndpointMusicSupportedConfigs? = null,
) : Endpoint() {
val type: String?
get() = watchEndpointMusicSupportedConfigs
?.watchEndpointMusicConfig
?.musicVideoType
@Serializable
data class WatchEndpointMusicSupportedConfigs(
val watchEndpointMusicConfig: WatchEndpointMusicConfig?
) {
@Serializable
data class WatchEndpointMusicConfig(
val musicVideoType: String?
)
}
}
@Serializable
data class WatchPlaylist(
val params: String?,
val playlistId: String?,
) : Endpoint()
@Serializable
data class Browse(
val params: String? = null,
val browseId: String? = null,
val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs? = null,
) : Endpoint() {
val type: String?
get() = browseEndpointContextSupportedConfigs
?.browseEndpointContextMusicConfig
?.pageType
@Serializable
data class BrowseEndpointContextSupportedConfigs(
val browseEndpointContextMusicConfig: BrowseEndpointContextMusicConfig
) {
@Serializable
data class BrowseEndpointContextMusicConfig(
val pageType: String
)
}
}
@Serializable
data class Search(
val params: String?,
val query: String,
) : Endpoint()
}
}
//@Serializable(with = NavigationEndpoint.Serializer::class)
//sealed class NavigationEndpoint {
// @Serializable
// data class Watch(
// val watchEndpoint: Data
// ) : NavigationEndpoint() {
// @Serializable
// data class Data(
// val params: String?,
// val playlistId: String,
// val videoId: String,
//// val index: Int?
// val watchEndpointMusicSupportedConfigs: WatchEndpointMusicSupportedConfigs,
// )
//
// @Serializable
// data class WatchEndpointMusicSupportedConfigs(
// val watchEndpointMusicConfig: WatchEndpointMusicConfig
// ) {
// @Serializable
// data class WatchEndpointMusicConfig(
// val musicVideoType: String
// )
// }
// }
//
// @Serializable
// data class WatchPlaylist(
// val watchPlaylistEndpoint: Data
// ) : NavigationEndpoint() {
// @Serializable
// data class Data(
// val params: String?,
// val playlistId: String,
// )
// }
//
// @Serializable
// data class Browse(
// val browseEndpoint: Data
// ) : NavigationEndpoint() {
// @Serializable
// data class Data(
// val params: String?,
// val browseId: String,
// val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs,
// )
//
// @Serializable
// data class BrowseEndpointContextSupportedConfigs(
// val browseEndpointContextMusicConfig: BrowseEndpointContextMusicConfig
// ) {
// @Serializable
// data class BrowseEndpointContextMusicConfig(
// val pageType: String
// )
// }
// }
//
// @Serializable
// data class Search(
// val searchEndpoint: Data
// ) : NavigationEndpoint() {
// @Serializable
// data class Data(
// val params: String?,
// val query: String,
// )
// }
//
// object Serializer : JsonContentPolymorphicSerializer<NavigationEndpoint>(NavigationEndpoint::class) {
// override fun selectDeserializer(element: JsonElement) = when {
// "watchEndpoint" in element.jsonObject -> Watch.serializer()
// "watchPlaylistEndpoint" in element.jsonObject -> WatchPlaylist.serializer()
// "browseEndpoint" in element.jsonObject -> Browse.serializer()
// "searchEndpoint" in element.jsonObject -> Search.serializer()
// else -> TODO()
// }
// }
//}

View File

@ -0,0 +1,87 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class NextResponse(
val contents: Contents?
) {
@Serializable
data class MusicQueueRenderer(
val content: Content?
) {
@Serializable
data class Content(
@JsonNames("playlistPanelContinuation")
val playlistPanelRenderer: PlaylistPanelRenderer?
) {
@Serializable
data class PlaylistPanelRenderer(
val contents: List<Content>?,
val continuations: List<Continuation>?,
) {
@Serializable
data class Content(
val playlistPanelVideoRenderer: PlaylistPanelVideoRenderer?,
val automixPreviewVideoRenderer: AutomixPreviewVideoRenderer?,
) {
@Serializable
data class AutomixPreviewVideoRenderer(
val content: Content?
) {
@Serializable
data class Content(
val automixPlaylistVideoRenderer: AutomixPlaylistVideoRenderer?
) {
@Serializable
data class AutomixPlaylistVideoRenderer(
val navigationEndpoint: NavigationEndpoint?
)
}
}
}
}
}
}
@Serializable
data class Contents(
val singleColumnMusicWatchNextResultsRenderer: SingleColumnMusicWatchNextResultsRenderer?
) {
@Serializable
data class SingleColumnMusicWatchNextResultsRenderer(
val tabbedRenderer: TabbedRenderer?
) {
@Serializable
data class TabbedRenderer(
val watchNextTabbedResultsRenderer: WatchNextTabbedResultsRenderer?
) {
@Serializable
data class WatchNextTabbedResultsRenderer(
val tabs: List<Tab>?
) {
@Serializable
data class Tab(
val tabRenderer: TabRenderer?
) {
@Serializable
data class TabRenderer(
val content: Content?,
val endpoint: NavigationEndpoint?,
val title: String?
) {
@Serializable
data class Content(
val musicQueueRenderer: MusicQueueRenderer?
)
}
}
}
}
}
}
}

View File

@ -0,0 +1,58 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.Serializable
@Serializable
data class PlayerResponse(
val playabilityStatus: PlayabilityStatus?,
val playerConfig: PlayerConfig?,
val streamingData: StreamingData?,
val videoDetails: VideoDetails?,
) {
@Serializable
data class PlayabilityStatus(
val status: String?
)
@Serializable
data class PlayerConfig(
val audioConfig: AudioConfig?
) {
@Serializable
data class AudioConfig(
private val loudnessDb: Double?
) {
// For music clients only
val normalizedLoudnessDb: Float?
get() = loudnessDb?.plus(7)?.toFloat()
}
}
@Serializable
data class StreamingData(
val adaptiveFormats: List<AdaptiveFormat>?
) {
val highestQualityFormat: AdaptiveFormat?
get() = adaptiveFormats?.findLast { it.itag == 251 || it.itag == 140 }
@Serializable
data class AdaptiveFormat(
val itag: Int,
val mimeType: String,
val bitrate: Long?,
val averageBitrate: Long?,
val contentLength: Long?,
val audioQuality: String?,
val approxDurationMs: Long?,
val lastModified: Long?,
val loudnessDb: Double?,
val audioSampleRate: Int?,
val url: String?,
)
}
@Serializable
data class VideoDetails(
val videoId: String?
)
}

View File

@ -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?,
)

View File

@ -0,0 +1,31 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.Serializable
@Serializable
data class Runs(
val runs: List<Run> = listOf()
) {
val text: String
get() = runs.joinToString("") { it.text ?: "" }
fun splitBySeparator(): List<List<Run>> {
return runs.flatMapIndexed { index, run ->
when {
index == 0 || index == runs.lastIndex -> listOf(index)
run.text == "" -> listOf(index - 1, index + 1)
else -> emptyList()
}
}.windowed(size = 2, step = 2) { (from, to) -> runs.slice(from..to) }.let {
it.ifEmpty {
listOf(runs)
}
}
}
@Serializable
data class Run(
val text: String?,
val navigationEndpoint: NavigationEndpoint?,
)
}

View File

@ -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?
)
}

View File

@ -0,0 +1,28 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.Serializable
@Serializable
data class SearchSuggestionsResponse(
val contents: List<Content>?
) {
@Serializable
data class Content(
val searchSuggestionsSectionRenderer: SearchSuggestionsSectionRenderer?
) {
@Serializable
data class SearchSuggestionsSectionRenderer(
val contents: List<Content>?
) {
@Serializable
data class Content(
val searchSuggestionRenderer: SearchSuggestionRenderer?
) {
@Serializable
data class SearchSuggestionRenderer(
val navigationEndpoint: NavigationEndpoint?,
)
}
}
}
}

View File

@ -0,0 +1,29 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class SectionListRenderer(
val contents: List<Content>?,
val continuations: List<Continuation>?
) {
@Serializable
data class Content(
@JsonNames("musicImmersiveCarouselShelfRenderer")
val musicCarouselShelfRenderer: MusicCarouselShelfRenderer?,
@JsonNames("musicPlaylistShelfRenderer")
val musicShelfRenderer: MusicShelfRenderer?,
val gridRenderer: GridRenderer?,
val musicDescriptionShelfRenderer: MusicDescriptionShelfRenderer?,
) {
@Serializable
data class MusicDescriptionShelfRenderer(
val description: Runs?,
)
}
}

View File

@ -0,0 +1,25 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.Serializable
@Serializable
data class Tabs(
val tabs: List<Tab>?
) {
@Serializable
data class Tab(
val tabRenderer: TabRenderer?
) {
@Serializable
data class TabRenderer(
val content: Content?,
val title: String?,
val tabIdentifier: String?,
) {
@Serializable
data class Content(
val sectionListRenderer: SectionListRenderer?,
)
}
}
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,22 @@
package com.player.musicoo.innertube.models
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class ThumbnailRenderer(
@JsonNames("croppedSquareThumbnailRenderer")
val musicThumbnailRenderer: MusicThumbnailRenderer?
) {
@Serializable
data class MusicThumbnailRenderer(
val thumbnail: Thumbnail?
) {
@Serializable
data class Thumbnail(
val thumbnails: List<com.player.musicoo.innertube.models.Thumbnail>?
)
}
}

View File

@ -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
)

View File

@ -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,
)

View File

@ -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
)
}

View File

@ -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
)

View File

@ -0,0 +1,11 @@
package com.player.musicoo.innertube.models.bodies
import com.player.musicoo.innertube.models.Context
import kotlinx.serialization.Serializable
@Serializable
data class QueueBody(
val context: Context = Context.DefaultWeb,
val videoIds: List<String>? = null,
val playlistId: String? = null,
)

View File

@ -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
)

View File

@ -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
)

View File

@ -0,0 +1,36 @@
package com.player.musicoo.innertube.requests
import io.ktor.http.Url
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.NavigationEndpoint
import com.player.musicoo.innertube.models.bodies.BrowseBody
suspend fun Innertube.albumPage(body: BrowseBody): Result<Innertube.PlaylistOrAlbumPage>? {
return playlistPage(body)?.map { album ->
album.url?.let { Url(it).parameters["list"] }?.let { playlistId ->
playlistPage(BrowseBody(browseId = "VL$playlistId"))?.getOrNull()?.let { playlist ->
album.copy(songsPage = playlist.songsPage)
}
} ?: album
}?.map { album ->
val albumInfo = Innertube.Info(
name = album.title,
endpoint = NavigationEndpoint.Endpoint.Browse(
browseId = body.browseId,
params = body.params
)
)
album.copy(
songsPage = album.songsPage?.copy(
items = album.songsPage.items?.map { song ->
song.copy(
authors = song.authors ?: album.authors,
album = albumInfo,
thumbnail = album.thumbnail
)
}
)
)
}
}

View File

@ -0,0 +1,106 @@
package com.player.musicoo.innertube.requests
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.BrowseResponse
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
import com.player.musicoo.innertube.models.MusicShelfRenderer
import com.player.musicoo.innertube.models.SectionListRenderer
import com.player.musicoo.innertube.models.bodies.BrowseBody
import com.player.musicoo.innertube.utils.findSectionByTitle
import com.player.musicoo.innertube.utils.from
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
suspend fun Innertube.artistPage(body: BrowseBody): Result<Innertube.ArtistPage>? =
runCatchingNonCancellable {
val response = client.post(browse) {
setBody(body)
mask("contents,header")
}.body<BrowseResponse>()
fun findSectionByTitle(text: String): SectionListRenderer.Content? {
return response
.contents
?.singleColumnBrowseResultsRenderer
?.tabs
?.get(0)
?.tabRenderer
?.content
?.sectionListRenderer
?.findSectionByTitle(text)
}
val songsSection = findSectionByTitle("Songs")?.musicShelfRenderer
val albumsSection = findSectionByTitle("Albums")?.musicCarouselShelfRenderer
val singlesSection = findSectionByTitle("Singles")?.musicCarouselShelfRenderer
Innertube.ArtistPage(
name = response
.header
?.musicImmersiveHeaderRenderer
?.title
?.text,
description = response
.header
?.musicImmersiveHeaderRenderer
?.description
?.text,
thumbnail = (response
.header
?.musicImmersiveHeaderRenderer
?.foregroundThumbnail
?: response
.header
?.musicImmersiveHeaderRenderer
?.thumbnail)
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.getOrNull(0),
shuffleEndpoint = response
.header
?.musicImmersiveHeaderRenderer
?.playButton
?.buttonRenderer
?.navigationEndpoint
?.watchEndpoint,
radioEndpoint = response
.header
?.musicImmersiveHeaderRenderer
?.startRadioButton
?.buttonRenderer
?.navigationEndpoint
?.watchEndpoint,
songs = songsSection
?.contents
?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
?.mapNotNull(Innertube.SongItem::from),
songsEndpoint = songsSection
?.bottomEndpoint
?.browseEndpoint,
albums = albumsSection
?.contents
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
?.mapNotNull(Innertube.AlbumItem::from),
albumsEndpoint = albumsSection
?.header
?.musicCarouselShelfBasicHeaderRenderer
?.moreContentButton
?.buttonRenderer
?.navigationEndpoint
?.browseEndpoint,
singles = singlesSection
?.contents
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
?.mapNotNull(Innertube.AlbumItem::from),
singlesEndpoint = singlesSection
?.header
?.musicCarouselShelfBasicHeaderRenderer
?.moreContentButton
?.buttonRenderer
?.navigationEndpoint
?.browseEndpoint,
)
}

View File

@ -0,0 +1,58 @@
package com.player.musicoo.innertube.requests
import android.util.Log
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.BrowseResponse
import com.player.musicoo.innertube.models.Context
import com.player.musicoo.innertube.models.SectionListRenderer
import com.player.musicoo.innertube.models.bodies.BrowseBody
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody
//
suspend fun Innertube.homePage(): Result<List<Innertube.HomePage>>? =
runCatchingNonCancellable {
val response = client.post(browse) {
setBody(BrowseBody(Context.DefaultWeb, "FEmusic_home"))
}.body<BrowseResponse>()
val sectionListRenderer = response
.contents
?.singleColumnBrowseResultsRenderer
?.tabs
?.firstOrNull()
?.tabRenderer
?.content
?.sectionListRenderer
val contents = sectionListRenderer?.contents
Log.d("ocean","contents->$contents")
val searchList: MutableList<Innertube.HomePage> = mutableListOf()
if (contents != null) {
for (content: SectionListRenderer.Content in contents) {
val text = content.musicCarouselShelfRenderer
?.header
?.musicCarouselShelfBasicHeaderRenderer
?.title
?.runs
?.firstOrNull()
?.text
val homePage =
content.musicCarouselShelfRenderer?.contents?.let {
Innertube.HomePage(text,
it
)
}
if (homePage != null) {
searchList.add(homePage)
}
}
}
searchList
}

View File

@ -0,0 +1,97 @@
package com.player.musicoo.innertube.requests
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.BrowseResponse
import com.player.musicoo.innertube.models.ContinuationResponse
import com.player.musicoo.innertube.models.GridRenderer
import com.player.musicoo.innertube.models.MusicResponsiveListItemRenderer
import com.player.musicoo.innertube.models.MusicShelfRenderer
import com.player.musicoo.innertube.models.MusicTwoRowItemRenderer
import com.player.musicoo.innertube.models.bodies.BrowseBody
import com.player.musicoo.innertube.models.bodies.ContinuationBody
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
suspend fun <T : Innertube.Item> Innertube.itemsPage(
body: BrowseBody,
fromMusicResponsiveListItemRenderer: (MusicResponsiveListItemRenderer) -> T? = { null },
fromMusicTwoRowItemRenderer: (MusicTwoRowItemRenderer) -> T? = { null },
) = runCatchingNonCancellable {
val response = client.post(browse) {
setBody(body)
// mask("contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents(musicPlaylistShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask),gridRenderer(continuations,items.$musicTwoRowItemRendererMask))")
}.body<BrowseResponse>()
val sectionListRendererContent = response
.contents
?.singleColumnBrowseResultsRenderer
?.tabs
?.firstOrNull()
?.tabRenderer
?.content
?.sectionListRenderer
?.contents
?.firstOrNull()
itemsPageFromMusicShelRendererOrGridRenderer(
musicShelfRenderer = sectionListRendererContent
?.musicShelfRenderer,
gridRenderer = sectionListRendererContent
?.gridRenderer,
fromMusicResponsiveListItemRenderer = fromMusicResponsiveListItemRenderer,
fromMusicTwoRowItemRenderer = fromMusicTwoRowItemRenderer,
)
}
suspend fun <T : Innertube.Item> Innertube.itemsPage(
body: ContinuationBody,
fromMusicResponsiveListItemRenderer: (MusicResponsiveListItemRenderer) -> T? = { null },
fromMusicTwoRowItemRenderer: (MusicTwoRowItemRenderer) -> T? = { null },
) = runCatchingNonCancellable {
val response = client.post(browse) {
setBody(body)
// mask("contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents(musicPlaylistShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask),gridRenderer(continuations,items.$musicTwoRowItemRendererMask))")
}.body<ContinuationResponse>()
itemsPageFromMusicShelRendererOrGridRenderer(
musicShelfRenderer = response
.continuationContents
?.musicShelfContinuation,
gridRenderer = null,
fromMusicResponsiveListItemRenderer = fromMusicResponsiveListItemRenderer,
fromMusicTwoRowItemRenderer = fromMusicTwoRowItemRenderer,
)
}
private fun <T : Innertube.Item> itemsPageFromMusicShelRendererOrGridRenderer(
musicShelfRenderer: MusicShelfRenderer?,
gridRenderer: GridRenderer?,
fromMusicResponsiveListItemRenderer: (MusicResponsiveListItemRenderer) -> T?,
fromMusicTwoRowItemRenderer: (MusicTwoRowItemRenderer) -> T?,
): Innertube.ItemsPage<T>? {
return if (musicShelfRenderer != null) {
Innertube.ItemsPage(
continuation = musicShelfRenderer
.continuations
?.firstOrNull()
?.nextContinuationData
?.continuation,
items = musicShelfRenderer
.contents
?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
?.mapNotNull(fromMusicResponsiveListItemRenderer)
)
} else if (gridRenderer != null) {
Innertube.ItemsPage(
continuation = null,
items = gridRenderer
.items
?.mapNotNull(GridRenderer.Item::musicTwoRowItemRenderer)
?.mapNotNull(fromMusicTwoRowItemRenderer)
)
} else {
null
}
}

View File

@ -0,0 +1,44 @@
package com.player.musicoo.innertube.requests
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.BrowseResponse
import com.player.musicoo.innertube.models.NextResponse
import com.player.musicoo.innertube.models.bodies.BrowseBody
import com.player.musicoo.innertube.models.bodies.NextBody
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
suspend fun Innertube.lyrics(body: NextBody): Result<String?>? = runCatchingNonCancellable {
val nextResponse = client.post(next) {
setBody(body)
mask("contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer(endpoint,title)")
}.body<NextResponse>()
val browseId = nextResponse
.contents
?.singleColumnMusicWatchNextResultsRenderer
?.tabbedRenderer
?.watchNextTabbedResultsRenderer
?.tabs
?.getOrNull(1)
?.tabRenderer
?.endpoint
?.browseEndpoint
?.browseId
?: return@runCatchingNonCancellable null
val response = client.post(browse) {
setBody(BrowseBody(browseId = browseId))
mask("contents.sectionListRenderer.contents.musicDescriptionShelfRenderer.description")
}.body<BrowseResponse>()
response.contents
?.sectionListRenderer
?.contents
?.firstOrNull()
?.musicDescriptionShelfRenderer
?.description
?.text
}

View File

@ -0,0 +1,90 @@
package com.player.musicoo.innertube.requests
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.ContinuationResponse
import com.player.musicoo.innertube.models.NextResponse
import com.player.musicoo.innertube.models.bodies.ContinuationBody
import com.player.musicoo.innertube.models.bodies.NextBody
import com.player.musicoo.innertube.utils.from
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
suspend fun Innertube.nextPage(body: NextBody): Result<Innertube.NextPage>? =
runCatchingNonCancellable {
val response = client.post(next) {
setBody(body)
mask("contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer.content.musicQueueRenderer.content.playlistPanelRenderer(continuations,contents(automixPreviewVideoRenderer,$playlistPanelVideoRendererMask))")
}.body<NextResponse>()
val tabs = response
.contents
?.singleColumnMusicWatchNextResultsRenderer
?.tabbedRenderer
?.watchNextTabbedResultsRenderer
?.tabs
val playlistPanelRenderer = tabs
?.getOrNull(0)
?.tabRenderer
?.content
?.musicQueueRenderer
?.content
?.playlistPanelRenderer
if (body.playlistId == null) {
val endpoint = playlistPanelRenderer
?.contents
?.lastOrNull()
?.automixPreviewVideoRenderer
?.content
?.automixPlaylistVideoRenderer
?.navigationEndpoint
?.watchPlaylistEndpoint
if (endpoint != null) {
return nextPage(
body.copy(
playlistId = endpoint.playlistId,
params = endpoint.params
)
)
}
}
Innertube.NextPage(
playlistId = body.playlistId,
playlistSetVideoId = body.playlistSetVideoId,
params = body.params,
itemsPage = playlistPanelRenderer
?.toSongsPage()
)
}
suspend fun Innertube.nextPage(body: ContinuationBody) = runCatchingNonCancellable {
val response = client.post(next) {
setBody(body)
mask("continuationContents.playlistPanelContinuation(continuations,contents.$playlistPanelVideoRendererMask)")
}.body<ContinuationResponse>()
response
.continuationContents
?.playlistPanelContinuation
?.toSongsPage()
}
private fun NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer?.toSongsPage() =
Innertube.ItemsPage(
items = this
?.contents
?.mapNotNull(NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer.Content::playlistPanelVideoRenderer)
?.mapNotNull(Innertube.SongItem::from),
continuation = this
?.continuations
?.firstOrNull()
?.nextContinuationData
?.continuation
)

View File

@ -0,0 +1,67 @@
package com.player.musicoo.innertube.requests
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.contentType
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.Context
import com.player.musicoo.innertube.models.PlayerResponse
import com.player.musicoo.innertube.models.bodies.PlayerBody
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
import kotlinx.serialization.Serializable
suspend fun Innertube.player(body: PlayerBody) = runCatchingNonCancellable {
val response = client.post(player) {
setBody(body)
mask("playabilityStatus.status,playerConfig.audioConfig,streamingData.adaptiveFormats,videoDetails.videoId")
}.body<PlayerResponse>()
if (response.playabilityStatus?.status == "OK") {
response
} else {
@Serializable
data class AudioStream(
val url: String,
val bitrate: Long
)
@Serializable
data class PipedResponse(
val audioStreams: List<AudioStream>
)
val safePlayerResponse = client.post(player) {
setBody(
body.copy(
context = Context.DefaultAgeRestrictionBypass.copy(
thirdParty = Context.ThirdParty(
embedUrl = "https://www.youtube.com/watch?v=${body.videoId}"
)
),
)
)
mask("playabilityStatus.status,playerConfig.audioConfig,streamingData.adaptiveFormats,videoDetails.videoId")
}.body<PlayerResponse>()
if (safePlayerResponse.playabilityStatus?.status != "OK") {
return@runCatchingNonCancellable response
}
val audioStreams = client.get("https://watchapi.whatever.social/streams/${body.videoId}") {
contentType(ContentType.Application.Json)
}.body<PipedResponse>().audioStreams
safePlayerResponse.copy(
streamingData = safePlayerResponse.streamingData?.copy(
adaptiveFormats = safePlayerResponse.streamingData.adaptiveFormats?.map { adaptiveFormat ->
adaptiveFormat.copy(
url = audioStreams.find { it.bitrate == adaptiveFormat.bitrate }?.url
)
}
)
)
}
}

View File

@ -0,0 +1,101 @@
package com.player.musicoo.innertube.requests
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.BrowseResponse
import com.player.musicoo.innertube.models.ContinuationResponse
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
import com.player.musicoo.innertube.models.MusicShelfRenderer
import com.player.musicoo.innertube.models.bodies.BrowseBody
import com.player.musicoo.innertube.models.bodies.ContinuationBody
import com.player.musicoo.innertube.utils.from
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
suspend fun Innertube.playlistPage(body: BrowseBody) = runCatchingNonCancellable {
val response = client.post(browse) {
setBody(body)
mask("contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents(musicPlaylistShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask),musicCarouselShelfRenderer.contents.$musicTwoRowItemRendererMask),header.musicDetailHeaderRenderer(title,subtitle,thumbnail),microformat")
}.body<BrowseResponse>()
val musicDetailHeaderRenderer = response
.header
?.musicDetailHeaderRenderer
val sectionListRendererContents = response
.contents
?.singleColumnBrowseResultsRenderer
?.tabs
?.firstOrNull()
?.tabRenderer
?.content
?.sectionListRenderer
?.contents
val musicShelfRenderer = sectionListRendererContents
?.firstOrNull()
?.musicShelfRenderer
val musicCarouselShelfRenderer = sectionListRendererContents
?.getOrNull(1)
?.musicCarouselShelfRenderer
Innertube.PlaylistOrAlbumPage(
title = musicDetailHeaderRenderer
?.title
?.text,
thumbnail = musicDetailHeaderRenderer
?.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.firstOrNull(),
authors = musicDetailHeaderRenderer
?.subtitle
?.splitBySeparator()
?.getOrNull(1)
?.map(Innertube::Info),
year = musicDetailHeaderRenderer
?.subtitle
?.splitBySeparator()
?.getOrNull(2)
?.firstOrNull()
?.text,
url = response
.microformat
?.microformatDataRenderer
?.urlCanonical,
songsPage = musicShelfRenderer
?.toSongsPage(),
otherVersions = musicCarouselShelfRenderer
?.contents
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
?.mapNotNull(Innertube.AlbumItem::from)
)
}
suspend fun Innertube.playlistPage(body: ContinuationBody) = runCatchingNonCancellable {
val response = client.post(browse) {
setBody(body)
mask("continuationContents.musicPlaylistShelfContinuation(continuations,contents.$musicResponsiveListItemRendererMask)")
}.body<ContinuationResponse>()
response
.continuationContents
?.musicShelfContinuation
?.toSongsPage()
}
private fun MusicShelfRenderer?.toSongsPage() =
Innertube.ItemsPage(
items = this
?.contents
?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
?.mapNotNull(Innertube.SongItem::from),
continuation = this
?.continuations
?.firstOrNull()
?.nextContinuationData
?.continuation
)

View File

@ -0,0 +1,29 @@
package com.player.musicoo.innertube.requests
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.GetQueueResponse
import com.player.musicoo.innertube.models.bodies.QueueBody
import com.player.musicoo.innertube.utils.from
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
suspend fun Innertube.queue(body: QueueBody) = runCatchingNonCancellable {
val response = client.post(queue) {
setBody(body)
mask("queueDatas.content.$playlistPanelVideoRendererMask")
}.body<GetQueueResponse>()
response
.queueDatas
?.mapNotNull { queueData ->
queueData
.content
?.playlistPanelVideoRenderer
?.let(Innertube.SongItem::from)
}
}
suspend fun Innertube.song(videoId: String): Result<Innertube.SongItem?>? =
queue(QueueBody(videoIds = listOf(videoId)))?.map { it?.firstOrNull() }

View File

@ -0,0 +1,72 @@
package com.player.musicoo.innertube.requests
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.BrowseResponse
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
import com.player.musicoo.innertube.models.NextResponse
import com.player.musicoo.innertube.models.bodies.BrowseBody
import com.player.musicoo.innertube.models.bodies.NextBody
import com.player.musicoo.innertube.utils.findSectionByStrapline
import com.player.musicoo.innertube.utils.findSectionByTitle
import com.player.musicoo.innertube.utils.from
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
suspend fun Innertube.relatedPage(body: NextBody) = runCatchingNonCancellable {
val nextResponse = client.post(next) {
setBody(body)
mask("contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer(endpoint,title)")
}.body<NextResponse>()
val browseId = nextResponse
.contents
?.singleColumnMusicWatchNextResultsRenderer
?.tabbedRenderer
?.watchNextTabbedResultsRenderer
?.tabs
?.getOrNull(2)
?.tabRenderer
?.endpoint
?.browseEndpoint
?.browseId
?: return@runCatchingNonCancellable null
val response = client.post(browse) {
setBody(BrowseBody(browseId = browseId))
mask("contents.sectionListRenderer.contents.musicCarouselShelfRenderer(header.musicCarouselShelfBasicHeaderRenderer(title,strapline),contents($musicResponsiveListItemRendererMask,$musicTwoRowItemRendererMask))")
}.body<BrowseResponse>()
val sectionListRenderer = response
.contents
?.sectionListRenderer
Innertube.RelatedPage(
songs = sectionListRenderer
?.findSectionByTitle("You might also like")
?.musicCarouselShelfRenderer
?.contents
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicResponsiveListItemRenderer)
?.mapNotNull(Innertube.SongItem::from),
playlists = sectionListRenderer
?.findSectionByTitle("Recommended playlists")
?.musicCarouselShelfRenderer
?.contents
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
?.mapNotNull(Innertube.PlaylistItem::from)
?.sortedByDescending { it.channel?.name == "YouTube Music" },
albums = sectionListRenderer
?.findSectionByStrapline("MORE FROM")
?.musicCarouselShelfRenderer
?.contents
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
?.mapNotNull(Innertube.AlbumItem::from),
artists = sectionListRenderer
?.findSectionByTitle("Similar artists")
?.musicCarouselShelfRenderer
?.contents
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
?.mapNotNull(Innertube.ArtistItem::from),
)
}

View File

@ -0,0 +1,62 @@
package com.player.musicoo.innertube.requests
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.ContinuationResponse
import com.player.musicoo.innertube.models.MusicShelfRenderer
import com.player.musicoo.innertube.models.SearchResponse
import com.player.musicoo.innertube.models.bodies.ContinuationBody
import com.player.musicoo.innertube.models.bodies.SearchBody
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
suspend fun <T : Innertube.Item> Innertube.searchPage(
body: SearchBody,
fromMusicShelfRendererContent: (MusicShelfRenderer.Content) -> T?
) = runCatchingNonCancellable {
val response = client.post(search) {
setBody(body)
mask("contents.tabbedSearchResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents.musicShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask)")
}.body<SearchResponse>()
response
.contents
?.tabbedSearchResultsRenderer
?.tabs
?.firstOrNull()
?.tabRenderer
?.content
?.sectionListRenderer
?.contents
?.lastOrNull()
?.musicShelfRenderer
?.toItemsPage(fromMusicShelfRendererContent)
}
suspend fun <T : Innertube.Item> Innertube.searchPage(
body: ContinuationBody,
fromMusicShelfRendererContent: (MusicShelfRenderer.Content) -> T?
) = runCatchingNonCancellable {
val response = client.post(search) {
setBody(body)
mask("continuationContents.musicShelfContinuation(continuations,contents.$musicResponsiveListItemRendererMask)")
}.body<ContinuationResponse>()
response
.continuationContents
?.musicShelfContinuation
?.toItemsPage(fromMusicShelfRendererContent)
}
private fun <T : Innertube.Item> MusicShelfRenderer?.toItemsPage(mapper: (MusicShelfRenderer.Content) -> T?) =
Innertube.ItemsPage(
items = this
?.contents
?.mapNotNull(mapper),
continuation = this
?.continuations
?.firstOrNull()
?.nextContinuationData
?.continuation
)

View File

@ -0,0 +1,29 @@
package com.player.musicoo.innertube.requests
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.SearchSuggestionsResponse
import com.player.musicoo.innertube.models.bodies.SearchSuggestionsBody
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
suspend fun Innertube.searchSuggestions(body: SearchSuggestionsBody) = runCatchingNonCancellable {
val response = client.post(searchSuggestions) {
setBody(body)
mask("contents.searchSuggestionsSectionRenderer.contents.searchSuggestionRenderer.navigationEndpoint.searchEndpoint.query")
}.body<SearchSuggestionsResponse>()
response
.contents
?.firstOrNull()
?.searchSuggestionsSectionRenderer
?.contents
?.mapNotNull { content ->
content
.searchSuggestionRenderer
?.navigationEndpoint
?.searchEndpoint
?.query
}
}

View File

@ -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()
}

View File

@ -0,0 +1,49 @@
package com.player.musicoo.innertube.utils
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.MusicResponsiveListItemRenderer
import com.player.musicoo.innertube.models.NavigationEndpoint
import com.player.musicoo.innertube.models.Runs
fun Innertube.SongItem.Companion.from(renderer: MusicResponsiveListItemRenderer): Innertube.SongItem? {
return Innertube.SongItem(
info = renderer
.flexColumns
.getOrNull(0)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.getOrNull(0)
?.let(Innertube::Info),
authors = renderer
.flexColumns
.getOrNull(1)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.map<Runs.Run, Innertube.Info<NavigationEndpoint.Endpoint.Browse>>(Innertube::Info)
?.takeIf(List<Any>::isNotEmpty),
durationText = renderer
.fixedColumns
?.getOrNull(0)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.getOrNull(0)
?.text,
album = renderer
.flexColumns
.getOrNull(2)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.firstOrNull()
?.let(Innertube::Info),
thumbnail = renderer
.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.firstOrNull()
).takeIf { it.info?.endpoint?.videoId != null }
}

View File

@ -0,0 +1,140 @@
package com.player.musicoo.innertube.utils
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.MusicShelfRenderer
import com.player.musicoo.innertube.models.NavigationEndpoint
fun Innertube.SongItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.SongItem? {
val (mainRuns, otherRuns) = content.runs
// Possible configurations:
// "song" • author(s) • album • duration
// "song" • author(s) • duration
// author(s) • album • duration
// author(s) • duration
val album: Innertube.Info<NavigationEndpoint.Endpoint.Browse>? = otherRuns
.getOrNull(otherRuns.lastIndex - 1)
?.firstOrNull()
?.takeIf { run ->
run
.navigationEndpoint
?.browseEndpoint
?.type == "MUSIC_PAGE_TYPE_ALBUM"
}
?.let(Innertube::Info)
return Innertube.SongItem(
info = mainRuns
.firstOrNull()
?.let(Innertube::Info),
authors = otherRuns
.getOrNull(otherRuns.lastIndex - if (album == null) 1 else 2)
?.map(Innertube::Info),
album = album,
durationText = otherRuns
.lastOrNull()
?.firstOrNull()?.text,
thumbnail = content
.thumbnail
).takeIf { it.info?.endpoint?.videoId != null }
}
fun Innertube.VideoItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.VideoItem? {
val (mainRuns, otherRuns) = content.runs
return Innertube.VideoItem(
info = mainRuns
.firstOrNull()
?.let(Innertube::Info),
authors = otherRuns
.getOrNull(otherRuns.lastIndex - 2)
?.map(Innertube::Info),
viewsText = otherRuns
.getOrNull(otherRuns.lastIndex - 1)
?.firstOrNull()
?.text,
durationText = otherRuns
.getOrNull(otherRuns.lastIndex)
?.firstOrNull()
?.text,
thumbnail = content
.thumbnail
).takeIf { it.info?.endpoint?.videoId != null }
}
fun Innertube.AlbumItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.AlbumItem? {
val (mainRuns, otherRuns) = content.runs
return Innertube.AlbumItem(
info = Innertube.Info(
name = mainRuns
.firstOrNull()
?.text,
endpoint = content
.musicResponsiveListItemRenderer
?.navigationEndpoint
?.browseEndpoint
),
authors = otherRuns
.getOrNull(otherRuns.lastIndex - 1)
?.map(Innertube::Info),
year = otherRuns
.getOrNull(otherRuns.lastIndex)
?.firstOrNull()
?.text,
thumbnail = content
.thumbnail
).takeIf { it.info?.endpoint?.browseId != null }
}
fun Innertube.ArtistItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.ArtistItem? {
val (mainRuns, otherRuns) = content.runs
return Innertube.ArtistItem(
info = Innertube.Info(
name = mainRuns
.firstOrNull()
?.text,
endpoint = content
.musicResponsiveListItemRenderer
?.navigationEndpoint
?.browseEndpoint
),
subscribersCountText = otherRuns
.lastOrNull()
?.last()
?.text,
thumbnail = content
.thumbnail
).takeIf { it.info?.endpoint?.browseId != null }
}
fun Innertube.PlaylistItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.PlaylistItem? {
val (mainRuns, otherRuns) = content.runs
return Innertube.PlaylistItem(
info = Innertube.Info(
name = mainRuns
.firstOrNull()
?.text,
endpoint = content
.musicResponsiveListItemRenderer
?.navigationEndpoint
?.browseEndpoint
),
channel = otherRuns
.firstOrNull()
?.firstOrNull()
?.let(Innertube::Info),
songCount = otherRuns
.lastOrNull()
?.firstOrNull()
?.text
?.split(' ')
?.firstOrNull()
?.toIntOrNull(),
thumbnail = content
.thumbnail
).takeIf { it.info?.endpoint?.browseId != null }
}

View File

@ -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 }
}

View File

@ -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 }
}

View File

@ -0,0 +1,50 @@
package com.player.musicoo.innertube.utils
import io.ktor.utils.io.CancellationException
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.SectionListRenderer
internal fun SectionListRenderer.findSectionByTitle(text: String): SectionListRenderer.Content? {
return contents?.find { content ->
val title = content
.musicCarouselShelfRenderer
?.header
?.musicCarouselShelfBasicHeaderRenderer
?.title
?: content
.musicShelfRenderer
?.title
title
?.runs
?.firstOrNull()
?.text == text
}
}
internal fun SectionListRenderer.findSectionByStrapline(text: String): SectionListRenderer.Content? {
return contents?.find { content ->
content
.musicCarouselShelfRenderer
?.header
?.musicCarouselShelfBasicHeaderRenderer
?.strapline
?.runs
?.firstOrNull()
?.text == text
}
}
internal inline fun <R> runCatchingNonCancellable(block: () -> R): Result<R>? {
val result = runCatching(block)
return when (result.exceptionOrNull()) {
is CancellationException -> null
else -> result
}
}
infix operator fun <T : Innertube.Item> Innertube.ItemsPage<T>?.plus(other: Innertube.ItemsPage<T>) =
other.copy(
items = (this?.items?.plus(other.items ?: emptyList())
?: other.items)?.distinctBy(Innertube.Item::key)
)

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -0,0 +1,19 @@
package com.player.musicoo.sp
import android.content.Context
import com.player.musicoo.sp.store.Store
import com.player.musicoo.sp.store.asStoreProvider
class AppStore(context: Context) {
private val store = Store(
context
.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
.asStoreProvider()
)
companion object {
private const val FILE_NAME = "music_oo_app"
}
}

View File

@ -0,0 +1,60 @@
package com.player.musicoo.sp.store
import android.content.SharedPreferences
import androidx.core.content.edit
class SharedPreferenceProvider(private val preferences: SharedPreferences) : StoreProvider {
override fun getInt(key: String, defaultValue: Int): Int {
return preferences.getInt(key, defaultValue)
}
override fun setInt(key: String, value: Int) {
preferences.edit {
putInt(key, value)
}
}
override fun getLong(key: String, defaultValue: Long): Long {
return preferences.getLong(key, defaultValue)
}
override fun setLong(key: String, value: Long) {
preferences.edit {
putLong(key, value)
}
}
override fun getString(key: String, defaultValue: String): String {
return preferences.getString(key, defaultValue)!!
}
override fun setString(key: String, value: String) {
preferences.edit {
putString(key, value)
}
}
override fun getStringSet(key: String, defaultValue: Set<String>): Set<String> {
return preferences.getStringSet(key, defaultValue)!!
}
override fun setStringSet(key: String, value: Set<String>) {
preferences.edit {
putStringSet(key, value)
}
}
override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
return preferences.getBoolean(key, defaultValue)
}
override fun setBoolean(key: String, value: Boolean) {
preferences.edit {
putBoolean(key, value)
}
}
}
fun SharedPreferences.asStoreProvider(): StoreProvider {
return SharedPreferenceProvider(this)
}

View File

@ -0,0 +1,98 @@
package com.player.musicoo.sp.store
import kotlin.reflect.KProperty
class Store(val provider: StoreProvider) {
interface Delegate<T> {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
}
fun int(key: String, defaultValue: Int): Delegate<Int> {
return object : Delegate<Int> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return provider.getInt(key, defaultValue)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
provider.setInt(key, value)
}
}
}
fun long(key: String, defaultValue: Long): Delegate<Long> {
return object : Delegate<Long> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Long {
return provider.getLong(key, defaultValue)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) {
provider.setLong(key, value)
}
}
}
fun string(key: String, defaultValue: String): Delegate<String> {
return object : Delegate<String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return provider.getString(key, defaultValue)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
provider.setString(key, value)
}
}
}
fun stringSet(key: String, defaultValue: Set<String>): Delegate<Set<String>> {
return object : Delegate<Set<String>> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Set<String> {
return provider.getStringSet(key, defaultValue)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set<String>) {
provider.setStringSet(key, value)
}
}
}
fun boolean(key: String, defaultValue: Boolean): Delegate<Boolean> {
return object : Delegate<Boolean> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean {
return provider.getBoolean(key, defaultValue)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
provider.setBoolean(key, value)
}
}
}
fun <T : Enum<T>> enum(key: String, defaultValue: T, values: Array<T>): Delegate<T> {
return object : Delegate<T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
val name = provider.getString(key, defaultValue.name)
return values.find { name == it.name } ?: defaultValue
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
provider.setString(key, value.name)
}
}
}
fun <T> typedString(key: String, from: (String) -> T?, to: (T?) -> String): Delegate<T?> {
return object : Delegate<T?> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T? {
val value = provider.getString(key, to(null))
return from(value)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
provider.setString(key, to(value))
}
}
}
}

View File

@ -0,0 +1,18 @@
package com.player.musicoo.sp.store
interface StoreProvider {
fun getInt(key: String, defaultValue: Int): Int
fun setInt(key: String, value: Int)
fun getLong(key: String, defaultValue: Long): Long
fun setLong(key: String, value: Long)
fun getString(key: String, defaultValue: String): String
fun setString(key: String, value: String)
fun getStringSet(key: String, defaultValue: Set<String>): Set<String>
fun setStringSet(key: String, value: Set<String>)
fun getBoolean(key: String, defaultValue: Boolean): Boolean
fun setBoolean(key: String, value: Boolean)
}

View File

@ -0,0 +1,6 @@
package com.player.musicoo.util
object LogTag {
const val VO_ACT_LOG = "vo-act—log"
const val VO_FRAGMENT_LOG = "vo_fragment_log"
}

View File

@ -0,0 +1,19 @@
package com.player.musicoo.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
open class ModuleView : FrameLayout {
protected var contentView: View? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
}

View File

@ -0,0 +1,30 @@
package com.player.musicoo.view
import android.annotation.SuppressLint
import android.content.Context
import android.widget.TextView
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.adapter.ResponsiveListAdapter
import com.player.musicoo.adapter.SoundsOfNatureAdapter
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.util.GridSpacingItemDecoration
@SuppressLint("ViewConstructor")
class MusicResponsiveListView(context: Context, homePage: Innertube.HomePage) :
ModuleView(context) {
init {
contentView = inflate(getContext(), R.layout.music_list_layout, this)
val title = contentView?.findViewById<TextView>(R.id.title)
title?.text = homePage.title
val rv = contentView?.findViewById<RecyclerView>(R.id.rv)
val adapter = ResponsiveListAdapter(context, homePage.contents)
rv?.layoutManager = GridLayoutManager(context, 3, GridLayoutManager.HORIZONTAL, false)
rv?.adapter = adapter
}
}

View File

@ -0,0 +1,28 @@
package com.player.musicoo.view
import android.annotation.SuppressLint
import android.content.Context
import android.widget.TextView
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.player.musicoo.R
import com.player.musicoo.adapter.ResponsiveListAdapter
import com.player.musicoo.adapter.TowRowListAdapter
import com.player.musicoo.innertube.Innertube
@SuppressLint("ViewConstructor")
class MusicTowRowListView(context: Context, homePage: Innertube.HomePage) : ModuleView(context) {
init {
contentView = inflate(getContext(), R.layout.music_list_layout, this)
val title = contentView?.findViewById<TextView>(R.id.title)
title?.text = homePage.title
val rv = contentView?.findViewById<RecyclerView>(R.id.rv)
val adapter = TowRowListAdapter(context, homePage.contents)
rv?.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
rv?.adapter = adapter
}
}

View File

@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_bg_color">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<FrameLayout
android:id="@+id/frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/bottom_layout"
android:layout_below="@+id/view" />
<LinearLayout
android:id="@+id/bottom_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="vertical">
<com.lihang.ShadowLayout
android:id="@+id/playing_status_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:visibility="gone"
android:layout_marginEnd="12dp"
app:hl_cornerRadius="36dp"
app:hl_layoutBackground="#FF80F988"
app:hl_shadowColor="#40040604"
app:hl_shadowOffsetX="0dp"
app:hl_shadowOffsetY="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="78dp"
android:layout_alignParentBottom="true"
android:gravity="center_vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:elevation="0dp"
app:cardCornerRadius="24dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/audio_img"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/breathe" />
</androidx.cardview.widget.CardView>
<com.player.musicoo.view.CircularProgressBar
android:id="@+id/progressBar"
android:layout_width="54dp"
android:layout_height="54dp"
android:layout_centerInParent="true" />
</RelativeLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="marquee"
android:fontFamily="@font/medium_font"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="16dp" />
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/black_60"
android:textSize="12dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/alarm_clock_btn"
android:layout_width="42dp"
android:visibility="gone"
android:layout_height="42dp"
android:gravity="center">
<ImageView
android:layout_width="34dp"
android:layout_height="34dp"
android:src="@drawable/alarm_clock_icon" />
</LinearLayout>
<LinearLayout
android:id="@+id/play_black_btn"
android:layout_width="42dp"
android:layout_height="42dp"
android:gravity="center">
<ImageView
android:id="@+id/play_status_img"
android:layout_width="34dp"
android:layout_height="34dp"
android:src="@drawable/play_black_icon" />
</LinearLayout>
</LinearLayout>
</com.lihang.ShadowLayout>
<RelativeLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="72dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/drw_main_bottom_bg"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/home_btn"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center">
<ImageView
android:id="@+id/home_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/home_select_icon" />
</LinearLayout>
<LinearLayout
android:id="@+id/import_btn"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center">
<ImageView
android:id="@+id/import_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/import_unselect_icon" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_bg_color">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@mipmap/main_bg_img" />
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/view"
android:layout_marginTop="-2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:fontFamily="@font/medium_font"
android:gravity="center"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="20dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_marginStart="16dp"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</RelativeLayout>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="MissingDefaultResource">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/medium_font"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="18dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp" />
</LinearLayout>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="320dp"
android:layout_height="wrap_content"
tools:ignore="MissingDefaultResource">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="12dp">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="0dp"
app:cardCornerRadius="4dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/image"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:src="@mipmap/musicoo_logo_img" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical">
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:maxLines="1"
android:fontFamily="@font/regular_font"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="14dp" />
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/desc_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:maxLines="1"
android:fontFamily="@font/regular_font"
android:text="@string/app_name"
android:textColor="@color/white_60"
android:textSize="12dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="MissingDefaultResource">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="12dp"
android:orientation="vertical"
android:paddingTop="12dp">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="0dp"
app:cardCornerRadius="4dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/image"
android:layout_width="148dp"
android:layout_height="148dp"
android:scaleType="centerCrop"
android:src="@mipmap/musicoo_logo_img" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/regular_font"
android:maxLines="2"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="14dp" />
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/desc_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/regular_font"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/white_60"
android:textSize="12dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -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
}