Skip to content
This repository has been archived by the owner on Oct 16, 2022. It is now read-only.

Commit

Permalink
use undefined token if all tokens expired
Browse files Browse the repository at this point in the history
added all request headers for playlist access token
  • Loading branch information
AndreyAsadchy committed Jan 24, 2021
1 parent a5fa876 commit 072b42c
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 73 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ android {
defaultConfig {
applicationId "com.github.exact7.xtra"
minSdkVersion 16
versionCode 99
versionName "1.5.6.7"
versionCode 100
versionName "1.5.6.8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
targetSdkVersion 29
javaCompileOptions {
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/com/github/exact7/xtra/api/GraphQLApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.HeaderMap
import retrofit2.http.POST

@JvmSuppressWildcards
Expand All @@ -23,8 +24,8 @@ interface GraphQLApi {
suspend fun getChannelPanel(@Body json: JsonArray): Response<ResponseBody>

@POST(".")
suspend fun getStreamAccessToken(@Header("Authorization") token: String, @Body json: JsonArray): StreamPlaylistTokenResponse
suspend fun getStreamAccessToken(@HeaderMap headers: Map<String, String>, @Body json: JsonArray): StreamPlaylistTokenResponse

@POST(".")
suspend fun getVideoAccessToken(@Header("Authorization") token: String, @Body json: JsonArray): VideoPlaylistTokenResponse
suspend fun getVideoAccessToken(@HeaderMap headers: Map<String, String>, @Body json: JsonArray): VideoPlaylistTokenResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ import kotlinx.coroutines.withContext
import okhttp3.ResponseBody
import retrofit2.HttpException
import retrofit2.Response
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.collections.HashMap
import kotlin.collections.set
import kotlin.random.Random

private const val TAG = "PlayerRepository"
private const val UNDEFINED = "undefined"

@Singleton
class PlayerRepository @Inject constructor(
Expand All @@ -52,99 +55,77 @@ class PlayerRepository @Inject constructor(
// val apiToken = UUID.randomUUID().toString().replace("-", "").substring(0, 32)
// val serverSessionId = UUID.randomUUID().toString().replace("-", "").substring(0, 32)
// val cookie = "unique_id=$uniqueId; unique_id_durable=$uniqueId; twitch.lohp.countryCode=BY; api_token=twilight.$apiToken; server_session_id=$serverSessionId"

// val accessToken = api.getStreamAccessToken(clientId, cookie, channelName, token, playerType)
val array = JsonArray(1)
val streamAccessTokenOperation = JsonObject().apply {
addProperty("operationName", "PlaybackAccessToken")
add("variables", JsonObject().apply {
addProperty("isLive", true)
addProperty("isVod", false)
addProperty("login", channelName)
addProperty("playerType", playerType)
addProperty("vodID", "")
})
add("extensions", JsonObject().apply {
add("persistedQuery", JsonObject().apply {
addProperty("version", 1)
addProperty("sha256Hash", "0828119ded1c13477966434e15800ff57ddacf13ba1911c129dc2200705b0712")
})
})

val accessTokenJson = getAccessTokenJson(isLive = true, isVod = false, login = channelName, playerType = playerType, vodId = "")
val accessTokenHeaders = getAccessTokenHeaders()

suspend fun loadStream(token: String): Uri {
accessTokenHeaders["Authorization"] = token
val accessToken = graphQL.getStreamAccessToken(accessTokenHeaders, accessTokenJson)
val playlistQueryOptions = HashMap<String, String>()
// playlistQueryOptions["token"] = accessToken.token
// playlistQueryOptions["sig"] = accessToken.sig
playlistQueryOptions["token"] = accessToken.token
playlistQueryOptions["sig"] = accessToken.signature
playlistQueryOptions["allow_source"] = "true"
playlistQueryOptions["allow_audio_only"] = "true"
playlistQueryOptions["type"] = "any"
playlistQueryOptions["p"] = Random.nextInt(999999).toString()
playlistQueryOptions["fast_bread"] = "true" //low latency

//not working anyway
// playlistQueryOptions["server_ads"] = "false"
// playlistQueryOptions["show_ads"] = "false"
val playlist = usher.getStreamPlaylist(channelName, playlistQueryOptions)
return playlist.raw().request().url().toString().toUri()
}
array.add(streamAccessTokenOperation)

val shuffled = tokenList.split(",").shuffled()
for (token in shuffled) {
try {
val accessToken = graphQL.getStreamAccessToken(TwitchApiHelper.addTokenPrefix(token), array)
val options = HashMap<String, String>()
// options["token"] = accessToken.token
// options["sig"] = accessToken.sig
options["token"] = accessToken.token
options["sig"] = accessToken.signature
options["allow_source"] = "true"
options["allow_audio_only"] = "true"
options["type"] = "any"
options["p"] = Random.nextInt(999999).toString()
options["fast_bread"] = "true" //low latency

//not working anyway
// options["server_ads"] = "false"
// options["show_ads"] = "false"
val playlist = usher.getStreamPlaylist(channelName, options)
return@withContext playlist.raw().request().url().toString().toUri()
return@withContext loadStream(TwitchApiHelper.addTokenPrefix(token))
} catch (e: HttpException) {
if (e.code() != 401) throw e
Log.e(TAG, "Token $token is expired")
}
}
throw Exception("Unable to load stream")
loadStream(UNDEFINED)
}

suspend fun loadVideoPlaylist(videoId: String, clientId: String, tokenList: String): Response<ResponseBody> = withContext(Dispatchers.IO) {
val id = videoId.substring(1) //substring 1 to remove v, should be removed when upgraded to new api
Log.d(TAG, "Getting video playlist for video $id. Client id: $clientId")

// val accessToken = api.getVideoAccessToken(clientId, id, token)
val array = JsonArray(1)
val videoAccessTokenOperation = JsonObject().apply {
addProperty("operationName", "PlaybackAccessToken")
add("variables", JsonObject().apply {
addProperty("isLive", false)
addProperty("isVod", true)
addProperty("login", "")
addProperty("playerType", "channel_home_live")
addProperty("vodID", id)
})
add("extensions", JsonObject().apply {
add("persistedQuery", JsonObject().apply {
addProperty("version", 1)
addProperty("sha256Hash", "0828119ded1c13477966434e15800ff57ddacf13ba1911c129dc2200705b0712")
})
})
val accessTokenJson = getAccessTokenJson(isLive = false, isVod = true, login = "", playerType = "channel_home_live", vodId = id)
val accessTokenHeaders = getAccessTokenHeaders()

suspend fun loadVideo(token: String): Response<ResponseBody> {
accessTokenHeaders["Authorization"] = token
val accessToken = graphQL.getVideoAccessToken(accessTokenHeaders, accessTokenJson)
val playlistQueryOptions = HashMap<String, String>()
// options["token"] = accessToken.token
// options["sig"] = accessToken.sig
playlistQueryOptions["token"] = accessToken.token
playlistQueryOptions["sig"] = accessToken.signature
playlistQueryOptions["allow_source"] = "true"
playlistQueryOptions["allow_audio_only"] = "true"
playlistQueryOptions["type"] = "any"
playlistQueryOptions["p"] = Random.nextInt(999999).toString()
return usher.getVideoPlaylist(id, playlistQueryOptions)
}
array.add(videoAccessTokenOperation)

val shuffled = tokenList.split(",").shuffled()
for (token in shuffled) {
try {
val accessToken = graphQL.getVideoAccessToken(TwitchApiHelper.addTokenPrefix(token), array)
val options = HashMap<String, String>()
// options["token"] = accessToken.token
// options["sig"] = accessToken.sig
options["token"] = accessToken.token
options["sig"] = accessToken.signature
options["allow_source"] = "true"
options["allow_audio_only"] = "true"
options["type"] = "any"
options["p"] = Random.nextInt(999999).toString()
return@withContext usher.getVideoPlaylist(id, options)
return@withContext loadVideo(TwitchApiHelper.addTokenPrefix(token))
} catch (e: HttpException) {
if (e.code() != 401) throw e
Log.e(TAG, "Token $token is expired")
}
}
throw Exception("Unable to load video")
loadVideo(UNDEFINED)
}

suspend fun loadSubscriberBadges(channelId: String): SubscriberBadgesResponse = withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -192,4 +173,46 @@ class PlayerRepository @Inject constructor(
videoPositions.insert(position)
}
}
}

private fun getAccessTokenJson(isLive: Boolean, isVod: Boolean, login: String, playerType: String, vodId: String): JsonArray {
val jsonArray = JsonArray(1)
val operation = JsonObject().apply {
addProperty("operationName", "PlaybackAccessToken")
add("variables", JsonObject().apply {
addProperty("isLive", isLive)
addProperty("isVod", isVod)
addProperty("login", login)
addProperty("playerType", playerType)
addProperty("vodID", vodId)
})
add("extensions", JsonObject().apply {
add("persistedQuery", JsonObject().apply {
addProperty("version", 1)
addProperty("sha256Hash", "0828119ded1c13477966434e15800ff57ddacf13ba1911c129dc2200705b0712")
})
})
}
jsonArray.add(operation)
return jsonArray
}

private fun getAccessTokenHeaders(): MutableMap<String, String> {
return HashMap<String, String>().apply {
put("X-Device-Id", UUID.randomUUID().toString().replace("-", "").substring(0, 32)) //removes "commercial break in progress"
put("Accept", "*/*")
put("Accept-Encoding", "gzip, deflate, br")
put("Accept-Language", "ru-RU")
put("Connection", "keep-alive")
put("Content-Type", "text/plain;charset=UTF-8")
put("Host", "gql.twitch.tv")
put("Origin", "https://www.twitch.tv")
put("Referer", "https://www.twitch.tv/")
put("sec-ch-ua", "\"Google Chrome\";v=\"87\", \" Not;A Brand\";v=\"99\", \"Chromium\";v=\"87\"")
put("sec-ch-ua-mobile", "?0")
put("Sec-Fetch-Dest", "empty")
put("Sec-Fetch-Mode", "cors")
put("Sec-Fetch-Site", "same-site")
put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36")
}
}
}
4 changes: 3 additions & 1 deletion app/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@
<string name="thank_you_so_much">شكراً جزيلاً!!! ❤❤❤</string>
<string name="animated_emotes">(GIF)الرموز التعبيرية المتحركة</string>
<string name="changelog">
<b>January 23rd, 1.5.6.7</b>
<b>January 24th, 1.5.6.8</b>
\n- Fixed stream errors (for now), but you may see more commercials
\n\n<b>January 23rd, 1.5.6.7</b>
\n- Just trying to keep the app alive
\n\n<b>January 14th, 1.5.6.6</b>
\n- Updated API for streams and videos
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@
<string name="thank_you_so_much">Огромное спасибо!!! ❤❤❤</string>
<string name="animated_emotes">Анимированные смайлики</string>
<string name="changelog">
<b>23 января, 1.5.6.7</b>
<b>24 января, 1.5.6.8</b>
\n- Исправлена ошибка загрузки стримов (пока твич что-то не изменит)
\n\n<b>23 января, 1.5.6.7</b>
\n- Просто пытаюсь продлить жизнь приложению
\n\n<b>14 января, 1.5.6.6</b>
\n- Обновлен API для стримов и видео
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@
<string name="support_the_developer">Support the developer</string>
<string name="animated_emotes">Animated emotes</string>
<string name="changelog">
<b>January 23rd, 1.5.6.7</b>
<b>January 24th, 1.5.6.8</b>
\n- Fixed stream errors (for now), but you may see more commercials
\n\n<b>January 23rd, 1.5.6.7</b>
\n- Just trying to keep the app alive
\n\n<b>January 14th, 1.5.6.6</b>
\n- Updated API for streams and videos
Expand Down

0 comments on commit 072b42c

Please sign in to comment.