Skip to content

Commit

Permalink
download fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
crackededed committed Jun 26, 2024
1 parent 7365f0c commit f671fdc
Show file tree
Hide file tree
Showing 58 changed files with 1,410 additions and 1,405 deletions.
4 changes: 1 addition & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ android {
minSdk = 16
targetSdk = 34
versionCode = 121
versionName = "2.32.4"
versionName = "2.32.5"
}

buildTypes {
Expand Down Expand Up @@ -116,9 +116,7 @@ dependencies {
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.gson)

implementation(libs.okio)
implementation(libs.open.m3u8)

implementation(libs.media3.exoplayer)
implementation(libs.media3.exoplayer.hls)
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
android:configChanges="orientation|screenSize"
android:windowSoftInputMode="adjustResize"/>

<service android:name=".ui.download.DownloadService" />
<service
android:name=".ui.player.PlaybackService"
android:foregroundServiceType="mediaPlayback"
Expand All @@ -60,6 +59,7 @@
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>

<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
Expand Down Expand Up @@ -106,4 +106,4 @@
<data android:scheme="https" />
</intent>
</queries>
</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package com.github.andreyasadchy.xtra.model

import android.os.Parcelable
import com.github.andreyasadchy.xtra.model.ui.Video
import kotlinx.parcelize.Parcelize

@Parcelize
data class VideoDownloadInfo(
val video: Video,
val qualities: Map<String, String>,
val qualities: Map<String, String>? = null,
val totalDuration: Long,
val currentPosition: Long) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class UserResultIDDeserializer : JsonDeserializer<UserResultIDQueryResponse> {
item.takeIf { it.isJsonObject }?.asJsonObject?.get("message")?.takeIf { it.isJsonPrimitive }?.asJsonPrimitive?.takeIf { it.isString }?.asString?.let { if (it == "failed integrity check") throw Exception(it) }
}
val dataJson = json.takeIf { it.isJsonObject }?.asJsonObject?.get("data")?.takeIf { it.isJsonObject }?.asJsonObject?.get("userResultByID")?.takeIf { it.isJsonObject }?.asJsonObject
val data = androidx.core.util.Pair(if (dataJson?.get("id")?.takeIf { it.isJsonPrimitive }?.asJsonPrimitive?.isString == true) null else "userResultByID", dataJson?.get("reason")?.takeIf { it.isJsonPrimitive }?.asJsonPrimitive?.takeIf { it.isString }?.asString)
val data = Pair(if (dataJson?.get("id")?.takeIf { it.isJsonPrimitive }?.asJsonPrimitive?.isString == true) null else "userResultByID", dataJson?.get("reason")?.takeIf { it.isJsonPrimitive }?.asJsonPrimitive?.takeIf { it.isString }?.asString)
return UserResultIDQueryResponse(data)
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package com.github.andreyasadchy.xtra.model.query

data class UserResultIDQueryResponse(val data: androidx.core.util.Pair<String?, String?>?)
data class UserResultIDQueryResponse(val data: Pair<String?, String?>?)
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class UserResultLoginDeserializer : JsonDeserializer<UserResultLoginQueryRespons
item.takeIf { it.isJsonObject }?.asJsonObject?.get("message")?.takeIf { it.isJsonPrimitive }?.asJsonPrimitive?.takeIf { it.isString }?.asString?.let { if (it == "failed integrity check") throw Exception(it) }
}
val dataJson = json.takeIf { it.isJsonObject }?.asJsonObject?.get("data")?.takeIf { it.isJsonObject }?.asJsonObject?.get("userResultByLogin")?.takeIf { it.isJsonObject }?.asJsonObject
val data = androidx.core.util.Pair(if (dataJson?.get("id")?.takeIf { it.isJsonPrimitive }?.asJsonPrimitive?.isString == true) null else "userResultByLogin", dataJson?.get("reason")?.takeIf { it.isJsonPrimitive }?.asJsonPrimitive?.takeIf { it.isString }?.asString)
val data = Pair(if (dataJson?.get("id")?.takeIf { it.isJsonPrimitive }?.asJsonPrimitive?.isString == true) null else "userResultByLogin", dataJson?.get("reason")?.takeIf { it.isJsonPrimitive }?.asJsonPrimitive?.takeIf { it.isString }?.asString)
return UserResultLoginQueryResponse(data)
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package com.github.andreyasadchy.xtra.model.query

data class UserResultLoginQueryResponse(val data: androidx.core.util.Pair<String?, String?>?)
data class UserResultLoginQueryResponse(val data: Pair<String?, String?>?)
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.github.andreyasadchy.xtra.repository

import android.util.Base64
import androidx.core.util.Pair
import com.github.andreyasadchy.xtra.R
import com.github.andreyasadchy.xtra.XtraApp
import com.github.andreyasadchy.xtra.api.HelixApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,8 @@ class GraphQLRepository @Inject constructor(private val graphQL: GraphQLApi) {
return graphQL.getQueryUserResultLogin(headers, json)
}

suspend fun loadPlaybackAccessToken(headers: Map<String, String>, login: String? = null, vodId: String? = null, playerType: String?): PlaybackAccessTokenResponse {
val json = JsonObject().apply {
fun getPlaybackAccessTokenRequestBody(login: String?, vodId: String?, playerType: String?): JsonObject {
return JsonObject().apply {
add("extensions", JsonObject().apply {
add("persistedQuery", JsonObject().apply {
addProperty("sha256Hash", "3093517e37e4f4cb48906155bcd894150aef92617939236d2508f3375ab732ce")
Expand All @@ -329,7 +329,10 @@ class GraphQLRepository @Inject constructor(private val graphQL: GraphQLApi) {
addProperty("playerType", playerType)
})
}
return graphQL.getPlaybackAccessToken(headers, json)
}

suspend fun loadPlaybackAccessToken(headers: Map<String, String>, login: String? = null, vodId: String? = null, playerType: String?): PlaybackAccessTokenResponse {
return graphQL.getPlaybackAccessToken(headers, getPlaybackAccessTokenRequestBody(login, vodId, playerType))
}

suspend fun loadClipUrls(headers: Map<String, String>, slug: String?): Map<String, String>? = withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -742,8 +745,8 @@ class GraphQLRepository @Inject constructor(private val graphQL: GraphQLApi) {
return data
}

suspend fun loadVideoMessages(headers: Map<String, String>, videoId: String?, offset: Int? = null, cursor: String? = null): VideoMessagesDataResponse {
val json = JsonObject().apply {
private fun getVideoMessagesRequestBody(videoId: String?, offset: Int?, cursor: String?): JsonObject {
return JsonObject().apply {
add("extensions", JsonObject().apply {
add("persistedQuery", JsonObject().apply {
addProperty("sha256Hash", "b70a3591ff0f4e0313d126c6a1502d79a1c02baebb288227c582044aa76adf6a")
Expand All @@ -757,25 +760,14 @@ class GraphQLRepository @Inject constructor(private val graphQL: GraphQLApi) {
addProperty("videoID", videoId)
})
}
return graphQL.getVideoMessages(headers, json)
}

suspend fun loadVideoMessages(headers: Map<String, String>, videoId: String?, offset: Int? = null, cursor: String? = null): VideoMessagesDataResponse {
return graphQL.getVideoMessages(headers, getVideoMessagesRequestBody(videoId, offset, cursor))
}

suspend fun loadVideoMessagesDownload(headers: Map<String, String>, videoId: String?, offset: Int? = null, cursor: String? = null): VideoMessagesDownloadDataResponse {
val json = JsonObject().apply {
add("extensions", JsonObject().apply {
add("persistedQuery", JsonObject().apply {
addProperty("sha256Hash", "b70a3591ff0f4e0313d126c6a1502d79a1c02baebb288227c582044aa76adf6a")
addProperty("version", 1)
})
})
addProperty("operationName", "VideoCommentsByOffsetOrCursor")
add("variables", JsonObject().apply {
addProperty("cursor", cursor)
addProperty("contentOffsetSeconds", offset)
addProperty("videoID", videoId)
})
}
return graphQL.getVideoMessagesDownload(headers, json)
return graphQL.getVideoMessagesDownload(headers, getVideoMessagesRequestBody(videoId, offset, cursor))
}

suspend fun loadVideoGames(headers: Map<String, String>, videoId: String?): VideoGamesDataResponse {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.github.andreyasadchy.xtra.repository

import android.net.Uri
import android.util.Base64
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
Expand All @@ -20,7 +19,8 @@ import com.github.andreyasadchy.xtra.model.chat.RecentMessagesResponse
import com.github.andreyasadchy.xtra.model.chat.StvChannelResponse
import com.github.andreyasadchy.xtra.model.chat.StvGlobalResponse
import com.github.andreyasadchy.xtra.util.C
import com.google.gson.JsonObject
import com.github.andreyasadchy.xtra.util.m3u8.MediaPlaylist
import com.github.andreyasadchy.xtra.util.m3u8.PlaylistUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
Expand All @@ -31,6 +31,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.ResponseBody
import okio.use
import org.json.JSONObject
import retrofit2.Response
import java.net.InetSocketAddress
Expand All @@ -50,89 +51,79 @@ class PlayerRepository @Inject constructor(
private val recentEmotes: RecentEmotesDao,
private val videoPositions: VideoPositionsDao) {

suspend fun getResponse(url: String, body: RequestBody? = null, headers: Map<String, String>? = null, proxyHost: String? = null, proxyPort: Int? = null, proxyUser: String? = null, proxyPassword: String? = null): String = withContext(Dispatchers.IO) {
suspend fun getMediaPlaylist(url: String): MediaPlaylist = withContext(Dispatchers.IO) {
okHttpClient.newCall(Request.Builder().url(url).build()).execute().use { response ->
response.body()!!.byteStream().use {
PlaylistUtils.parseMediaPlaylist(it)
}
}
}

suspend fun loadStreamPlaylistUrl(gqlHeaders: Map<String, String>, channelLogin: String, randomDeviceId: Boolean?, xDeviceId: String?, playerType: String?, supportedCodecs: String?, proxyPlaybackAccessToken: Boolean, proxyHost: String?, proxyPort: Int?, proxyUser: String?, proxyPassword: String?, enableIntegrity: Boolean): String = withContext(Dispatchers.IO) {
val accessToken = loadStreamPlaybackAccessToken(gqlHeaders, channelLogin, randomDeviceId, xDeviceId, playerType, proxyPlaybackAccessToken, proxyHost, proxyPort, proxyUser, proxyPassword, enableIntegrity)
buildUrl(
"https://usher.ttvnw.net/api/channel/hls/$channelLogin.m3u8?",
"allow_source", "true",
"allow_audio_only", "true",
"fast_bread", "true", //low latency
"p", Random.nextInt(9999999).toString(),
"platform", if (supportedCodecs?.contains("av1", true) == true) "web" else null,
"sig", accessToken?.signature,
"supported_codecs", supportedCodecs,
"token", accessToken?.token
).toString()
}

suspend fun loadStreamPlaylistResponse(url: String, proxyMultivariantPlaylist: Boolean, proxyHost: String?, proxyPort: Int?, proxyUser: String?, proxyPassword: String?): ResponseBody = withContext(Dispatchers.IO) {
okHttpClient.newBuilder().apply {
if (!proxyHost.isNullOrBlank() && proxyPort != null) {
if (proxyMultivariantPlaylist && !proxyHost.isNullOrBlank() && proxyPort != null) {
proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(proxyHost, proxyPort)))
}
if (!proxyUser.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
proxyAuthenticator { _, response ->
response.request().newBuilder().header(
"Proxy-Authorization", Credentials.basic(proxyUser, proxyPassword)
).build()
if (!proxyUser.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
proxyAuthenticator { _, response ->
response.request().newBuilder().header("Proxy-Authorization", Credentials.basic(proxyUser, proxyPassword)).build()
}
}
}
}.build().newCall(Request.Builder().apply {
url(url)
body?.let { post(it) }
headers?.entries?.forEach {
addHeader(it.key, it.value)
}
}.build()).execute().use { it.body()?.string() ?: "" }
}.build().newCall(Request.Builder().url(url).build()).execute().use { response ->
response.body()!!
}
}

suspend fun loadStreamPlaylistUrl(gqlHeaders: Map<String, String>, channelLogin: String, randomDeviceId: Boolean?, xDeviceId: String?, playerType: String?, supportedCodecs: String?, proxyPlaybackAccessToken: Boolean, proxyMultivariantPlaylist: Boolean, proxyHost: String?, proxyPort: Int?, proxyUser: String?, proxyPassword: String?, enableIntegrity: Boolean): String = withContext(Dispatchers.IO) {
private suspend fun loadStreamPlaybackAccessToken(gqlHeaders: Map<String, String>, channelLogin: String, randomDeviceId: Boolean?, xDeviceId: String?, playerType: String?, proxyPlaybackAccessToken: Boolean, proxyHost: String?, proxyPort: Int?, proxyUser: String?, proxyPassword: String?, enableIntegrity: Boolean): PlaybackAccessToken? = withContext(Dispatchers.IO) {
val accessTokenHeaders = getPlaybackAccessTokenHeaders(gqlHeaders, randomDeviceId, xDeviceId, enableIntegrity)
val accessToken = if (proxyPlaybackAccessToken && !proxyHost.isNullOrBlank() && proxyPort != null) {
val json = JsonObject().apply {
add("extensions", JsonObject().apply {
add("persistedQuery", JsonObject().apply {
addProperty("sha256Hash", "3093517e37e4f4cb48906155bcd894150aef92617939236d2508f3375ab732ce")
addProperty("version", 1)
})
})
addProperty("operationName", "PlaybackAccessToken")
add("variables", JsonObject().apply {
addProperty("isLive", true)
addProperty("login", channelLogin)
addProperty("isVod", false)
addProperty("vodID", "")
addProperty("playerType", playerType)
})
if (proxyPlaybackAccessToken && !proxyHost.isNullOrBlank() && proxyPort != null) {
okHttpClient.newBuilder().apply {
proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(proxyHost, proxyPort)))
if (!proxyUser.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
proxyAuthenticator { _, response ->
response.request().newBuilder().header(
"Proxy-Authorization", Credentials.basic(proxyUser, proxyPassword)
).build()
}
}
}.build().newCall(Request.Builder().apply {
url("https://gql.twitch.tv/gql/")
post(RequestBody.create(MediaType.get("application/x-www-form-urlencoded; charset=utf-8"), graphQL.getPlaybackAccessTokenRequestBody(channelLogin, "", playerType).toString()))
accessTokenHeaders.filterKeys { it == C.HEADER_CLIENT_ID || it == "X-Device-Id" }.forEach {
addHeader(it.key, it.value)
}
}.build()).execute().use { response ->
val text = response.body()!!.string()
if (text.isNotBlank()) {
val message = JSONObject(text).optJSONObject("data")?.optJSONObject("streamPlaybackAccessToken")
PlaybackAccessToken(
token = message?.optString("value"),
signature = message?.optString("signature"),
)
} else null
}
val text = getResponse(
url = "https://gql.twitch.tv/gql/",
body = RequestBody.create(MediaType.get("application/x-www-form-urlencoded; charset=utf-8"), json.toString()),
headers = accessTokenHeaders.filterKeys { it == C.HEADER_CLIENT_ID || it == "X-Device-Id" },
proxyHost = proxyHost,
proxyPort = proxyPort,
proxyUser = proxyUser,
proxyPassword = proxyPassword
)
val data = if (text.isNotBlank()) JSONObject(text).optJSONObject("data") else null
val message = data?.optJSONObject("streamPlaybackAccessToken")
PlaybackAccessToken(
token = message?.optString("value"),
signature = message?.optString("signature"),
)
} else {
graphQL.loadPlaybackAccessToken(
headers = accessTokenHeaders,
login = channelLogin,
playerType = playerType
).streamToken
}
val url = buildUrl(
"https://usher.ttvnw.net/api/channel/hls/$channelLogin.m3u8?",
"allow_source", "true",
"allow_audio_only", "true",
"fast_bread", "true", //low latency
"p", Random.nextInt(9999999).toString(),
"platform", if (supportedCodecs?.contains("av1", true) == true) "web" else null,
"sig", accessToken?.signature,
"supported_codecs", supportedCodecs,
"token", accessToken?.token
).toString()
if (proxyMultivariantPlaylist) {
val response = getResponse(
url = url,
proxyHost = proxyHost,
proxyPort = proxyPort,
proxyUser = proxyUser,
proxyPassword = proxyPassword
)
Base64.encodeToString(response.toByteArray(), Base64.DEFAULT)
} else url
}

suspend fun loadVideoPlaylistUrl(gqlHeaders: Map<String, String>, videoId: String?, playerType: String?, supportedCodecs: String?, enableIntegrity: Boolean): Uri = withContext(Dispatchers.IO) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.andreyasadchy.xtra.repository.datasource

import androidx.core.util.Pair
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.github.andreyasadchy.xtra.R
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.andreyasadchy.xtra.repository.datasource

import androidx.core.util.Pair
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.github.andreyasadchy.xtra.R
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.github.andreyasadchy.xtra.repository.datasource

import android.content.Context
import androidx.core.util.Pair
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.github.andreyasadchy.xtra.R
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.andreyasadchy.xtra.repository.datasource

import androidx.core.util.Pair
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.github.andreyasadchy.xtra.R
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.andreyasadchy.xtra.repository.datasource

import androidx.core.util.Pair
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.github.andreyasadchy.xtra.R
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.andreyasadchy.xtra.repository.datasource

import androidx.core.util.Pair
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.github.andreyasadchy.xtra.R
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.andreyasadchy.xtra.repository.datasource

import androidx.core.util.Pair
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.github.andreyasadchy.xtra.R
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.andreyasadchy.xtra.repository.datasource

import androidx.core.util.Pair
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.github.andreyasadchy.xtra.R
Expand Down
Loading

0 comments on commit f671fdc

Please sign in to comment.