Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ android {

buildConfigField("String", "LASTFM_API_KEY", "\"$lastFmKey\"")
buildConfigField("String", "LASTFM_SECRET", "\"$lastFmSecret\"")

// NDK configuration for vibra_fp library
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
}
}

externalNativeBuild {
cmake {
path("src/main/cpp/vibrafp/lib/CMakeLists.txt")
Expand All @@ -57,13 +57,13 @@ android {
isDefault = true
buildConfigField("Boolean", "CAST_AVAILABLE", "false")
}

// GMS variant - with Google Cast support (requires Google Play Services)
create("gms") {
dimension = "variant"
buildConfigField("Boolean", "CAST_AVAILABLE", "true")
}

create("universal") {
dimension = "abi"
ndk {
Expand Down Expand Up @@ -279,6 +279,9 @@ dependencies {
implementation(libs.jsoup)
ksp(libs.hilt.compiler)

// Shared KMP module (includes database, player, repositories)
implementation(project(":shared"))

implementation(project(":innertube"))
implementation(project(":kugou"))
implementation(project(":lrclib"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import okio.ByteString.Companion.encodeUtf8
import java.io.File
import kotlin.math.roundToInt


@OptIn(ExperimentalCoilApi::class, ExperimentalMaterial3Api::class, DelicateCoilApi::class)
@Composable
fun StorageSettings(
Expand Down
24 changes: 12 additions & 12 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
if (project.findProperty("enableComposeCompilerReports") == "true") {
arrayOf("reports", "metrics").forEach {
freeCompilerArgs.add("-P")
freeCompilerArgs.add("plugin:androidx.compose.compiler.plugins.kotlin:${it}Destination=${project.layout.buildDirectory}/compose_metrics")
}
}
}
}
}
// subprojects {
// tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
// compilerOptions {
// if (project.findProperty("enableComposeCompilerReports") == "true") {
// arrayOf("reports", "metrics").forEach {
// freeCompilerArgs.add("-P")
// freeCompilerArgs.add("plugin:androidx.compose.compiler.plugins.kotlin:${it}Destination=${project.layout.buildDirectory}/compose_metrics")
// }
// }
// }
// }
// }
788 changes: 788 additions & 0 deletions build_log.txt

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ systemProp.org.gradle.internal.http.socketTimeout=180000

# Disable caching for SNAPSHOT dependencies to avoid timeout issues
org.gradle.caching=false

38 changes: 38 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ newpipeextractor = "8885892764428a3f20eaec014dcc37a21cc209f5"
tinypinyin = "2.0.3"
mlkit = "17.0.6"
protobuf = "4.33.5"
koin = "4.1.0"
sqldelight = "2.0.1"
kotlinxCoroutines = "1.10.2"
composeMultiplatform = "1.8.7"
datastoreCore = "1.1.1"

[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
Expand Down Expand Up @@ -116,9 +121,42 @@ tinypinyin = { module = "com.github.promeG:tinypinyin", version.ref = "tinypinyi
mlkit-language-id = { module = "com.google.mlkit:language-id", version.ref = "mlkit" }
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" }
protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.0" }

# Koin Dependency Injection
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }

# SQLDelight
sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" }
sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }
sqldelight-coroutines-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }

# Kotlinx Coroutines
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }

# Kotlinx Serialization
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.8.1" }

# Datastore Core (multiplatform)
androidx-datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastoreCore" }
androidx-datastore-core = { module = "androidx.datastore:datastore-core", version.ref = "datastoreCore" }

# Media3 Common
androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" }
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3" }

[plugins]
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
compose-plugin = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
36 changes: 24 additions & 12 deletions innertube/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
plugins {
alias(libs.plugins.kotlin.serialization)
kotlin("jvm")
kotlin("multiplatform")
}

kotlin {
jvmToolchain(21)
}
jvm()
iosX64()
iosArm64()
iosSimulatorArm64()

sourceSets {
commonMain.dependencies {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.json)
implementation(libs.ktor.client.encoding)
implementation(libs.kotlinx.datetime)
}

jvmMain.dependencies {
implementation(libs.ktor.client.okhttp)
implementation(libs.newpipeextractor)
implementation(libs.brotli)
}

dependencies {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.json)
implementation(libs.ktor.client.encoding)
implementation(libs.brotli)
implementation(libs.newpipeextractor)
testImplementation(libs.junit)
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,24 @@ import com.metrolist.innertube.models.YouTubeClient
import com.metrolist.innertube.models.YouTubeLocale
import com.metrolist.innertube.models.body.*
import com.metrolist.innertube.models.response.NextResponse
import com.metrolist.innertube.utils.HttpClientFactory
import com.metrolist.innertube.utils.PlatformProxy
import com.metrolist.innertube.utils.getDefaultCountryCode
import com.metrolist.innertube.utils.getDefaultLanguageTag
import com.metrolist.innertube.utils.parseCookieString
import com.metrolist.innertube.utils.sha1
import io.ktor.client.*
import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.compression.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.request.*
import io.ktor.client.statement.bodyAsText
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import java.net.Proxy
import java.io.IOException
import kotlinx.coroutines.delay
import java.util.*
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlinx.datetime.Clock
import io.ktor.utils.io.errors.IOException

/**
* Provide access to InnerTube endpoints.
Expand All @@ -38,8 +35,8 @@ class InnerTube {
private var httpClient = createClient()

var locale = YouTubeLocale(
gl = Locale.getDefault().country,
hl = Locale.getDefault().toLanguageTag()
gl = getDefaultCountryCode(),
hl = getDefaultLanguageTag()
)
var visitorData: String? = null
var dataSyncId: String? = null
Expand All @@ -50,102 +47,33 @@ class InnerTube {
}
private var cookieMap = emptyMap<String, String>()

var proxy: Proxy? = null
var proxy: PlatformProxy? = null
set(value) {
field = value
httpClient.close()
httpClient = createClient()
}

var proxyAuth: String? = null

var useLoginForBrowse: Boolean = false

@OptIn(ExperimentalSerializationApi::class)
private fun createClient() = HttpClient(OkHttp) {
expectSuccess = true

install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
explicitNulls = false
encodeDefaults = true
})
}

install(ContentEncoding) {
gzip(0.9F)
deflate(0.8F)
}

// Enhanced network configuration for better performance
engine {
config {
// Connection pool settings for better connection reuse
connectionPool(
okhttp3.ConnectionPool(
10, // maxIdleConnections
5, // keepAliveDuration
java.util.concurrent.TimeUnit.MINUTES
)
)

// Timeout configurations
connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
writeTimeout(60, java.util.concurrent.TimeUnit.SECONDS)

// Enable HTTP/2 for better performance
protocols(listOf(okhttp3.Protocol.HTTP_2, okhttp3.Protocol.HTTP_1_1))

// Retry on connection failure
retryOnConnectionFailure(true)

// Cache configuration for better performance
cache(
okhttp3.Cache(
directory = java.io.File(System.getProperty("java.io.tmpdir"), "http_cache"),
maxSize = 50L * 1024L * 1024L // 50 MB
)
)

// Apply proxy configuration
this@InnerTube.proxy?.let { proxyConfig ->
proxy(proxyConfig)
}

// Apply proxy authentication
this@InnerTube.proxyAuth?.let { auth ->
proxyAuthenticator { _, response ->
response.request.newBuilder()
.header("Proxy-Authorization", auth)
.build()
}
}
}
}

// Request timeout configuration
install(HttpTimeout) {
requestTimeoutMillis = 60000
connectTimeoutMillis = 30000
socketTimeoutMillis = 60000
}

defaultRequest {
url(YouTubeClient.API_URL_YOUTUBE_MUSIC)
// Add common headers for better compatibility
header("Accept", "application/json")
header("Accept-Language", "en-US,en;q=0.9")
header("Cache-Control", "no-cache")
}
}
private fun createClient() = HttpClientFactory.create(
proxy = proxy,
proxyAuth = proxyAuth,
visitorData = visitorData,
dataSyncId = dataSyncId
)

private fun HttpRequestBuilder.ytClient(client: YouTubeClient, setLogin: Boolean = false) {
contentType(ContentType.Application.Json)
headers {
append("X-Goog-Api-Format-Version", "1")
append("X-YouTube-Client-Name", client.clientId /* Not a typo. The Client-Name header does contain the client id. */)
append(
"X-YouTube-Client-Name",
client.clientId /* Not a typo. The Client-Name header does contain the client id. */
)
append("X-YouTube-Client-Version", client.clientVersion)
append("X-Origin", YouTubeClient.ORIGIN_YOUTUBE_MUSIC)
append("Referer", YouTubeClient.REFERER_YOUTUBE_MUSIC)
Expand All @@ -154,7 +82,7 @@ class InnerTube {
cookie?.let { cookie ->
append("cookie", cookie)
if ("SAPISID" !in cookieMap) return@let
val currentTime = System.currentTimeMillis() / 1000
val currentTime = Clock.System.now().toEpochMilliseconds() / 1000
val sapisidHash = sha1("$currentTime ${cookieMap["SAPISID"]} ${YouTubeClient.ORIGIN_YOUTUBE_MUSIC}")
append("Authorization", "SAPISIDHASH ${currentTime}_${sapisidHash}")
}
Expand Down Expand Up @@ -603,7 +531,7 @@ class InnerTube {
)
}
}

suspend fun getUploadCustomThumbnailLink(
client: YouTubeClient,
contentLength: Int
Expand Down
10 changes: 10 additions & 0 deletions innertube/src/commonMain/kotlin/com/metrolist/innertube/NewPipe.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.metrolist.innertube

import com.metrolist.innertube.models.response.PlayerResponse

expect object NewPipeExtractor {
fun init()
fun getSignatureTimestamp(videoId: String): Result<Int>
fun getStreamUrl(format: PlayerResponse.StreamingData.Format, videoId: String): String?
fun newPipePlayer(videoId: String): List<Pair<Int, String>>
}
Loading