diff --git a/README.md b/README.md
index 5d8fb6535..ad1124f23 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@
Make your own music library with any song from YouTube Music.
No ads, free, and simple.
+[](https://f-droid.org/packages/com.zionhuang.music)
[](https://apt.izzysoft.de/fdroid/index/apk/com.zionhuang.music)
[![Latest release](https://img.shields.io/github/v/release/z-huang/InnerTune?include_prereleases)](https://github.com/z-huang/music/releases)
@@ -98,7 +99,13 @@ Use other music scrobbler apps. I recommend [Pano Scrobbler](https://play.google
### Q: How to export downloaded song files?
-*InnerTune* support SAF. You can find the provider in Android native file manager. You can also use [Material Files](https://play.google.com/store/apps/details?id=me.zhanghai.android.files) with [instruction](https://github.com/z-huang/InnerTune/issues/117#issuecomment-1295090708) (recommended).
+*InnerTune* supports SAF. You can find the provider in Android native file manager. You can also use [Material Files](https://play.google.com/store/apps/details?id=me.zhanghai.android.files) with [instruction](https://github.com/z-huang/InnerTune/issues/117#issuecomment-1295090708) (recommended).
+
+### Q: Why InnerTune isn't showing in Android Auto?
+
+1. Go to Android Auto's settings and tap multiple times on the version in the bottom to enable developer settings
+2. In the three dots menu at the top-right of the screen, click "Developer settings"
+3. Enable "Unknown sources"
## Contribution
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9d1ecfb8d..3248af4c7 100755
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -15,8 +15,8 @@ android {
applicationId = "com.zionhuang.music"
minSdk = 24
targetSdk = 32
- versionCode = 14
- versionName = "0.4.3"
+ versionCode = 15
+ versionName = "0.4.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
diff --git a/app/src/main/java/com/zionhuang/music/App.kt b/app/src/main/java/com/zionhuang/music/App.kt
index ba5f31344..dc9985c00 100644
--- a/app/src/main/java/com/zionhuang/music/App.kt
+++ b/app/src/main/java/com/zionhuang/music/App.kt
@@ -1,6 +1,7 @@
package com.zionhuang.music
import android.app.Application
+import android.content.SharedPreferences
import android.os.Build
import android.util.Log
import android.widget.Toast
@@ -12,6 +13,8 @@ import coil.disk.DiskCache
import com.zionhuang.innertube.YouTube
import com.zionhuang.innertube.models.YouTubeLocale
import com.zionhuang.kugou.KuGou
+import com.zionhuang.music.constants.Constants.INNERTUBE_COOKIE
+import com.zionhuang.music.constants.Constants.VISITOR_DATA
import com.zionhuang.music.extensions.getEnum
import com.zionhuang.music.extensions.sharedPreferences
import com.zionhuang.music.extensions.toInetSocketAddress
@@ -22,7 +25,7 @@ import kotlinx.coroutines.launch
import java.net.Proxy
import java.util.*
-class App : Application(), ImageLoaderFactory {
+class App : Application(), ImageLoaderFactory, SharedPreferences.OnSharedPreferenceChangeListener {
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate() {
super.onCreate()
@@ -61,14 +64,20 @@ class App : Application(), ImageLoaderFactory {
}
GlobalScope.launch {
- val visitorData = sharedPreferences.getString(getString(R.string.pref_visitor_data), null) ?: YouTube.generateVisitorData().getOrNull()?.also {
+ YouTube.visitorData = sharedPreferences.getString(VISITOR_DATA, null) ?: YouTube.generateVisitorData().getOrNull()?.also {
sharedPreferences.edit {
- putString(getString(R.string.pref_visitor_data), it)
+ putString(VISITOR_DATA, it)
}
- }
- visitorData?.let {
- YouTube.visitorData = it
- }
+ } ?: YouTube.DEFAULT_VISITOR_DATA
+ }
+ YouTube.cookie = sharedPreferences.getString(INNERTUBE_COOKIE, null)
+ sharedPreferences.registerOnSharedPreferenceChangeListener(this)
+ }
+
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
+ when (key) {
+ VISITOR_DATA -> YouTube.visitorData = sharedPreferences.getString(VISITOR_DATA, null) ?: YouTube.DEFAULT_VISITOR_DATA
+ INNERTUBE_COOKIE -> YouTube.cookie = sharedPreferences.getString(INNERTUBE_COOKIE, null)
}
}
diff --git a/app/src/main/java/com/zionhuang/music/constants/Constants.kt b/app/src/main/java/com/zionhuang/music/constants/Constants.kt
index cb424594a..9bff59d51 100644
--- a/app/src/main/java/com/zionhuang/music/constants/Constants.kt
+++ b/app/src/main/java/com/zionhuang/music/constants/Constants.kt
@@ -20,4 +20,9 @@ object Constants {
const val GITHUB_ISSUE_URL = "https://github.com/z-huang/InnerTune/issues"
const val ERROR_INFO = "error_info"
+
+ const val VISITOR_DATA = "visitor_data"
+ const val INNERTUBE_COOKIE = "innertube_cookie"
+ const val ACCOUNT_NAME = "account_name"
+ const val ACCOUNT_EMAIL = "account_email"
}
\ No newline at end of file
diff --git a/app/src/main/java/com/zionhuang/music/constants/MediaSessionConstants.kt b/app/src/main/java/com/zionhuang/music/constants/MediaSessionConstants.kt
index 509e0e710..e11505ed4 100644
--- a/app/src/main/java/com/zionhuang/music/constants/MediaSessionConstants.kt
+++ b/app/src/main/java/com/zionhuang/music/constants/MediaSessionConstants.kt
@@ -7,6 +7,7 @@ object MediaSessionConstants {
const val ACTION_TOGGLE_LIKE = "action_toggle_like"
const val ACTION_LIKE = "action_like"
const val ACTION_UNLIKE = "action_unlike"
+ const val ACTION_TOGGLE_SHUFFLE = "action_shuffle"
const val COMMAND_SEEK_TO_QUEUE_ITEM = "seek_to_queue_item"
const val COMMAND_PLAY_NEXT = "action_play_next"
const val COMMAND_ADD_TO_QUEUE = "action_add_to_queue"
diff --git a/app/src/main/java/com/zionhuang/music/extensions/ContextExt.kt b/app/src/main/java/com/zionhuang/music/extensions/ContextExt.kt
index 41582c28c..992fc5977 100644
--- a/app/src/main/java/com/zionhuang/music/extensions/ContextExt.kt
+++ b/app/src/main/java/com/zionhuang/music/extensions/ContextExt.kt
@@ -45,6 +45,7 @@ val Context.sharedPreferences: SharedPreferences
fun Context.preference(@StringRes keyId: Int, defaultValue: T) = Preference(this, keyId, defaultValue)
fun Context.preferenceLiveData(@StringRes keyId: Int, defaultValue: T) = PreferenceLiveData(this, keyId, defaultValue)
+fun Context.preferenceLiveData(key: String, defaultValue: T) = PreferenceLiveData(this, key, defaultValue)
fun Context.tryOrReport(block: () -> Unit) = try {
block()
diff --git a/app/src/main/java/com/zionhuang/music/extensions/FragmentExt.kt b/app/src/main/java/com/zionhuang/music/extensions/FragmentExt.kt
index c34f5ff7e..4a9ba0172 100644
--- a/app/src/main/java/com/zionhuang/music/extensions/FragmentExt.kt
+++ b/app/src/main/java/com/zionhuang/music/extensions/FragmentExt.kt
@@ -7,6 +7,8 @@ import androidx.fragment.app.Fragment
fun Fragment.requireAppCompatActivity(): AppCompatActivity = requireActivity() as AppCompatActivity
+val Fragment.sharedPreferences get() = requireContext().sharedPreferences
+
fun DialogFragment.show(context: Context, tag: String? = null) {
context.getActivity()?.let {
show(it.supportFragmentManager, tag)
diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt
index ecee98c03..7d1b0ea39 100644
--- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt
+++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt
@@ -61,6 +61,7 @@ import com.zionhuang.music.constants.MediaSessionConstants.ACTION_LIKE
import com.zionhuang.music.constants.MediaSessionConstants.ACTION_REMOVE_FROM_LIBRARY
import com.zionhuang.music.constants.MediaSessionConstants.ACTION_TOGGLE_LIBRARY
import com.zionhuang.music.constants.MediaSessionConstants.ACTION_TOGGLE_LIKE
+import com.zionhuang.music.constants.MediaSessionConstants.ACTION_TOGGLE_SHUFFLE
import com.zionhuang.music.constants.MediaSessionConstants.ACTION_UNLIKE
import com.zionhuang.music.constants.MediaSessionConstants.COMMAND_ADD_TO_QUEUE
import com.zionhuang.music.constants.MediaSessionConstants.COMMAND_PLAY_NEXT
@@ -275,6 +276,18 @@ class SongPlayer(
if (currentSong != null) R.drawable.ic_library_add_check else R.drawable.ic_library_add
).build()
} else null
+ },
+ object : MediaSessionConnector.CustomActionProvider {
+ override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
+ player.shuffleModeEnabled = !player.shuffleModeEnabled
+ }
+
+ override fun getCustomAction(player: Player) =
+ CustomAction.Builder(
+ ACTION_TOGGLE_SHUFFLE,
+ context.getString(R.string.btn_shuffle),
+ if (player.shuffleModeEnabled) R.drawable.ic_shuffle_on else R.drawable.ic_shuffle
+ ).build()
}
)
setQueueNavigator { player, windowIndex -> player.getMediaItemAt(windowIndex).metadata!!.toMediaDescription(context) }
diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt
index 798d16c41..9f0d268cf 100644
--- a/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt
@@ -130,7 +130,6 @@ class QueueSheetFragment : Fragment() {
mainActivity.queueSheetBehavior.state = STATE_COLLAPSED
}
binding.btnLyrics.setOnClickListener {
- val sharedPreferences = requireContext().sharedPreferences
sharedPreferences.edit {
putBoolean(getString(R.string.pref_show_lyrics), !sharedPreferences.getBoolean(getString(R.string.pref_show_lyrics), false))
}
diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/WebViewFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/WebViewFragment.kt
new file mode 100644
index 000000000..db2279478
--- /dev/null
+++ b/app/src/main/java/com/zionhuang/music/ui/fragments/WebViewFragment.kt
@@ -0,0 +1,86 @@
+package com.zionhuang.music.ui.fragments
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.View
+import android.webkit.CookieManager
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.core.content.edit
+import com.zionhuang.innertube.YouTube
+import com.zionhuang.music.constants.Constants.ACCOUNT_EMAIL
+import com.zionhuang.music.constants.Constants.ACCOUNT_NAME
+import com.zionhuang.music.constants.Constants.INNERTUBE_COOKIE
+import com.zionhuang.music.constants.Constants.VISITOR_DATA
+import com.zionhuang.music.databinding.FragmentWebviewBinding
+import com.zionhuang.music.extensions.sharedPreferences
+import com.zionhuang.music.ui.fragments.base.BindingFragment
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+class WebViewFragment : BindingFragment() {
+ override fun getViewBinding() = FragmentWebviewBinding.inflate(layoutInflater)
+
+ @OptIn(DelicateCoroutinesApi::class)
+ private val webViewClient = object : WebViewClient() {
+ override fun doUpdateVisitedHistory(view: WebView, url: String, isReload: Boolean) {
+ if (url.startsWith("https://music.youtube.com")) {
+ val cookies = CookieManager.getInstance().getCookie(url)
+ if (sharedPreferences.getString(INNERTUBE_COOKIE, null) != cookies) {
+ sharedPreferences.edit {
+ putString(INNERTUBE_COOKIE, cookies)
+ }
+ GlobalScope.launch {
+ YouTube.getAccountInfo().onSuccess {
+ sharedPreferences.edit {
+ putString(ACCOUNT_NAME, it?.name)
+ putString(ACCOUNT_EMAIL, it?.email)
+ }
+ }.onFailure {
+ it.printStackTrace()
+ }
+ }
+ }
+ }
+ }
+
+ override fun onPageFinished(view: WebView, url: String?) {
+ binding.webview.loadUrl("javascript:Android.onRetrieveVisitorData(window.yt.config_.VISITOR_DATA)")
+ }
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.webview.apply {
+ if (savedInstanceState != null) {
+ restoreState(savedInstanceState)
+ } else {
+ loadUrl("https://accounts.google.com/ServiceLogin?ltmpl=music&service=youtube&passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26next%3Dhttps%253A%252F%252Fmusic.youtube.com%252F")
+ }
+ webViewClient = this@WebViewFragment.webViewClient
+ settings.apply {
+ javaScriptEnabled = true
+ setSupportZoom(true)
+ builtInZoomControls = true
+ }
+ addJavascriptInterface(this@WebViewFragment, "Android")
+ }
+ }
+
+ @JavascriptInterface
+ fun onRetrieveVisitorData(visitorData: String?) {
+ if (visitorData != null && sharedPreferences.getString(VISITOR_DATA, null) != visitorData) {
+ sharedPreferences.edit {
+ putString(VISITOR_DATA, visitorData)
+ }
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ binding.webview.saveState(outState)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/base/BaseSettingsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/base/BaseSettingsFragment.kt
index 77606c345..fcd7d8ddf 100644
--- a/app/src/main/java/com/zionhuang/music/ui/fragments/base/BaseSettingsFragment.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/fragments/base/BaseSettingsFragment.kt
@@ -30,7 +30,7 @@ abstract class BaseSettingsFragment : PreferenceFragmentCompat() {
}
is EditTextPreference -> {
val binding = DialogEditTextPreferenceBinding.inflate(layoutInflater)
- binding.editText.setText(requireContext().sharedPreferences.getString(preference.key, ""))
+ binding.editText.setText(sharedPreferences.getString(preference.key, ""))
MaterialAlertDialogBuilder(requireContext())
.setTitle(preference.title)
.setView(binding.root)
diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/ContentSettingsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/ContentSettingsFragment.kt
index c4ced9f54..cb1a65f36 100644
--- a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/ContentSettingsFragment.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/ContentSettingsFragment.kt
@@ -1,25 +1,40 @@
package com.zionhuang.music.ui.fragments.settings
import android.content.Intent
+import android.content.SharedPreferences
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle
import android.view.View
+import androidx.navigation.fragment.findNavController
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
import com.zionhuang.music.R
+import com.zionhuang.music.constants.Constants.ACCOUNT_EMAIL
+import com.zionhuang.music.constants.Constants.ACCOUNT_NAME
import com.zionhuang.music.extensions.preferenceLiveData
+import com.zionhuang.music.extensions.sharedPreferences
import com.zionhuang.music.ui.activities.MainActivity
import com.zionhuang.music.ui.fragments.base.BaseSettingsFragment
import kotlin.system.exitProcess
-class ContentSettingsFragment : BaseSettingsFragment() {
+class ContentSettingsFragment : BaseSettingsFragment(), OnSharedPreferenceChangeListener {
+ private lateinit var accountPreference: Preference
private lateinit var proxyTypePreference: ListPreference
private lateinit var proxyUrlPreference: EditTextPreference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_content)
+ accountPreference = findPreference(getString(R.string.pref_account))!!.apply {
+ title = sharedPreferences?.getString(ACCOUNT_NAME, null) ?: getString(R.string.login)
+ summary = sharedPreferences?.getString(ACCOUNT_EMAIL, null).orEmpty()
+ setOnPreferenceClickListener {
+ findNavController().navigate(R.id.webviewFragment)
+ true
+ }
+ }
val proxyEnabledPreference = findPreference(getString(R.string.pref_proxy_enabled))!!
proxyTypePreference = findPreference(getString(R.string.pref_proxy_type))!!.apply {
isVisible = proxyEnabledPreference.isChecked
@@ -39,5 +54,18 @@ class ContentSettingsFragment : BaseSettingsFragment() {
proxyTypePreference.isVisible = it
proxyUrlPreference.isVisible = it
}
+ sharedPreferences.registerOnSharedPreferenceChangeListener(this)
+ }
+
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
+ when (key) {
+ ACCOUNT_NAME -> accountPreference.title = sharedPreferences.getString(ACCOUNT_NAME, null) ?: getString(R.string.login)
+ ACCOUNT_EMAIL -> accountPreference.summary = sharedPreferences.getString(ACCOUNT_EMAIL, null).orEmpty()
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt
index d6d3ced52..46f8af454 100644
--- a/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt
+++ b/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt
@@ -130,7 +130,7 @@ class YouTubeSuggestionFragment : NavigationFragment(viewGroup, R.layout.item_youtube_header) {
fun bind(header: Header) {
binding.header = header
- binding.root.isClickable = header.moreNavigationEndpoint != null
+ binding.root.isEnabled = header.moreNavigationEndpoint != null
if (header.moreNavigationEndpoint != null) {
binding.root.setOnClickListener {
navigationEndpointHandler.handle(header.moreNavigationEndpoint)
diff --git a/app/src/main/java/com/zionhuang/music/utils/preference/PreferenceLiveData.kt b/app/src/main/java/com/zionhuang/music/utils/preference/PreferenceLiveData.kt
index 36cb294cd..82181a400 100644
--- a/app/src/main/java/com/zionhuang/music/utils/preference/PreferenceLiveData.kt
+++ b/app/src/main/java/com/zionhuang/music/utils/preference/PreferenceLiveData.kt
@@ -10,11 +10,12 @@ import com.zionhuang.music.utils.livedata.SafeLiveData
open class PreferenceLiveData(
context: Context,
- @StringRes private val keyId: Int,
+ val key: String,
private val defValue: T,
) : SafeLiveData(defValue) {
protected val sharedPreferences: SharedPreferences = context.sharedPreferences
- protected val key = context.getString(keyId)
+
+ constructor(context: Context, @StringRes keyId: Int, defValue: T) : this(context, context.getString(keyId), defValue)
protected fun getPreferenceValue() = sharedPreferences.get(key, defValue)
diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml
new file mode 100644
index 000000000..a37adc634
--- /dev/null
+++ b/app/src/main/res/drawable/ic_person.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_shuffle.xml b/app/src/main/res/drawable/ic_shuffle.xml
index 41876a2bd..3e52f67f1 100644
--- a/app/src/main/res/drawable/ic_shuffle.xml
+++ b/app/src/main/res/drawable/ic_shuffle.xml
@@ -5,6 +5,6 @@
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/app/src/main/res/drawable/ic_shuffle_on.xml b/app/src/main/res/drawable/ic_shuffle_on.xml
new file mode 100644
index 000000000..3bb8d2afa
--- /dev/null
+++ b/app/src/main/res/drawable/ic_shuffle_on.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/layout/fragment_webview.xml b/app/src/main/res/layout/fragment_webview.xml
new file mode 100644
index 000000000..9ee060794
--- /dev/null
+++ b/app/src/main/res/layout/fragment_webview.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/settings_navigation_graph.xml b/app/src/main/res/navigation/settings_navigation_graph.xml
index 094ba0973..73336723d 100644
--- a/app/src/main/res/navigation/settings_navigation_graph.xml
+++ b/app/src/main/res/navigation/settings_navigation_graph.xml
@@ -39,4 +39,7 @@
android:id="@+id/aboutFragment"
android:name="com.zionhuang.music.ui.fragments.settings.AboutFragment"
android:label="@string/pref_about_title" />
+
\ No newline at end of file
diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml
index b0e2c246e..4607c22ac 100644
--- a/app/src/main/res/values-es-rUS/strings.xml
+++ b/app/src/main/res/values-es-rUS/strings.xml
@@ -26,6 +26,7 @@
Right
Contenido
+ Login
Idioma por defecto del contenido
País por defecto del contenido
Enable proxy
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 697f7bb53..5bd70707c 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -26,6 +26,7 @@
Right
Contenido
+ Login
Idioma de contenido predeterminado
País de contenido predeterminado
Enable proxy
diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml
index 91e20af97..ea9fc90f3 100644
--- a/app/src/main/res/values-fa-rIR/strings.xml
+++ b/app/src/main/res/values-fa-rIR/strings.xml
@@ -26,6 +26,7 @@
Right
محتوا
+ Login
زبان پیشفرض محتوا
کشور پیشفرض محتوا
فعالکردن پروکسی
diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml
index 69eea1e76..e0d72a89e 100644
--- a/app/src/main/res/values-fi-rFI/strings.xml
+++ b/app/src/main/res/values-fi-rFI/strings.xml
@@ -26,6 +26,7 @@
Right
Sisältö
+ Login
Sisällön oletuskieli
Sisällön oletusmaa
Enable proxy
diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml
index 2b4237d39..22710c1f9 100644
--- a/app/src/main/res/values-fr-rFR/strings.xml
+++ b/app/src/main/res/values-fr-rFR/strings.xml
@@ -26,6 +26,7 @@
Right
Content
+ Login
Langue du contenu par défaut
Pays du contenu par défaut
Enable proxy
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 13aa6d506..86d4be3f3 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -26,6 +26,7 @@
Jobbra
Tartalom
+ Bejelentkezés
A tartalom alapért. nyelve
A tartalom alapért.t országa
Proxy bekapcsolása
@@ -121,7 +122,7 @@
Újrahív
Megosztás
Törlés
- Search online
+ Keresés online
Válasszon más dalszövegeket
@@ -137,7 +138,7 @@
Ismeretlen
Dalszöveg szerkesztése
- Search lyrics
+ Dalszöveg keresése
Válassza ki a dalszövegeket
Dal szerkesztése
@@ -204,7 +205,7 @@
Jelentés GitHub-on
- Dátum hozzáadva
+ Létrehozás dátuma
Név
Előadó
Év
diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml
new file mode 100644
index 000000000..7b3d4937d
--- /dev/null
+++ b/app/src/main/res/values-id/strings.xml
@@ -0,0 +1,306 @@
+
+
+ Beranda
+ Lagu
+ Artis
+ Album
+ Daftar putar
+ Jelajah
+ Pengaturan
+ Sedang diputar
+ Laporkan kesalahan
+
+
+ Tampilan
+ Ikuti tema sistem
+ Warna tema
+ Tema gelap
+ Aktif
+ Tidak aktif
+ Ikuti sistem
+ Tab buka bawaan
+ Sesuaikan tab navigasi
+ Posisi teks lirik
+ Kiri
+ Tengah
+ Kanan
+
+ Konten
+ Masuk
+ Bahasa konten bawaan
+ Negara konten bawaan
+ Aktifkan proxy
+ Tipe proxy
+ URL proxy
+ Mulai ulang agar berlaku
+
+ Pemutar dan audio
+ Kualitas audio
+ Otomatis
+ Tinggi
+ Rendah
+ Dalam antrian
+ Lewati keheningan
+ Normalisasi audio
+ Ekualiser
+
+ Penyimpanan
+ Lihat file yang diunduh di SAF
+ Ini mungkin tidak berfungsi di beberapa perangkat
+ Data sementara (cache)
+ Ukuran gambar data sementara (cache) maksimum
+ Bersihkan gambar data sementara (cache)
+ Ukuran lagu data sementara (cache) maksimum
+ %s digunakan
+
+ Umum
+ Unduh secara otomatis
+ Unduh lagu saat ditambahkan ke perpustakaan
+ Tambahkan lagu secara otomatis ke perpustakaan
+ Tambahkan lagu ke perpustakaan Anda saat selesai diputar
+ Perluas pemutar bawah saat diputar
+ Lebih banyak tindakan dalam pemberitahuan
+ Tampilkan tombol tambahkan ke perpustakaan dan suka
+
+ Privasi
+ Jeda riwayat pencarian
+ Bersihkan riwayat pencarian
+ Apakah anda yakin untuk menghapus semua riwayat penelusuran?
+ Aktifkan penyedia lirik KuGou
+
+ Cadangkan dan pulihkan
+ Cadangkan
+ Pulihkan
+
+ Tentang
+ Versi aplikasi
+
+
+ Sakura
+ Merah
+ Merah muda
+ Ungu
+ Ungu gelap
+ Nila
+ Biru
+ Biru terang
+ Biru Kehijau-hijauan
+ Hijau laut
+ Hijau
+ Hijau terang
+ Hijau kekuningan
+ Kuning
+ Kuning lulur
+ Jingga
+ Jingga gelap
+ Cokelat
+ Biru keabu-abuan
+
+
+ Caru
+ Cari di YouTube Music…
+ Cari di perpustakaan…
+
+
+ Lagu yang disukai
+ Lagu yang diunduh
+
+
+ Rincian
+ Sunting
+ Putar radio
+ Putar
+ Putar selanjutnya
+ Tambahkan ke antrian
+ Tambahkan ke perpustakaan
+ Unduh
+ Hapus unduhan
+ Pindahkan ke daftar putar
+ Tambahkan ke daftar putar
+ Lihat artis
+ Lihat album
+ Ambil kembali
+ Bagikan
+ Hapus
+ Cari secara online
+ Pilih lirik lain
+
+
+ Rincian
+ ID media
+ Tipe MIME
+ Codecs
+ Kecepatan bit
+ Tingkat sampel
+ Kekerasan
+ Volume
+ Ukuran berkas
+ Tidak diketahui
+
+ Sunting lirik
+ Cari lirik
+ Pilih lirik
+
+ Sunting lagu
+ Judul lagu
+ Artis lagu
+ Judul lagu wajib diisi.
+ Artis lagu wajib diisi.
+ Simpan
+
+ Buat daftar putar
+ Nama daftar putar
+ Nama daftar putar wajib diisi.
+
+ Sunting artis
+ Nama artis
+ Nama artis wajib diisi.
+
+ Artis duplikat
+ Artis %1$s sudah ada.
+
+ Pilih daftar putar
+
+ Sunting daftar putar
+
+ Pilih cadangan konten
+ Pilih pulihkan konten
+ Preferensi
+ Basis Data
+ Lagu yang telah diunduh
+ Cadangan berhasil dibuat
+ Tidak dapat membuat cadangan
+ Gagal untuk memulihkan cadangan
+
+
+ Pemutar Musik
+ Unduh
+
+
+
+ - %d lagu
+ - %d lagu
+
+
+ - %d artis
+ - %d artis
+
+
+ - %d album
+ - %d album
+
+
+ - %d daftar putar
+ - %d daftar putar
+
+
+
+ Mencoba kembali
+ Putar
+ Putar semua
+ Radio
+ Acak
+ Salin stacktrace
+ Laporkan
+ Laporkan di GitHub
+
+
+ Tanggal ditambahkan
+ Nama
+ Artis
+ Tahun
+ Hitungan lagu
+ Ukuran
+ Waktu dimainkan
+
+
+
+ - %d lagu telah dihapus.
+ - %d lagu telah dihapus.
+
+
+ - %d dipilih
+
+ Kembali
+ Tidak dapat mengidentifikasi url ini.
+
+ - %d lagu akan diputar selanjutnya
+ - %d lagu akan diputar selanjutnya
+
+
+ - %d artis akan diputar selanjutnya
+ - %d artis akan diputar selanjutnya
+
+
+ - %d album akan diputar selanjutnya
+ - %d album akan diputar selanjutnya
+
+
+ - %d daftar putar akan diputar selanjutnya
+ - %d daftar putar akan diputar selanjutnya
+
+ Dipilih akan diputar selanjutnya
+
+ - %d lagu ditambahkan ke antrian
+ - %d lagu ditambahkan ke antrian
+
+
+ - %d artis ditambahkan ke antrian
+ - %d artis ditambahkan ke antrian
+
+
+ - %d album ditambahkan ke antrian
+ - %d album ditambahkan ke antrian
+
+
+ - %d daftar putar ditambahkan ke antrian
+ - %d daftar putar ditambahkan ke antrian
+
+ Dipilih ditambahkan ke antrean
+ Ditambahkan ke perpustakaan
+ Dihapus dari perpustakaan
+ Daftar putar dipindahkan
+ Ditambahkan ke %1$s
+
+ - Mulai mengunduh %d lagu
+ - Mulai mengunduh %d lagu
+
+ Unduhan dihapus
+ Lihat
+
+
+ Suka
+ Hapus suka
+ Tambahkan ke perpustakaan
+ Hapus dari perpustakaan
+
+
+ Semua
+ Lagu
+ Video
+ Album
+ Artis
+ Daftar putar
+ Daftar putar komunitas
+ Daftar putar unggulan
+
+ System default
+ Dari perpustakaan anda
+
+
+ Maaf, itu seharusnya tidak terjadi.
+ Disalin ke papan klip
+
+
+ Tidak ada stream yang tersedia
+ Tidak ada koneksi jaringan
+ Waktu habis
+ Kesalahan yang tidak diketahui
+
+
+ Semua lagu
+ Lagu yang telah dicari
+
+
+ Lirik tidak ditemukan
+
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 2f39585ec..be758369c 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -26,6 +26,7 @@
Destra
Contenuti
+ Login
Lingua predefinita dei contenuti
Paese predefinito dei contenuti
Attiva proxy
diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml
index 9076f9f92..6999ef7d5 100644
--- a/app/src/main/res/values-ja-rJP/strings.xml
+++ b/app/src/main/res/values-ja-rJP/strings.xml
@@ -19,13 +19,14 @@
オフ
システムに従う
起動時に開くタブ
- Customize navigation tabs
- Lyrics text position
- Left
- Center
- Right
+ ナビゲーションタブのカスタマイズ
+ 歌詞テキストの位置
+ 左
+ 中央
+ 右
コンテンツ
+ ログイン
デフォルトのコンテンツの言語
デフォルトのコンテンツの国
プロキシを有効化
@@ -38,34 +39,34 @@
自動
高
低
- Persistent queue
- Skip silence
- Audio normalization
+ 再生キューを保持
+ 無音部分をスキップ
+ オーディオノーマライゼーション
イコライザー
- Storage
- View downloaded files in SAF
- This may not work in some devices
- Cache
- Max image cache size
- Clear image cache
- Max song cache size
- %s used
+ ストレージ
+ SAFにダウンロードされたファイルを表示
+ 一部の端末では動作しない場合があります
+ キャッシュ
+ 画像の最大キャッシュサイズ
+ 画像のキャッシュを削除
+ 曲の最大キャッシュサイズ
+ %s 使用中
一般
自動ダウンロード
ライブラリに追加したら曲をダウンロードする
曲を自動でライブラリに追加
再生が終了したら曲をライブラリに追加する
- 再生時にボトムプレイヤーを展開する
- More actions in notification
- Show add to library and like buttons
+ プレイヤーを再生時に展開
+ 通知にもっとアクションを表示
+ 「ライブラリに追加」と「いいね」ボタンを表示する
プライバシー
履歴の記録を一時停止
履歴を削除
すべての検索履歴を削除しますか?
- Enable KuGou lyrics provider
+ 酷狗からの歌詞取得を有効化
バックアップとリストア
バックアップ
@@ -75,24 +76,24 @@
アプリのバージョン
- 桜色
- 赤
+ サクラ
+ レッド
ピンク
- 紫
- 深紫
- 藍
- 青
+ パープル
+ ディープパープル
+ インディゴ
+ ブルー
ライトブルー
シアン
- 青緑
- 緑
+ ティール
+ グリーン
ライトグリーン
- 黄緑
- 黄
+ ライム
+ イエロー
アンバー
オレンジ
- 深いオレンジ
- 茶
+ ディープオレンジ
+ ブラウン
ブルーグレー
@@ -101,8 +102,8 @@
ライブラリを検索…
- Liked songs
- Downloaded songs
+ いいねした曲
+ ダウンロードした曲
Details
@@ -121,24 +122,24 @@
再取得
共有
削除
- Search online
- Choose other lyrics
+ オンラインで検索
+ ほかの歌詞を選択
- Details
- Media id
- MIME type
- Codecs
- Bitrate
- Sample rate
- Loudness
- Volume
- File size
- Unknown
-
- Edit lyrics
- Search lyrics
- Choose lyrics
+ 詳細
+ メディアID
+ MIMEタイプ
+ コーデック
+ ビットレート
+ サンプリングレート
+ ラウドネス
+ ボリューム
+ ファイルサイズ
+ 不明
+
+ 歌詞を編集
+ 歌詞を検索
+ 歌詞を選択
曲を編集
曲名
@@ -263,10 +264,10 @@
見る
- Like
- Remove like
+ いいね
+ いいねを削除
ライブラリに追加
- Remove from library
+ ライブラリから削除
すべて
@@ -286,15 +287,15 @@
クリップボードにコピー
- No stream available
- No network connection
- Timeout
- Unknown error
+ ストリームが利用できません
+ ネットワーク接続がありません
+ タイムアウトしました
+ 不明なエラーです
- All songs
- Searched songs
+ すべての曲
+ 検索した曲
- Lyrics not found
+ 歌詞が見つかりません
diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml
index 0eb0a529b..1edd31e0b 100644
--- a/app/src/main/res/values-ko-rKR/strings.xml
+++ b/app/src/main/res/values-ko-rKR/strings.xml
@@ -26,6 +26,7 @@
Right
콘텐츠
+ Login
기본 콘텐츠 언어
기본 콘텐츠 국가
Enable proxy
diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml
index f8edcba75..2ec038156 100644
--- a/app/src/main/res/values-ml-rIN/strings.xml
+++ b/app/src/main/res/values-ml-rIN/strings.xml
@@ -26,6 +26,7 @@
Right
കന്റെന്റ്
+ Login
സ്ഥിര കന്റെന്റ് ഭാഷ
സ്ഥിര കന്റെന്റ് രാജ്യം
പ്രോക്സി പ്രവർത്തനക്ഷമമാക്കുക
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 3751e9caf..f50e19d45 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -26,6 +26,7 @@
Right
Conteúdo
+ Login
Idioma padrão do conteúdo
País padrão do conteúdo
Ativar proxy
diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml
index 7b29b8866..4ddea00a4 100644
--- a/app/src/main/res/values-sv-rSE/strings.xml
+++ b/app/src/main/res/values-sv-rSE/strings.xml
@@ -26,6 +26,7 @@
Right
Innehåll
+ Login
Standard innehållsspråk
Standard innehållsland
Enable proxy
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 1255a10c7..12d9ecd9c 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -26,6 +26,7 @@
靠右
内容
+ 登录
默认内容语言
默认内容国家
启用代理
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 14d1c21e3..9c3e0e31b 100755
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -26,6 +26,7 @@
靠右
內容
+ 登入
預設內容語言
預設內容國家
啟用 Proxy
diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml
index 5a3c64880..c4308ef0e 100644
--- a/app/src/main/res/values/constants.xml
+++ b/app/src/main/res/values/constants.xml
@@ -25,9 +25,9 @@
NAV_TAB_CONFIG
LRC_TEXT_POS
+ ACCOUNT
CONTENT_LANGUAGE
CONTENT_COUNTRY
- VISITOR_DATA
PROXY_ENABLED
PROXY_TYPE
PROXY_URL
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1864dbd94..a2af0e499 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -26,6 +26,7 @@
Right
Content
+ Login
Default content language
Default content country
Enable proxy
diff --git a/app/src/main/res/xml/pref_content.xml b/app/src/main/res/xml/pref_content.xml
index 62729a536..4b362a93c 100644
--- a/app/src/main/res/xml/pref_content.xml
+++ b/app/src/main/res/xml/pref_content.xml
@@ -1,6 +1,12 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
Musicの目的です。
+このアプリを使ったら、無料の音楽ストリーミングサービスを手に入れたようなものです。YouTube Musicの音楽を聴いたり、自分だけのライブラリを作り上げたりできます。さらには、音楽をダウンロードし、オフラインで再生することもできます。また、プレイリストを作って、曲を整理することも可能です。簡単で、実用的で、広告のないアプリで、誰もが無料で音楽を聴くことができるようにすることが、InnerTuneの目的です。
注意:
diff --git a/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt b/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt
index 5e6a51fb7..b4519d638 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt
@@ -4,6 +4,8 @@ import com.zionhuang.innertube.encoder.brotli
import com.zionhuang.innertube.models.YouTubeClient
import com.zionhuang.innertube.models.YouTubeLocale
import com.zionhuang.innertube.models.body.*
+import com.zionhuang.innertube.utils.parseCookieString
+import com.zionhuang.innertube.utils.sha1
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
@@ -22,21 +24,26 @@ import java.util.*
* For making HTTP requests, not parsing response.
*/
class InnerTube {
- var httpClient = createClient()
+ private var httpClient = createClient()
var locale = YouTubeLocale(
gl = Locale.getDefault().country,
hl = Locale.getDefault().toLanguageTag()
)
- private var _proxy: Proxy? = null
- var proxy: Proxy?
- get() = _proxy
+ var visitorData: String = "CgtsZG1ySnZiQWtSbyiMjuGSBg%3D%3D"
+ var cookie: String? = null
set(value) {
- _proxy = value
+ field = value
+ cookieMap = if (value == null) emptyMap() else parseCookieString(value)
+ }
+ private var cookieMap = emptyMap()
+
+ var proxy: Proxy? = null
+ set(value) {
+ field = value
httpClient.close()
httpClient = createClient()
}
- var visitorData: String = "CgtsZG1ySnZiQWtSbyiMjuGSBg%3D%3D"
@OptIn(ExperimentalSerializationApi::class)
private fun createClient() = HttpClient(OkHttp) {
@@ -56,9 +63,9 @@ class InnerTube {
deflate(0.8F)
}
- if (_proxy != null) {
+ if (proxy != null) {
engine {
- this.proxy = _proxy
+ proxy = this@InnerTube.proxy
}
}
@@ -73,9 +80,17 @@ class InnerTube {
append("X-Goog-Api-Format-Version", "1")
append("X-YouTube-Client-Name", client.clientName)
append("X-YouTube-Client-Version", client.clientVersion)
+ append("x-origin", "https://music.youtube.com")
if (client.referer != null) {
append("Referer", client.referer)
}
+ cookie?.let { cookie ->
+ append("cookie", cookie)
+ if ("SAPISID" !in cookieMap) return@let
+ val currentTime = System.currentTimeMillis() / 1000
+ val sapisidHash = sha1("$currentTime ${cookieMap["SAPISID"]} https://music.youtube.com")
+ append("Authorization", "SAPISIDHASH ${currentTime}_${sapisidHash}")
+ }
}
userAgent(client.userAgent)
parameter("key", client.api_key)
@@ -174,4 +189,11 @@ class InnerTube {
playlistId = playlistId
))
}
+
+ suspend fun getSwJsData() = httpClient.get("https://music.youtube.com/sw.js_data")
+
+ suspend fun accountMenu(client: YouTubeClient) = httpClient.post("account/account_menu") {
+ configYTClient(client)
+ setBody(AccountMenuBody(client.toContext(locale, visitorData)))
+ }
}
\ No newline at end of file
diff --git a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt
index 3549e2645..2b739f22a 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt
@@ -6,7 +6,6 @@ import com.zionhuang.innertube.models.YouTubeClient.Companion.WEB_REMIX
import com.zionhuang.innertube.models.response.*
import com.zionhuang.innertube.utils.insertSeparator
import io.ktor.client.call.*
-import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.serialization.json.Json
@@ -31,6 +30,11 @@ object YouTube {
set(value) {
innerTube.visitorData = value
}
+ var cookie: String?
+ get() = innerTube.cookie
+ set(value) {
+ innerTube.cookie = value
+ }
var proxy: Proxy?
get() = innerTube.proxy
set(value) {
@@ -155,23 +159,27 @@ object YouTube {
.mapNotNull { it.content.playlistPanelVideoRenderer?.toSongItem() }
}
- suspend fun generateVisitorData() = runCatching {
- Json.parseToJsonElement(innerTube.httpClient.get("https://music.youtube.com/sw.js_data").bodyAsText().substring(5))
+ suspend fun generateVisitorData(): Result = runCatching {
+ Json.parseToJsonElement(innerTube.getSwJsData().bodyAsText().substring(5))
.jsonArray[0]
.jsonArray[2]
.jsonArray[6]
.jsonPrimitive.content
}
+ suspend fun getAccountInfo(): Result = runCatching {
+ innerTube.accountMenu(WEB_REMIX).body().actions[0].openPopupAction.popup.multiPageMenuRenderer.header?.activeAccountHeaderRenderer?.toAccountInfo()
+ }
+
@JvmInline
value class SearchFilter(val value: String) {
companion object {
- val FILTER_SONG = SearchFilter("EgWKAQIIAWoMEAMQDhAEEAkQChAF")
- val FILTER_VIDEO = SearchFilter("EgWKAQIQAWoMEAMQDhAEEAkQChAF")
- val FILTER_ALBUM = SearchFilter("EgWKAQIYAWoMEAMQDhAEEAkQChAF")
- val FILTER_ARTIST = SearchFilter("EgWKAQIgAWoMEAMQDhAEEAkQChAF")
- val FILTER_FEATURED_PLAYLIST = SearchFilter("EgeKAQQoADgBagwQAxAOEAQQCRAKEAU%3D")
- val FILTER_COMMUNITY_PLAYLIST = SearchFilter("EgeKAQQoAEABagwQAxAOEAQQCRAKEAU%3D")
+ val FILTER_SONG = SearchFilter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D")
+ val FILTER_VIDEO = SearchFilter("EgWKAQIQAWoKEAkQChAFEAMQBA%3D%3D")
+ val FILTER_ALBUM = SearchFilter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D")
+ val FILTER_ARTIST = SearchFilter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D")
+ val FILTER_FEATURED_PLAYLIST = SearchFilter("EgeKAQQoADgBagwQDhAKEAMQBRAJEAQ%3D")
+ val FILTER_COMMUNITY_PLAYLIST = SearchFilter("EgeKAQQoAEABagoQAxAEEAoQCRAF")
}
}
@@ -179,4 +187,6 @@ object YouTube {
const val EXPLORE_BROWSE_ID = "FEmusic_explore"
const val MAX_GET_QUEUE_SIZE = 1000
+
+ const val DEFAULT_VISITOR_DATA = "CgtsZG1ySnZiQWtSbyiMjuGSBg%3D%3D"
}
\ No newline at end of file
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/AccountInfo.kt b/innertube/src/main/java/com/zionhuang/innertube/models/AccountInfo.kt
new file mode 100644
index 000000000..cacedbfed
--- /dev/null
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/AccountInfo.kt
@@ -0,0 +1,6 @@
+package com.zionhuang.innertube.models
+
+data class AccountInfo(
+ val name: String,
+ val email: String,
+)
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/Continuation.kt b/innertube/src/main/java/com/zionhuang/innertube/models/Continuation.kt
index 2e34a03f4..a079a30f7 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/models/Continuation.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/Continuation.kt
@@ -9,7 +9,7 @@ import kotlinx.serialization.json.JsonNames
@Serializable
data class Continuation(
@JsonNames("nextContinuationData", "nextRadioContinuationData")
- val nextContinuationData: NextContinuationData,
+ val nextContinuationData: NextContinuationData?,
) {
@Serializable
data class NextContinuationData(
@@ -18,9 +18,9 @@ data class Continuation(
}
fun List.getContinuation() =
- get(0).nextContinuationData.continuation
+ get(0).nextContinuationData?.continuation
fun List.getContinuations() =
- map { it.nextContinuationData.continuation }
+ mapNotNull { it.nextContinuationData?.continuation }
.ifEmpty { null }
\ No newline at end of file
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/MusicResponsiveListItemRenderer.kt b/innertube/src/main/java/com/zionhuang/innertube/models/MusicResponsiveListItemRenderer.kt
index 6dfaebd49..7fa0d1aa8 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/models/MusicResponsiveListItemRenderer.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/MusicResponsiveListItemRenderer.kt
@@ -27,8 +27,8 @@ data class MusicResponsiveListItemRenderer(
) {
fun getTitle() = flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.toString()
fun getSubtitle() = (flexColumns.drop(1) + fixedColumns.orEmpty())
- .filter {
- it.musicResponsiveListItemFlexColumnRenderer.text.runs.isNotEmpty()
+ .filterNot {
+ it.musicResponsiveListItemFlexColumnRenderer.text?.runs.isNullOrEmpty()
}.joinToString(separator = " • ") {
it.musicResponsiveListItemFlexColumnRenderer.text.toString()
}
@@ -60,7 +60,7 @@ data class MusicResponsiveListItemRenderer(
) {
@Serializable
data class MusicResponsiveListItemFlexColumnRenderer(
- val text: Runs,
+ val text: Runs?,
)
}
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt b/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt
index 98a627c92..3eb74a843 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt
@@ -10,7 +10,7 @@ import kotlinx.serialization.json.JsonNames
@Serializable
data class SectionListRenderer(
val header: Header?,
- val contents: List,
+ val contents: List?,
val continuations: List?,
) {
@Serializable
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/YTItem.kt b/innertube/src/main/java/com/zionhuang/innertube/models/YTItem.kt
index 264d6264e..9f085fb25 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/models/YTItem.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/YTItem.kt
@@ -53,35 +53,35 @@ data class SongItem(
val menu = item.menu.toItemMenu()
return SongItem(
id = item.playlistItemData?.videoId
- ?: item.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint?.watchEndpoint?.videoId
+ ?: item.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text?.runs?.firstOrNull()?.navigationEndpoint?.watchEndpoint?.videoId
?: menu.radioEndpoint?.watchEndpoint?.videoId
?: return null,
title = item.getTitle(),
subtitle = item.getSubtitle(),
index = item.index?.toString(),
- artists = item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs
- .filter { it.navigationEndpoint?.getEndpointType() == ITEM_ARTIST }
- .ifEmpty {
+ artists = item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text?.runs
+ ?.filter { it.navigationEndpoint?.getEndpointType() == ITEM_ARTIST }
+ ?.ifEmpty {
listOfNotNull(
if (item.fixedColumns != null) {
// Table style
- item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs.getOrNull(0)
+ item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text?.runs?.getOrNull(0)
} else {
// From search
- item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs.let {
+ item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text?.runs?.let {
it.getOrNull(it.lastIndex - 4) ?: it.getOrNull(it.lastIndex - 2)
}
}
)
- },
- album = item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs
- .find { it.navigationEndpoint?.getEndpointType() == ITEM_ALBUM }
+ }.orEmpty(),
+ album = item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text?.runs
+ ?.find { it.navigationEndpoint?.getEndpointType() == ITEM_ALBUM }
?.toLink(),
- duration = item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs.lastOrNull()?.text?.let { TimeParser.parse(it) }
+ duration = item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text?.runs?.lastOrNull()?.text?.let { TimeParser.parse(it) }
?: item.fixedColumns?.firstOrNull()?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.text?.let { TimeParser.parse(it) },
thumbnails = item.thumbnail?.getThumbnails().orEmpty(),
menu = menu,
- navigationEndpoint = item.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint!!
+ navigationEndpoint = item.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text?.runs?.firstOrNull()?.navigationEndpoint!!
)
}
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/body/AccountMenuBody.kt b/innertube/src/main/java/com/zionhuang/innertube/models/body/AccountMenuBody.kt
new file mode 100644
index 000000000..d92b60200
--- /dev/null
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/body/AccountMenuBody.kt
@@ -0,0 +1,11 @@
+package com.zionhuang.innertube.models.body
+
+import com.zionhuang.innertube.models.Context
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class AccountMenuBody(
+ val context: Context,
+ val deviceTheme: String = "DEVICE_THEME_SELECTED",
+ val userInterfaceTheme: String = "USER_INTERFACE_THEME_DARK",
+)
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/response/AccountMenuResponse.kt b/innertube/src/main/java/com/zionhuang/innertube/models/response/AccountMenuResponse.kt
new file mode 100644
index 000000000..7c1118771
--- /dev/null
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/response/AccountMenuResponse.kt
@@ -0,0 +1,46 @@
+package com.zionhuang.innertube.models.response
+
+import com.zionhuang.innertube.models.AccountInfo
+import com.zionhuang.innertube.models.Runs
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class AccountMenuResponse(
+ val actions: List,
+) {
+ @Serializable
+ data class Action(
+ val openPopupAction: OpenPopupAction,
+ ) {
+ @Serializable
+ data class OpenPopupAction(
+ val popup: Popup,
+ ) {
+ @Serializable
+ data class Popup(
+ val multiPageMenuRenderer: MultiPageMenuRenderer,
+ ) {
+ @Serializable
+ data class MultiPageMenuRenderer(
+ val header: Header?,
+ ) {
+ @Serializable
+ data class Header(
+ val activeAccountHeaderRenderer: ActiveAccountHeaderRenderer,
+ ) {
+ @Serializable
+ data class ActiveAccountHeaderRenderer(
+ val accountName: Runs,
+ val email: Runs,
+ ) {
+ fun toAccountInfo() = AccountInfo(
+ accountName.toString(),
+ email.toString()
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/response/PlayerResponse.kt b/innertube/src/main/java/com/zionhuang/innertube/models/response/PlayerResponse.kt
index a98f0291f..678e62906 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/models/response/PlayerResponse.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/models/response/PlayerResponse.kt
@@ -34,7 +34,7 @@ data class PlayerResponse(
@Serializable
data class StreamingData(
- val formats: List,
+ val formats: List?,
val adaptiveFormats: List,
val expiresInSeconds: Int,
) {
diff --git a/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt b/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt
index df7bbc0b4..a6da8b408 100644
--- a/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt
+++ b/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt
@@ -3,6 +3,18 @@ package com.zionhuang.innertube.utils
import java.io.UnsupportedEncodingException
import java.net.URL
import java.net.URLDecoder
+import java.security.MessageDigest
+
+fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
+
+fun sha1(str: String): String = MessageDigest.getInstance("SHA-1").digest(str.toByteArray()).toHex()
+
+fun parseCookieString(cookie: String): Map =
+ cookie.split("; ").associate {
+ val (key, value) = it.split("=")
+ key to value
+ }
+
fun isHTTP(url: URL): Boolean {
// Make sure it's HTTP or HTTPS
diff --git a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt
index 6a5268875..1127eaf3c 100644
--- a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt
+++ b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt
@@ -131,7 +131,7 @@ object KuGou {
private fun generateKeyword(title: String, artist: String) = normalizeTitle(title) to normalizeArtist(artist)
- private fun String.normalize(keyword: Pair): String = lines().filter { line ->
+ private fun String.normalize(keyword: Pair): String = replace("'", "'").lines().filter { line ->
line matches ACCEPTED_REGEX
}.let {
// Remove useless information such as singer, writer, composer, guitar, etc.