mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-19 19:14:51 +00:00
SubsExchange fixes
This commit is contained in:
parent
7f7ebafa46
commit
c1993ffa03
13 changed files with 119 additions and 24 deletions
|
@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.contents
|
|||
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
interface IPlatformContent {
|
||||
|
|
|
@ -14,6 +14,7 @@ import java.time.OffsetDateTime
|
|||
|
||||
@kotlinx.serialization.Serializable
|
||||
open class SerializedPlatformVideo(
|
||||
override val contentType: ContentType = ContentType.MEDIA,
|
||||
override val id: PlatformID,
|
||||
override val name: String,
|
||||
override val thumbnails: Thumbnails,
|
||||
|
@ -27,7 +28,6 @@ open class SerializedPlatformVideo(
|
|||
override val viewCount: Long,
|
||||
override val isShort: Boolean = false
|
||||
) : IPlatformVideo, SerializedPlatformContent {
|
||||
override val contentType: ContentType = ContentType.MEDIA;
|
||||
|
||||
override val isLive: Boolean = false;
|
||||
|
||||
|
@ -44,6 +44,7 @@ open class SerializedPlatformVideo(
|
|||
companion object {
|
||||
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
||||
return SerializedPlatformVideo(
|
||||
ContentType.MEDIA,
|
||||
video.id,
|
||||
video.name,
|
||||
video.thumbnails,
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.futo.platformplayer.models
|
|||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import java.time.LocalDateTime
|
||||
|
@ -46,6 +47,7 @@ class HistoryVideo {
|
|||
val name = str.substring(indexNext + 3);
|
||||
|
||||
val video = resolve?.invoke(url) ?: SerializedPlatformVideo(
|
||||
ContentType.MEDIA,
|
||||
id = PlatformID.asUrlID(url),
|
||||
name = name,
|
||||
thumbnails = Thumbnails(),
|
||||
|
|
|
@ -39,4 +39,16 @@ class OffsetDateTimeSerializer : KSerializer<OffsetDateTime> {
|
|||
return OffsetDateTime.MIN;
|
||||
return OffsetDateTime.of(LocalDateTime.ofEpochSecond(epochSecond, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||
}
|
||||
}
|
||||
class OffsetDateTimeStringSerializer : KSerializer<OffsetDateTime> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("OffsetDateTime", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: OffsetDateTime) {
|
||||
encoder.encodeString(value.toString());
|
||||
}
|
||||
override fun deserialize(decoder: Decoder): OffsetDateTime {
|
||||
val str = decoder.decodeString();
|
||||
|
||||
return OffsetDateTime.parse(str);
|
||||
}
|
||||
}
|
|
@ -375,7 +375,16 @@ class StateSubscriptions {
|
|||
}
|
||||
|
||||
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
||||
val exchangeClient = if(Settings.instance.subscriptions.useSubscriptionExchange) getSubsExchangeClient() else null;
|
||||
var exchangeClient: SubsExchangeClient? = null;
|
||||
if(Settings.instance.subscriptions.useSubscriptionExchange) {
|
||||
try {
|
||||
exchangeClient = getSubsExchangeClient();
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Failed to get subs exchange client: ${ex.message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool, exchangeClient);
|
||||
if(onNewCacheHit != null)
|
||||
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
||||
|
|
|
@ -5,6 +5,9 @@ import com.futo.platformplayer.UIDialogs
|
|||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
||||
|
@ -78,8 +81,10 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||
|
||||
val contractableTasks = tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) };
|
||||
val contract = if(contractableTasks.size > 10) subsExchangeClient?.requestContract(*contractableTasks.map { ChannelRequest(it.url) }.toTypedArray()) else null;
|
||||
if(contract?.provided?.isNotEmpty() == true)
|
||||
Logger.i(TAG, "Received subscription exchange contract (Requires ${contract?.required?.size}, Provides ${contract?.provided?.size}), ID: ${contract?.id}");
|
||||
var providedTasks: MutableList<SubscriptionTask>? = null;
|
||||
if(contract != null && contract.provided.isNotEmpty()){
|
||||
if(contract != null && contract.required.isNotEmpty()){
|
||||
providedTasks = mutableListOf()
|
||||
for(task in tasks.toList()){
|
||||
if(!task.fromCache && !task.fromPeek && contract.provided.contains(task.url)) {
|
||||
|
@ -127,16 +132,18 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||
//Resolve Subscription Exchange
|
||||
if(contract != null) {
|
||||
try {
|
||||
val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract.required.contains(it.task.url) }.map {
|
||||
ChannelResolve(
|
||||
it.task.url,
|
||||
it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) }
|
||||
)
|
||||
}.toTypedArray()
|
||||
val resolve = subsExchangeClient?.resolveContract(
|
||||
contract,
|
||||
*taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) }.map {
|
||||
ChannelResolve(
|
||||
it.task.url,
|
||||
it.pager!!.getResults()
|
||||
)
|
||||
}.toTypedArray()
|
||||
*resolves
|
||||
);
|
||||
if (resolve != null) {
|
||||
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})")
|
||||
for(result in resolve){
|
||||
val task = providedTasks?.find { it.url == result.channelUrl };
|
||||
if(task != null) {
|
||||
|
@ -153,6 +160,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||
}
|
||||
catch(ex: Throwable) {
|
||||
//TODO: fetch remainder after all?
|
||||
Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package com.futo.platformplayer.subsexchange
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class ChannelRequest(
|
||||
var url: String
|
||||
@SerialName("ChannelUrl")
|
||||
var channelUrl: String
|
||||
);
|
|
@ -2,12 +2,18 @@ package com.futo.platformplayer.subsexchange
|
|||
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Serializable
|
||||
class ChannelResolve(
|
||||
@SerialName("ChannelUrl")
|
||||
var channelUrl: String,
|
||||
var content: List<IPlatformContent>,
|
||||
@SerialName("Content")
|
||||
var content: List<SerializedPlatformContent>,
|
||||
@SerialName("Channel")
|
||||
var channel: IPlatformChannel? = null
|
||||
)
|
|
@ -2,16 +2,22 @@ package com.futo.platformplayer.subsexchange
|
|||
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Serializable
|
||||
class ChannelResult(
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
@SerialName("DateTime")
|
||||
var dateTime: OffsetDateTime,
|
||||
@SerialName("ChannelUrl")
|
||||
var channelUrl: String,
|
||||
var content: List<IPlatformContent>,
|
||||
@SerialName("Content")
|
||||
var content: List<SerializedPlatformContent>,
|
||||
@SerialName("Channel")
|
||||
var channel: IPlatformChannel? = null
|
||||
)
|
|
@ -2,16 +2,27 @@ package com.futo.platformplayer.subsexchange
|
|||
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeStringSerializer
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Serializer
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Serializable
|
||||
class ExchangeContract(
|
||||
@SerialName("ID")
|
||||
var id: String,
|
||||
@SerialName("Requests")
|
||||
var requests: List<ChannelRequest>,
|
||||
@SerialName("Provided")
|
||||
var provided: List<String> = listOf(),
|
||||
@SerialName("Required")
|
||||
var required: List<String> = listOf(),
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
@SerialName("Expire")
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeStringSerializer::class)
|
||||
var expired: OffsetDateTime = OffsetDateTime.MIN,
|
||||
@SerialName("ContractVersion")
|
||||
var contractVersion: Int = 1
|
||||
)
|
|
@ -1,10 +1,14 @@
|
|||
package com.futo.platformplayer.subsexchange
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ExchangeContractResolve(
|
||||
@SerialName("PublicKey")
|
||||
val publicKey: String,
|
||||
@SerialName("Signature")
|
||||
val signature: String,
|
||||
@SerialName("Data")
|
||||
val data: String
|
||||
)
|
|
@ -1,3 +1,5 @@
|
|||
import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.subsexchange.ChannelRequest
|
||||
import com.futo.platformplayer.subsexchange.ChannelResolve
|
||||
import com.futo.platformplayer.subsexchange.ChannelResult
|
||||
|
@ -19,13 +21,19 @@ import java.util.Base64
|
|||
import java.io.InputStreamReader
|
||||
import java.io.OutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.math.BigInteger
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.RSAPublicKeySpec
|
||||
|
||||
|
||||
class SubsExchangeClient(private val server: String, private val privateKey: String) {
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
private val publicKey: String = extractPublicKey(privateKey)
|
||||
|
||||
// Endpoints
|
||||
|
@ -43,18 +51,18 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
|
|||
// Endpoint: Resolve
|
||||
fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
|
||||
val contractResolve = convertResolves(*resolves)
|
||||
val result = post("/api/Channel/Resolve?contractId=${contract.id}", Json.encodeToString(contractResolve), "application/json")
|
||||
return Json.decodeFromString(result)
|
||||
val result = post("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json")
|
||||
return Serializer.json.decodeFromString(result)
|
||||
}
|
||||
suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
|
||||
val contractResolve = convertResolves(*resolves)
|
||||
val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Json.encodeToString(contractResolve), "application/json")
|
||||
return Json.decodeFromString(result)
|
||||
val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json")
|
||||
return Serializer.json.decodeFromString(result)
|
||||
}
|
||||
|
||||
|
||||
private fun convertResolves(vararg resolves: ChannelResolve): ExchangeContractResolve {
|
||||
val data = Json.encodeToString(resolves)
|
||||
val data = Serializer.json.encodeToString(resolves)
|
||||
val signature = createSignature(data, privateKey)
|
||||
|
||||
return ExchangeContractResolve(
|
||||
|
@ -66,15 +74,31 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
|
|||
|
||||
// IO methods
|
||||
private fun post(query: String, body: String, contentType: String): String {
|
||||
val url = URL("$server$query")
|
||||
val url = URL("${server.trim('/')}$query")
|
||||
with(url.openConnection() as HttpURLConnection) {
|
||||
requestMethod = "POST"
|
||||
setRequestProperty("Content-Type", contentType)
|
||||
doOutput = true
|
||||
OutputStreamWriter(outputStream, StandardCharsets.UTF_8).use { it.write(body) }
|
||||
OutputStreamWriter(outputStream, StandardCharsets.UTF_8).use { it.write(body); it.flush() }
|
||||
|
||||
val status = responseCode;
|
||||
Logger.i("SubsExchangeClient", "POST [${url}]: ${status}");
|
||||
|
||||
if(status == 200)
|
||||
InputStreamReader(inputStream, StandardCharsets.UTF_8).use {
|
||||
return it.readText()
|
||||
}
|
||||
else {
|
||||
var errorStr = "";
|
||||
try {
|
||||
errorStr = InputStreamReader(errorStream, StandardCharsets.UTF_8).use {
|
||||
return@use it.readText()
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){}
|
||||
|
||||
throw Exception("Exchange server resulted in code ${status}:\n" + errorStr);
|
||||
|
||||
InputStreamReader(inputStream, StandardCharsets.UTF_8).use {
|
||||
return it.readText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,8 +122,15 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
|
|||
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))
|
||||
val keyFactory = KeyFactory.getInstance("RSA")
|
||||
val privateKeyObj = keyFactory.generatePrivate(keySpec) as RSAPrivateKey
|
||||
val publicKeyObj: RSAPublicKey = keyFactory.generatePublic(keySpec) as RSAPublicKey;
|
||||
return Base64.getEncoder().encodeToString(publicKeyObj.encoded)
|
||||
val publicKeyObj: PublicKey? = keyFactory.generatePublic(RSAPublicKeySpec(privateKeyObj.modulus, BigInteger.valueOf(65537)));
|
||||
var publicKeyBase64 = Base64.getEncoder().encodeToString(publicKeyObj?.encoded);
|
||||
var pem = "-----BEGIN PUBLIC KEY-----"
|
||||
while(publicKeyBase64.length > 0) {
|
||||
val length = Math.min(publicKeyBase64.length, 64);
|
||||
pem += "\n" + publicKeyBase64.substring(0, length);
|
||||
publicKeyBase64 = publicKeyBase64.substring(length);
|
||||
}
|
||||
return pem + "\n-----END PUBLIC KEY-----";
|
||||
}
|
||||
|
||||
fun createSignature(data: String, privateKey: String): String {
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.Serializer
|
|||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.Thumbnail
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||
|
@ -39,6 +40,7 @@ class RequireMigrationTests {
|
|||
val viewCount = 1000L
|
||||
|
||||
return SerializedPlatformVideo(
|
||||
ContentType.MEDIA,
|
||||
platformId,
|
||||
name,
|
||||
thumbnails,
|
||||
|
|
Loading…
Add table
Reference in a new issue