Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Koen 2023-12-06 10:34:32 +01:00
commit aee5b75c2f
53 changed files with 2059 additions and 387 deletions

View file

@ -5,6 +5,7 @@ plugins {
id 'org.ajoberstar.grgit' version '1.7.2' id 'org.ajoberstar.grgit' version '1.7.2'
id 'com.google.protobuf' id 'com.google.protobuf'
id 'kotlin-parcelize' id 'kotlin-parcelize'
id 'kotlin-kapt'
} }
ext { ext {
@ -38,7 +39,7 @@ protobuf {
android { android {
namespace 'com.futo.platformplayer' namespace 'com.futo.platformplayer'
compileSdk 33 compileSdk 34
flavorDimensions "buildType" flavorDimensions "buildType"
productFlavors { productFlavors {
stable { stable {
@ -194,6 +195,12 @@ dependencies {
implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'androidx.work:work-runtime-ktx:2.8.1'
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0' implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
//Database
implementation("androidx.room:room-runtime:2.6.0")
annotationProcessor("androidx.room:room-compiler:2.6.0")
kapt("androidx.room:room-compiler:2.6.0")
implementation("androidx.room:room-ktx:2.6.0")
//Payment //Payment
implementation 'com.stripe:stripe-android:20.28.3' implementation 'com.stripe:stripe-android:20.28.3'

View file

@ -0,0 +1,368 @@
package com.futo.platformplayer
import androidx.test.platform.app.InstrumentationRegistry
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.stores.db.ManagedDBDescriptor
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.testing.DBTOs
import org.junit.Assert
import org.junit.Test
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import kotlin.reflect.KClass
class ManagedDBStoreTests {
val context = InstrumentationRegistry.getInstrumentation().targetContext;
@Test
fun startup() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
store.shutdown();
}
@Test
fun insert() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testObj = DBTOs.TestObject();
createAndAssert(store, testObj);
store.shutdown();
}
@Test
fun update() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testObj = DBTOs.TestObject();
val obj = createAndAssert(store, testObj);
testObj.someStr = "Testing";
store.update(obj.id!!, testObj);
val obj2 = store.get(obj.id!!);
assertIndexEquals(obj2, testObj);
store.shutdown();
}
@Test
fun delete() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testObj = DBTOs.TestObject();
val obj = createAndAssert(store, testObj);
store.delete(obj.id!!);
Assert.assertEquals(store.count(), 0);
Assert.assertNull(store.getOrNull(obj.id!!));
store.shutdown();
}
@Test
fun withIndex() {
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
val store = ManagedDBStore.create("test", Descriptor())
.withIndex({it.someString}, index, true)
.load(context, true);
store.deleteAll();
val testObj1 = DBTOs.TestObject();
val testObj2 = DBTOs.TestObject();
val testObj3 = DBTOs.TestObject();
val obj1 = createAndAssert(store, testObj1);
val obj2 = createAndAssert(store, testObj2);
val obj3 = createAndAssert(store, testObj3);
Assert.assertEquals(store.count(), 3);
Assert.assertTrue(index.containsKey(testObj1.someStr));
Assert.assertTrue(index.containsKey(testObj2.someStr));
Assert.assertTrue(index.containsKey(testObj3.someStr));
Assert.assertEquals(index.size, 3);
val oldStr = testObj1.someStr;
testObj1.someStr = UUID.randomUUID().toString();
store.update(obj1.id!!, testObj1);
Assert.assertEquals(index.size, 3);
Assert.assertFalse(index.containsKey(oldStr));
Assert.assertTrue(index.containsKey(testObj1.someStr));
Assert.assertTrue(index.containsKey(testObj2.someStr));
Assert.assertTrue(index.containsKey(testObj3.someStr));
store.delete(obj2.id!!);
Assert.assertEquals(index.size, 2);
Assert.assertFalse(index.containsKey(oldStr));
Assert.assertTrue(index.containsKey(testObj1.someStr));
Assert.assertFalse(index.containsKey(testObj2.someStr));
Assert.assertTrue(index.containsKey(testObj3.someStr));
store.shutdown();
}
@Test
fun withUnique() {
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
val store = ManagedDBStore.create("test", Descriptor())
.withIndex({it.someString}, index, false, true)
.load(context, true);
store.deleteAll();
val testObj1 = DBTOs.TestObject();
val testObj2 = DBTOs.TestObject();
val testObj3 = DBTOs.TestObject();
val obj1 = createAndAssert(store, testObj1);
val obj2 = createAndAssert(store, testObj2);
testObj3.someStr = testObj2.someStr;
Assert.assertEquals(store.insert(testObj3), obj2.id!!);
Assert.assertEquals(store.count(), 2);
store.shutdown();
}
@Test
fun getPage() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testObjs = createSequence(store, 25);
val page1 = store.getPage(0, 10);
val page2 = store.getPage(1, 10);
val page3 = store.getPage(2, 10);
Assert.assertEquals(10, page1.size);
Assert.assertEquals(10, page2.size);
Assert.assertEquals(5, page3.size);
store.shutdown();
}
@Test
fun query() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testStr = UUID.randomUUID().toString();
val testObj1 = DBTOs.TestObject();
val testObj2 = DBTOs.TestObject();
val testObj3 = DBTOs.TestObject();
val testObj4 = DBTOs.TestObject();
testObj3.someStr = testStr;
testObj4.someStr = testStr;
val obj1 = createAndAssert(store, testObj1);
val obj2 = createAndAssert(store, testObj2);
val obj3 = createAndAssert(store, testObj3);
val obj4 = createAndAssert(store, testObj4);
val results = store.query(DBTOs.TestIndex::someString, testStr);
Assert.assertEquals(2, results.size);
for(result in results) {
if(result.someNum == obj3.someNum)
assertIndexEquals(obj3, result);
else
assertIndexEquals(obj4, result);
}
store.shutdown();
}
@Test
fun queryPage() {
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
val store = ManagedDBStore.create("test", Descriptor())
.withIndex({ it.someNum }, index)
.load(context, true);
store.deleteAll();
val testStr = UUID.randomUUID().toString();
val testResults = createSequence(store, 40, { i, testObject ->
if(i % 2 == 0)
testObject.someStr = testStr;
});
val page1 = store.queryPage(DBTOs.TestIndex::someString, testStr, 0,10);
val page2 = store.queryPage(DBTOs.TestIndex::someString, testStr, 1,10);
val page3 = store.queryPage(DBTOs.TestIndex::someString, testStr, 2,10);
Assert.assertEquals(10, page1.size);
Assert.assertEquals(10, page2.size);
Assert.assertEquals(0, page3.size);
store.shutdown();
}
@Test
fun queryPager() {
val testStr = UUID.randomUUID().toString();
testQuery(100, { i, testObject ->
if(i % 2 == 0)
testObject.someStr = testStr;
}) {
val pager = it.queryPager(DBTOs.TestIndex::someString, testStr, 10);
val items = pager.getResults().toMutableList();
while(pager.hasMorePages()) {
pager.nextPage();
items.addAll(pager.getResults());
}
Assert.assertEquals(50, items.size);
for(i in 0 until 50) {
val k = i * 2;
Assert.assertEquals(k, items[i].someNum);
}
}
}
@Test
fun queryLike() {
val testStr = UUID.randomUUID().toString();
val testStrLike = testStr.substring(0, 8) + "Testing" + testStr.substring(8, testStr.length);
testQuery(100, { i, testObject ->
if(i % 2 == 0)
testObject.someStr = testStrLike;
}) {
val results = it.queryLike(DBTOs.TestIndex::someString, "%Testing%");
Assert.assertEquals(50, results.size);
}
}
@Test
fun queryLikePager() {
val testStr = UUID.randomUUID().toString();
val testStrLike = testStr.substring(0, 8) + "Testing" + testStr.substring(8, testStr.length);
testQuery(100, { i, testObject ->
if(i % 2 == 0)
testObject.someStr = testStrLike;
}) {
val pager = it.queryLikePager(DBTOs.TestIndex::someString, "%Testing%", 10);
val items = pager.getResults().toMutableList();
while(pager.hasMorePages()) {
pager.nextPage();
items.addAll(pager.getResults());
}
Assert.assertEquals(50, items.size);
for(i in 0 until 50) {
val k = i * 2;
Assert.assertEquals(k, items[i].someNum);
}
}
}
@Test
fun queryGreater() {
testQuery(100, { i, testObject ->
testObject.someNum = i;
}) {
val results = it.queryGreater(DBTOs.TestIndex::someNum, 51);
Assert.assertEquals(48, results.size);
}
}
@Test
fun querySmaller() {
testQuery(100, { i, testObject ->
testObject.someNum = i;
}) {
val results = it.querySmaller(DBTOs.TestIndex::someNum, 30);
Assert.assertEquals(30, results.size);
}
}
@Test
fun queryBetween() {
testQuery(100, { i, testObject ->
testObject.someNum = i;
}) {
val results = it.queryBetween(DBTOs.TestIndex::someNum, 30, 65);
Assert.assertEquals(34, results.size);
}
}
@Test
fun queryIn() {
val ids = mutableListOf<String>()
testQuery(1100, { i, testObject ->
testObject.someNum = i;
ids.add(testObject.someStr);
}) {
val pager = it.queryInPager(DBTOs.TestIndex::someString, ids.take(1000), 65);
val list = mutableListOf<Any>();
list.addAll(pager.getResults());
while(pager.hasMorePages())
{
pager.nextPage();
list.addAll(pager.getResults());
}
Assert.assertEquals(1000, list.size);
}
}
private fun testQuery(items: Int, modifier: (Int, DBTOs.TestObject)->Unit, testing: (ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>)->Unit) {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
createSequence(store, items, modifier);
try {
testing(store);
}
finally {
store.shutdown();
}
}
private fun createSequence(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, count: Int, modifier: ((Int, DBTOs.TestObject)->Unit)? = null): List<DBTOs.TestIndex> {
val list = mutableListOf<DBTOs.TestIndex>();
for(i in 0 until count) {
val obj = DBTOs.TestObject();
obj.someNum = i;
modifier?.invoke(i, obj);
list.add(createAndAssert(store, obj));
}
return list;
}
private fun createAndAssert(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, obj: DBTOs.TestObject): DBTOs.TestIndex {
val id = store.insert(obj);
Assert.assertTrue(id > 0);
val dbObj = store.get(id);
assertIndexEquals(dbObj, obj);
return dbObj;
}
private fun assertObjectEquals(obj1: DBTOs.TestObject, obj2: DBTOs.TestObject) {
Assert.assertEquals(obj1.someStr, obj2.someStr);
Assert.assertEquals(obj1.someNum, obj2.someNum);
}
private fun assertIndexEquals(obj1: DBTOs.TestIndex, obj2: DBTOs.TestObject) {
Assert.assertEquals(obj1.someString, obj2.someStr);
Assert.assertEquals(obj1.someNum, obj2.someNum);
assertObjectEquals(obj1.obj, obj2);
}
private fun assertIndexEquals(obj1: DBTOs.TestIndex, obj2: DBTOs.TestIndex) {
Assert.assertEquals(obj1.someString, obj2.someString);
Assert.assertEquals(obj1.someNum, obj2.someNum);
assertIndexEquals(obj1, obj2.obj);
}
class Descriptor: ManagedDBDescriptor<DBTOs.TestObject, DBTOs.TestIndex, DBTOs.DB, DBTOs.DBDAO>() {
override val table_name: String = "testing";
override fun indexClass(): KClass<DBTOs.TestIndex> = DBTOs.TestIndex::class;
override fun dbClass(): KClass<DBTOs.DB> = DBTOs.DB::class;
override fun create(obj: DBTOs.TestObject): DBTOs.TestIndex = DBTOs.TestIndex(obj);
}
}

View file

@ -8,7 +8,6 @@ import android.webkit.CookieManager
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.* import com.futo.platformplayer.activities.*
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@ -299,7 +298,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 14) @FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 14)
fun clearChannelCache() { fun clearChannelCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing.."); UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
ChannelContentCache.instance.clear(); StateCache.instance.clear();
UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing"); UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
} }
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer
import android.content.Context import android.content.Context
import android.webkit.CookieManager import android.webkit.CookieManager
import androidx.lifecycle.lifecycleScope
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.Data import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
@ -12,25 +13,31 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.futo.platformplayer.activities.DeveloperActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.background.BackgroundWorker import com.futo.platformplayer.background.BackgroundWorker
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.views.fields.ButtonField
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField import com.futo.platformplayer.views.fields.FormField
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -39,6 +46,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import java.time.OffsetDateTime
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.stream.IntStream.range import java.util.stream.IntStream.range
@ -82,26 +90,153 @@ class SettingsDev : FragmentedStorageFileJson() {
var backgroundSubscriptionFetching: Boolean = false; var backgroundSubscriptionFetching: Boolean = false;
} }
@FormField(R.string.cache, FieldForm.GROUP, -1, 3)
val cache: Cache = Cache();
@Serializable
class Cache {
@FormField(R.string.subscriptions_cache_5000, FieldForm.BUTTON, -1, 1, "subscription_cache_button")
fun subscriptionsCache5000() {
Logger.i("SettingsDev", "Started caching 5000 sub items");
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"Started caching 5000 sub items"
);
val button = DeveloperActivity.getActivity()?.getField("subscription_cache_button");
if(button is ButtonField)
button.setButtonEnabled(false);
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
val subsCache =
StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(cacheScope = this)?.first;
var total = 0;
var page = 0;
var lastToast = System.currentTimeMillis();
while(subsCache!!.hasMorePages() && total < 5000) {
subsCache!!.nextPage();
total += subsCache!!.getResults().size;
page++;
if(page % 10 == 0)
withContext(Dispatchers.Main) {
val diff = System.currentTimeMillis() - lastToast;
lastToast = System.currentTimeMillis();
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
);
}
Thread.sleep(250);
}
withContext(Dispatchers.Main) {
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"FINISHED Page: ${page}, Total: ${total}"
);
}
}
catch(ex: Throwable) {
Logger.e("SettingsDev", ex.message, ex);
Logger.i("SettingsDev", "Failed: ${ex.message}");
}
finally {
withContext(Dispatchers.Main) {
if(button is ButtonField)
button.setButtonEnabled(true);
}
}
}
}
@FormField(R.string.history_cache_100, FieldForm.BUTTON, -1, 1, "history_cache_button")
fun historyCache100() {
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"Started caching 100 history items (from home)"
);
val button = DeveloperActivity.getActivity()?.getField("history_cache_button");
if(button is ButtonField)
button.setButtonEnabled(false);
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
val subsCache = StatePlatform.instance.getHome();
var num = 0;
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
num++;
}
var total = 0;
var page = 0;
var lastToast = System.currentTimeMillis();
while(subsCache!!.hasMorePages() && total < 5000) {
subsCache!!.nextPage();
total += subsCache!!.getResults().size;
page++;
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
num++;
}
if(page % 4 == 0)
withContext(Dispatchers.Main) {
val diff = System.currentTimeMillis() - lastToast;
lastToast = System.currentTimeMillis();
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
);
}
Thread.sleep(500);
}
withContext(Dispatchers.Main) {
UIDialogs.toast(
SettingsActivity.getActivity()!!,
"FINISHED Page: ${page}, Total: ${total}"
);
}
}
catch(ex: Throwable) {
Logger.e("SettingsDev", ex.message, ex);
Logger.i("SettingsDev", "Failed: ${ex.message}");
}
finally {
withContext(Dispatchers.Main) {
if(button is ButtonField)
button.setButtonEnabled(true);
}
}
}
}
}
@FormField(R.string.crash_me, FieldForm.BUTTON, @FormField(R.string.crash_me, FieldForm.BUTTON,
R.string.crashes_the_application_on_purpose, 2) R.string.crashes_the_application_on_purpose, 3)
fun crashMe() { fun crashMe() {
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!"); throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
} }
@FormField(R.string.delete_announcements, FieldForm.BUTTON, @FormField(R.string.delete_announcements, FieldForm.BUTTON,
R.string.delete_all_announcements, 2) R.string.delete_all_announcements, 3)
fun deleteAnnouncements() { fun deleteAnnouncements() {
StateAnnouncement.instance.deleteAllAnnouncements(); StateAnnouncement.instance.deleteAllAnnouncements();
} }
@FormField(R.string.clear_cookies, FieldForm.BUTTON, @FormField(R.string.clear_cookies, FieldForm.BUTTON,
R.string.clear_all_cookies_from_the_cookieManager, 2) R.string.clear_all_cookies_from_the_cookieManager, 3)
fun clearCookies() { fun clearCookies() {
val cookieManager: CookieManager = CookieManager.getInstance() val cookieManager: CookieManager = CookieManager.getInstance()
cookieManager.removeAllCookies(null); cookieManager.removeAllCookies(null);
} }
@FormField(R.string.test_background_worker, FieldForm.BUTTON, @FormField(R.string.test_background_worker, FieldForm.BUTTON,
R.string.test_background_worker_description, 3) R.string.test_background_worker_description, 4)
fun triggerBackgroundUpdate() { fun triggerBackgroundUpdate() {
val act = SettingsActivity.getActivity()!!; val act = SettingsActivity.getActivity()!!;
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker"); UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
@ -113,10 +248,10 @@ class SettingsDev : FragmentedStorageFileJson() {
wm.enqueue(req); wm.enqueue(req);
} }
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, @FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
R.string.test_background_worker_description, 3) R.string.test_background_worker_description, 4)
fun clearChannelContentCache() { fun clearChannelContentCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache"); UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
ChannelContentCache.instance.clearToday(); StateCache.instance.clearToday();
UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared"); UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared");
} }
@ -363,6 +498,17 @@ class SettingsDev : FragmentedStorageFileJson() {
} }
} }
@Contextual
@Transient
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
var info = Info();
@Serializable
class Info {
@FormField(R.string.dev_info_channel_cache_size, FieldForm.READONLYTEXT, -1, 1, "channelCacheSize")
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
}
//region BOILERPLATE //region BOILERPLATE
override fun encode(): String { override fun encode(): String {
return Json.encodeToString(this); return Json.encodeToString(this);

View file

@ -1,17 +1,24 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.widget.ImageButton import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.IField
class DeveloperActivity : AppCompatActivity() { class DeveloperActivity : AppCompatActivity() {
private lateinit var _form: FieldForm; private lateinit var _form: FieldForm;
private lateinit var _buttonBack: ImageButton; private lateinit var _buttonBack: ImageButton;
fun getField(id: String): IField? {
return _form.findField(id);
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
DeveloperActivity._lastActivity = this;
setContentView(R.layout.activity_dev); setContentView(R.layout.activity_dev);
setNavigationBarColorAndIcons(); setNavigationBarColorAndIcons();
@ -33,4 +40,19 @@ class DeveloperActivity : AppCompatActivity() {
super.finish() super.finish()
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up) overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
} }
companion object {
//TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak")
private var _lastActivity: DeveloperActivity? = null;
fun getActivity(): DeveloperActivity? {
val act = _lastActivity;
if(act != null)
return act;
return null;
}
}
} }

View file

@ -6,9 +6,13 @@ import com.futo.platformplayer.api.media.models.locked.IPlatformLockedContent
import com.futo.platformplayer.api.media.models.nested.IPlatformNestedContent import com.futo.platformplayer.api.media.models.nested.IPlatformNestedContent
import com.futo.platformplayer.api.media.models.post.IPlatformPost import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.serializers.PlatformContentSerializer import com.futo.platformplayer.serializers.PlatformContentSerializer
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.SerialName
@kotlinx.serialization.Serializable(with = PlatformContentSerializer::class) @kotlinx.serialization.Serializable(with = PlatformContentSerializer::class)
interface SerializedPlatformContent: IPlatformContent { interface SerializedPlatformContent: IPlatformContent {
override val contentType: ContentType;
fun toJson() : String; fun toJson() : String;
fun fromJson(str : String) : SerializedPlatformContent; fun fromJson(str : String) : SerializedPlatformContent;
fun fromJsonArray(str : String) : Array<SerializedPlatformContent>; fun fromJsonArray(str : String) : Array<SerializedPlatformContent>;

View file

@ -30,7 +30,7 @@ open class SerializedPlatformLockedContent(
override val unlockUrl: String? = null, override val unlockUrl: String? = null,
override val contentThumbnails: Thumbnails override val contentThumbnails: Thumbnails
) : IPlatformLockedContent, SerializedPlatformContent { ) : IPlatformLockedContent, SerializedPlatformContent {
final override val contentType: ContentType get() = ContentType.LOCKED; override val contentType: ContentType = ContentType.LOCKED;
override fun toJson() : String { override fun toJson() : String {
return Json.encodeToString(this); return Json.encodeToString(this);

View file

@ -30,7 +30,7 @@ open class SerializedPlatformNestedContent(
override val contentProvider: String?, override val contentProvider: String?,
override val contentThumbnails: Thumbnails override val contentThumbnails: Thumbnails
) : IPlatformNestedContent, SerializedPlatformContent { ) : IPlatformNestedContent, SerializedPlatformContent {
final override val contentType: ContentType get() = ContentType.NESTED_VIDEO; final override val contentType: ContentType = ContentType.NESTED_VIDEO;
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id; override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
override val contentSupported: Boolean get() = contentPlugin != null; override val contentSupported: Boolean get() = contentPlugin != null;

View file

@ -8,6 +8,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.post.IPlatformPost import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.polycentric.core.combineHashCodes import com.futo.polycentric.core.combineHashCodes
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -26,7 +27,7 @@ open class SerializedPlatformPost(
override val thumbnails: List<Thumbnails?>, override val thumbnails: List<Thumbnails?>,
override val images: List<String> override val images: List<String>
) : IPlatformPost, SerializedPlatformContent { ) : IPlatformPost, SerializedPlatformContent {
final override val contentType: ContentType get() = ContentType.POST; override val contentType: ContentType = ContentType.POST;
override fun toJson() : String { override fun toJson() : String {
return Json.encodeToString(this); return Json.encodeToString(this);

View file

@ -26,7 +26,7 @@ open class SerializedPlatformVideo(
override val duration: Long, override val duration: Long,
override val viewCount: Long, override val viewCount: Long,
) : IPlatformVideo, SerializedPlatformContent { ) : IPlatformVideo, SerializedPlatformContent {
final override val contentType: ContentType get() = ContentType.MEDIA; override val contentType: ContentType = ContentType.MEDIA;
override val isLive: Boolean = false; override val isLive: Boolean = false;

View file

@ -0,0 +1,31 @@
package com.futo.platformplayer.api.media.structures
class AdhocPager<T>: IPager<T> {
private var _page = 0;
private val _nextPage: (Int) -> List<T>;
private var _currentResults: List<T> = listOf();
private var _hasMore = true;
constructor(nextPage: (Int) -> List<T>, initialResults: List<T>? = null){
_nextPage = nextPage;
if(initialResults != null)
_currentResults = initialResults;
else
nextPage();
}
override fun hasMorePages(): Boolean {
return _hasMore;
}
override fun nextPage() {
val newResults = _nextPage(++_page);
if(newResults.isEmpty())
_hasMore = false;
_currentResults = newResults;
}
override fun getResults(): List<T> {
return _currentResults;
}
}

View file

@ -122,7 +122,7 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
//Only for testing notifications //Only for testing notifications
val testNotifs = 0; val testNotifs = 0;
if(contentNotifs.size == 0 && testNotifs > 0) { if(contentNotifs.size == 0 && testNotifs > 0) {
results.first.getResults().filter { it is IPlatformVideo && it.datetime?.let { it < now } == true } results.first.getResults().filter { it is IPlatformVideo }
.take(testNotifs).forEach { .take(testNotifs).forEach {
contentNotifs.add(Pair(StateSubscriptions.instance.getSubscriptions().first(), it)); contentNotifs.add(Pair(StateSubscriptions.instance.getSubscriptions().first(), it));
} }

View file

@ -1,213 +0,0 @@
package com.futo.platformplayer.cache
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.api.media.structures.DedupContentPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.PlatformContentPager
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.serializers.PlatformContentSerializer
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.toSafeFileName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.OffsetDateTime
import kotlin.streams.toList
import kotlin.system.measureTimeMillis
class ChannelContentCache {
private val _targetCacheSize = 3000;
val _channelCacheDir = FragmentedStorage.getOrCreateDirectory("channelCache");
val _channelContents: HashMap<String, ManagedStore<SerializedPlatformContent>>;
init {
val allFiles = _channelCacheDir.listFiles() ?: arrayOf();
val initializeTime = measureTimeMillis {
_channelContents = HashMap(allFiles
.filter { it.isDirectory }
.parallelStream().map {
Pair(it.name, FragmentedStorage.storeJson(_channelCacheDir, it.name, PlatformContentSerializer())
.withoutBackup()
.load())
}.toList().associate { it })
}
val minDays = OffsetDateTime.now().minusDays(10);
val totalItems = _channelContents.map { it.value.count() }.sum();
val toTrim = totalItems - _targetCacheSize;
val trimmed: Int;
if(toTrim > 0) {
val redundantContent = _channelContents.flatMap { it.value.getItems().filter { it.datetime != null && it.datetime!!.isBefore(minDays) }.drop(9) }
.sortedBy { it.datetime!! }.take(toTrim);
for(content in redundantContent)
uncacheContent(content);
trimmed = redundantContent.size;
}
else trimmed = 0;
Logger.i(TAG, "ChannelContentCache time: ${initializeTime}ms channels: ${allFiles.size}, videos: ${totalItems}, trimmed: ${trimmed}, total: ${totalItems - trimmed}");
}
fun clear() {
synchronized(_channelContents) {
for(channel in _channelContents)
for(content in channel.value.getItems())
uncacheContent(content);
}
}
fun clearToday() {
val yesterday = OffsetDateTime.now().minusDays(1);
synchronized(_channelContents) {
for(channel in _channelContents)
for(content in channel.value.getItems().filter { it.datetime?.isAfter(yesterday) == true })
uncacheContent(content);
}
}
fun getChannelCachePager(channelUrl: String): PlatformContentPager {
val validID = channelUrl.toSafeFileName();
val validStores = _channelContents
.filter { it.key == validID }
.map { it.value };
val items = validStores.flatMap { it.getItems() }
.sortedByDescending { it.datetime };
return PlatformContentPager(items, Math.min(150, items.size));
}
fun getSubscriptionCachePager(): DedupContentPager {
Logger.i(TAG, "Subscriptions CachePager get subscriptions");
val subs = StateSubscriptions.instance.getSubscriptions();
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
val allUrls = subs.map {
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
if(!otherUrls.contains(it.channel.url))
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
else
return@map otherUrls;
}.flatten().distinct();
Logger.i(TAG, "Subscriptions CachePager compiling");
val validSubIds = allUrls.map { it.toSafeFileName() }.toHashSet();
val validStores = _channelContents
.filter { validSubIds.contains(it.key) }
.map { it.value };
val items = validStores.flatMap { it.getItems() }
.sortedByDescending { it.datetime };
return DedupContentPager(PlatformContentPager(items, Math.min(30, items.size)), StatePlatform.instance.getEnabledClients().map { it.id });
}
fun uncacheContent(content: SerializedPlatformContent) {
val store = getContentStore(content);
store?.delete(content);
}
fun cacheContents(contents: List<IPlatformContent>): List<IPlatformContent> {
return contents.filter { cacheContent(it) };
}
fun cacheContent(content: IPlatformContent, doUpdate: Boolean = false): Boolean {
if(content.author.url.isEmpty())
return false;
val channelId = content.author.url.toSafeFileName();
val store = getContentStore(channelId).let {
if(it == null) {
Logger.i(TAG, "New Channel Cache for channel ${content.author.name}");
val store = FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, channelId, PlatformContentSerializer()).load();
_channelContents.put(channelId, store);
return@let store;
}
else return@let it;
}
val serialized = SerializedPlatformContent.fromContent(content);
val existing = store.findItems { it.url == content.url };
if(existing.isEmpty() || doUpdate) {
if(existing.isNotEmpty())
existing.forEach { store.delete(it) };
store.save(serialized);
}
return existing.isEmpty();
}
private fun getContentStore(content: IPlatformContent): ManagedStore<SerializedPlatformContent>? {
val channelId = content.author.url.toSafeFileName();
return getContentStore(channelId);
}
private fun getContentStore(channelId: String): ManagedStore<SerializedPlatformContent>? {
return synchronized(_channelContents) {
var channelStore = _channelContents.get(channelId);
return@synchronized channelStore;
}
}
companion object {
private val TAG = "ChannelCache";
private val _lock = Object();
private var _instance: ChannelContentCache? = null;
val instance: ChannelContentCache get() {
synchronized(_lock) {
if(_instance == null) {
_instance = ChannelContentCache();
}
}
return _instance!!;
}
fun cachePagerResults(scope: CoroutineScope, pager: IPager<IPlatformContent>, onNewCacheHit: ((IPlatformContent)->Unit)? = null): IPager<IPlatformContent> {
return ChannelVideoCachePager(pager, scope, onNewCacheHit);
}
}
class ChannelVideoCachePager(val pager: IPager<IPlatformContent>, private val scope: CoroutineScope, private val onNewCacheItem: ((IPlatformContent)->Unit)? = null): IPager<IPlatformContent> {
init {
val results = pager.getResults();
Logger.i(TAG, "Caching ${results.size} subscription initial results [${pager.hashCode()}]");
scope.launch(Dispatchers.IO) {
try {
val newCacheItems = instance.cacheContents(results);
if(onNewCacheItem != null)
newCacheItems.forEach { onNewCacheItem!!(it) }
} catch (e: Throwable) {
Logger.e(TAG, "Failed to cache videos.", e);
}
}
}
override fun hasMorePages(): Boolean {
return pager.hasMorePages();
}
override fun nextPage() {
pager.nextPage();
val results = pager.getResults();
Logger.i(TAG, "Caching ${results.size} subscription results");
scope.launch(Dispatchers.IO) {
try {
val newCacheItems = instance.cacheContents(results);
if(onNewCacheItem != null)
newCacheItems.forEach { onNewCacheItem!!(it) }
} catch (e: Throwable) {
Logger.e(TAG, "Failed to cache videos.", e);
}
}
}
override fun getResults(): List<IPlatformContent> {
val results = pager.getResults();
return results;
}
}
}

View file

@ -18,6 +18,7 @@ import com.futo.platformplayer.engine.internal.V8BindObject
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import kotlin.streams.asSequence
import kotlin.streams.toList import kotlin.streams.toList
class PackageHttp: V8Package { class PackageHttp: V8Package {
@ -171,7 +172,9 @@ class PackageHttp: V8Package {
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers); return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers);
else else
return@map it.first.request(it.second.method, it.second.url, it.second.headers); return@map it.first.request(it.second.method, it.second.url, it.second.headers);
}.toList(); }
.asSequence()
.toList();
} }
} }

View file

@ -24,7 +24,6 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.IRefreshPager import com.futo.platformplayer.api.media.structures.IRefreshPager
import com.futo.platformplayer.api.media.structures.IReplacerPager import com.futo.platformplayer.api.media.structures.IReplacerPager
import com.futo.platformplayer.api.media.structures.MultiPager import com.futo.platformplayer.api.media.structures.MultiPager
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
@ -32,6 +31,7 @@ import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.fragment.mainactivity.main.FeedView import com.futo.platformplayer.fragment.mainactivity.main.FeedView
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
@ -78,7 +78,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
private val _taskLoadVideos = TaskHandler<IPlatformChannel, IPager<IPlatformContent>>({lifecycleScope}, { private val _taskLoadVideos = TaskHandler<IPlatformChannel, IPager<IPlatformContent>>({lifecycleScope}, {
val livePager = getContentPager(it); val livePager = getContentPager(it);
return@TaskHandler if(_channel?.let { StateSubscriptions.instance.isSubscribed(it) } == true) return@TaskHandler if(_channel?.let { StateSubscriptions.instance.isSubscribed(it) } == true)
ChannelContentCache.cachePagerResults(lifecycleScope, livePager); StateCache.cachePagerResults(lifecycleScope, livePager);
else livePager; else livePager;
}).success { livePager -> }).success { livePager ->
setLoading(false); setLoading(false);
@ -106,7 +106,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
} }
val posBefore = _results.size; val posBefore = _results.size;
val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }; val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
_results.addAll(toAdd); _results.addAll(toAdd);
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); }; _adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); };
}.exception<Throwable> { }.exception<Throwable> {

View file

@ -372,6 +372,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
} }
private fun loadPagerInternal(pager: TPager, cache: ItemCache<TResult>? = null) { private fun loadPagerInternal(pager: TPager, cache: ItemCache<TResult>? = null) {
Logger.i(TAG, "Setting new internal pager on feed");
_cache = cache; _cache = cache;
detachPagerEvents(); detachPagerEvents();
@ -418,6 +419,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
} }
} }
var _lastNextPage = false;
private fun loadNextPage() { private fun loadNextPage() {
synchronized(_pager_lock) { synchronized(_pager_lock) {
val pager: TPager = recyclerData.pager ?: return; val pager: TPager = recyclerData.pager ?: return;
@ -426,9 +428,14 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
//loadCachedPage(); //loadCachedPage();
if (pager.hasMorePages()) { if (pager.hasMorePages()) {
_lastNextPage = true;
setLoading(true); setLoading(true);
_nextPageHandler.run(pager); _nextPageHandler.run(pager);
} }
else if(_lastNextPage) {
Logger.i(TAG, "End of page reached");
_lastNextPage = false;
}
} }
} }

View file

@ -12,6 +12,7 @@ import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.others.TagsView import com.futo.platformplayer.views.others.TagsView
@ -58,7 +59,7 @@ class HistoryFragment : MainFragment() {
tagsView.onClick.subscribe { timeMinutesToErase -> tagsView.onClick.subscribe { timeMinutesToErase ->
UIDialogs.showConfirmationDialog(requireContext(), getString(R.string.are_you_sure_delete_historical), { UIDialogs.showConfirmationDialog(requireContext(), getString(R.string.are_you_sure_delete_historical), {
StatePlaylists.instance.removeHistoryRange(timeMinutesToErase.second as Long); StateHistory.instance.removeHistoryRange(timeMinutesToErase.second as Long);
UIDialogs.toast(view.context, timeMinutesToErase.first + " " + getString(R.string.removed)); UIDialogs.toast(view.context, timeMinutesToErase.first + " " + getString(R.string.removed));
adapter.updateFilteredVideos(); adapter.updateFilteredVideos();
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();

View file

@ -15,13 +15,13 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.exceptions.ChannelException import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.exceptions.RateLimitException import com.futo.platformplayer.exceptions.RateLimitException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
@ -40,6 +40,8 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import kotlin.system.measureTimeMillis
import kotlin.time.measureTime
class SubscriptionsFeedFragment : MainFragment() { class SubscriptionsFeedFragment : MainFragment() {
override val isMainView : Boolean = true; override val isMainView : Boolean = true;
@ -132,8 +134,10 @@ class SubscriptionsFeedFragment : MainFragment() {
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen) if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen)
loadResults(false); loadResults(false);
else if(recyclerData.results.size == 0) else if(recyclerData.results.size == 0) {
loadCache(); loadCache();
setLoading(false);
}
} }
val announcementsView = _announcementsView; val announcementsView = _announcementsView;
@ -306,12 +310,21 @@ class SubscriptionsFeedFragment : MainFragment() {
private fun loadCache() { private fun loadCache() {
Logger.i(TAG, "Subscriptions load cache"); fragment.lifecycleScope.launch(Dispatchers.IO) {
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager(); val cachePager: IPager<IPlatformContent>;
val results = cachePager.getResults(); Logger.i(TAG, "Subscriptions retrieving cache");
Logger.i(TAG, "Subscriptions show cache (${results.size})"); val time = measureTimeMillis {
setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null); cachePager = StateCache.instance.getSubscriptionCachePager();
setPager(cachePager); }
Logger.i(TAG, "Subscriptions retrieved cache (${time}ms)");
withContext(Dispatchers.Main) {
val results = cachePager.getResults();
Logger.i(TAG, "Subscriptions show cache (${results.size})");
setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
setPager(cachePager);
}
}
} }
private fun loadResults(withRefetch: Boolean = false) { private fun loadResults(withRefetch: Boolean = false) {
setLoading(true); setLoading(true);

View file

@ -72,6 +72,7 @@ import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.states.* import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.views.MonetizationView import com.futo.platformplayer.views.MonetizationView
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
import com.futo.platformplayer.views.casting.CastView import com.futo.platformplayer.views.casting.CastView
@ -122,6 +123,7 @@ class VideoDetailView : ConstraintLayout {
private set; private set;
var videoLocal: VideoLocal? = null; var videoLocal: VideoLocal? = null;
private var _playbackTracker: IPlaybackTracker? = null; private var _playbackTracker: IPlaybackTracker? = null;
private var _historyIndex: DBHistory.Index? = null;
val currentUrl get() = video?.url ?: _searchVideo?.url ?: _url; val currentUrl get() = video?.url ?: _searchVideo?.url ?: _url;
@ -769,6 +771,15 @@ class VideoDetailView : ConstraintLayout {
} }
} }
} }
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
val current = _historyIndex;
if(current == null || current.url != video.url) {
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
_historyIndex = index;
return@withContext index;
}
return@withContext current;
}
//Lifecycle //Lifecycle
@ -1274,24 +1285,30 @@ class VideoDetailView : ConstraintLayout {
updateQueueState(); updateQueueState();
_historicalPosition = StatePlaylists.instance.updateHistoryPosition(video, false, (toResume.toFloat() / 1000.0f).toLong()); fragment.lifecycleScope.launch(Dispatchers.IO) {
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"); val historyItem = getHistoryIndex(videoDetail);
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
_layoutResume.visibility = View.VISIBLE;
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) { withContext(Dispatchers.Main) {
try { _historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
delay(8000); Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
_layoutResume.visibility = View.VISIBLE;
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
delay(8000);
_layoutResume.visibility = View.GONE;
_textResume.text = "";
} catch (e: Throwable) {
Logger.e(TAG, "Failed to set resume changes.", e);
}
}
} else {
_layoutResume.visibility = View.GONE; _layoutResume.visibility = View.GONE;
_textResume.text = ""; _textResume.text = "";
} catch (e: Throwable) {
Logger.e(TAG, "Failed to set resume changes.", e);
} }
} }
} else {
_layoutResume.visibility = View.GONE;
_textResume.text = "";
} }
@ -2087,7 +2104,10 @@ class VideoDetailView : ConstraintLayout {
val v = video ?: return; val v = video ?: return;
val currentTime = System.currentTimeMillis(); val currentTime = System.currentTimeMillis();
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) { if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
StatePlaylists.instance.updateHistoryPosition(v, true, (positionMilliseconds.toFloat() / 1000.0f).toLong()); fragment.lifecycleScope.launch(Dispatchers.IO) {
val history = getHistoryIndex(v);
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
}
_lastPositionSaveTime = currentTime; _lastPositionSaveTime = currentTime;
} }

View file

@ -3,6 +3,7 @@ package com.futo.platformplayer.serializers
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.api.media.models.video.SerializedPlatformNestedContent import com.futo.platformplayer.api.media.models.video.SerializedPlatformNestedContent
import com.futo.platformplayer.api.media.models.video.SerializedPlatformPost
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
@ -22,7 +23,7 @@ class PlatformContentSerializer() : JsonContentPolymorphicSerializer<SerializedP
"MEDIA" -> SerializedPlatformVideo.serializer(); "MEDIA" -> SerializedPlatformVideo.serializer();
"NESTED_VIDEO" -> SerializedPlatformNestedContent.serializer(); "NESTED_VIDEO" -> SerializedPlatformNestedContent.serializer();
"ARTICLE" -> throw NotImplementedError("Articles not yet implemented"); "ARTICLE" -> throw NotImplementedError("Articles not yet implemented");
"POST" -> throw NotImplementedError("Post not yet implemented"); "POST" -> SerializedPlatformPost.serializer();
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.contentOrNull}") else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.contentOrNull}")
}; };
else else
@ -30,7 +31,7 @@ class PlatformContentSerializer() : JsonContentPolymorphicSerializer<SerializedP
ContentType.MEDIA.value -> SerializedPlatformVideo.serializer(); ContentType.MEDIA.value -> SerializedPlatformVideo.serializer();
ContentType.NESTED_VIDEO.value -> SerializedPlatformNestedContent.serializer(); ContentType.NESTED_VIDEO.value -> SerializedPlatformNestedContent.serializer();
ContentType.ARTICLE.value -> throw NotImplementedError("Articles not yet implemented"); ContentType.ARTICLE.value -> throw NotImplementedError("Articles not yet implemented");
ContentType.POST.value -> throw NotImplementedError("Post not yet implemented"); ContentType.POST.value -> SerializedPlatformPost.serializer();
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.int}") else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.int}")
}; };
} }

View file

@ -1,24 +1,19 @@
package com.futo.platformplayer.states package com.futo.platformplayer.states
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager
import android.media.AudioManager import android.media.AudioManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.net.Uri import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.util.DisplayMetrics import android.util.DisplayMetrics
import androidx.activity.ComponentActivity import android.util.Xml
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -28,12 +23,11 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.activities.CaptchaActivity import com.futo.platformplayer.activities.CaptchaActivity
import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.Serializer 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.DevJSClient import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.background.BackgroundWorker import com.futo.platformplayer.background.BackgroundWorker
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
@ -43,20 +37,23 @@ import com.futo.platformplayer.logging.AndroidLogConsumer
import com.futo.platformplayer.logging.FileLogConsumer import com.futo.platformplayer.logging.FileLogConsumer
import com.futo.platformplayer.logging.LogLevel import com.futo.platformplayer.logging.LogLevel
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.receivers.AudioNoisyReceiver import com.futo.platformplayer.receivers.AudioNoisyReceiver
import com.futo.platformplayer.serializers.PlatformContentSerializer
import com.futo.platformplayer.services.DownloadService import com.futo.platformplayer.services.DownloadService
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.stripe.android.core.utils.encodeToJson
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
import kotlin.time.measureTime
/*** /***
* This class contains global context for unconventional cases where obtaining context is hard. * This class contains global context for unconventional cases where obtaining context is hard.
@ -66,20 +63,6 @@ import kotlin.time.measureTime
class StateApp { class StateApp {
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
/*
private val externalRootDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "Grayjay");
fun getExternalRootDirectory(): File? {
if(!externalRootDirectory.exists()) {
val result = externalRootDirectory.mkdirs();
if(!result)
return null;
return externalRootDirectory;
}
else
return externalRootDirectory;
}*/
fun getExternalGeneralDirectory(context: Context): DocumentFile? { fun getExternalGeneralDirectory(context: Context): DocumentFile? {
val generalUri = Settings.instance.storage.getStorageGeneralUri(); val generalUri = Settings.instance.storage.getStorageGeneralUri();
if(isValidStorageUri(context, generalUri)) if(isValidStorageUri(context, generalUri))
@ -427,7 +410,7 @@ class StateApp {
try { try {
Logger.i(TAG, "MainApp Started: Initializing [ChannelContentCache]"); Logger.i(TAG, "MainApp Started: Initializing [ChannelContentCache]");
val time = measureTimeMillis { val time = measureTimeMillis {
ChannelContentCache.instance; StateCache.instance;
} }
Logger.i(TAG, "ChannelContentCache initialized in ${time}ms"); Logger.i(TAG, "ChannelContentCache initialized in ${time}ms");
} catch (e: Throwable) { } catch (e: Throwable) {
@ -565,7 +548,34 @@ class StateApp {
StateAnnouncement.instance.registerDefaultHandlerAnnouncement(); StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
StateAnnouncement.instance.registerDidYouKnow(); StateAnnouncement.instance.registerDidYouKnow();
Logger.i(TAG, "MainApp Started: Finished"); Logger.i(TAG, "MainApp Started: Finished");
StatePlaylists.instance.toMigrateCheck();
if(StateHistory.instance.shouldMigrateLegacyHistory())
StateHistory.instance.migrateLegacyHistory();
if(false) {
/*
Logger.i(TAG, "TEST:--------(200)---------");
testHistoryDB(200);
Logger.i(TAG, "TEST:--------(1000)---------");
testHistoryDB(1000);
Logger.i(TAG, "TEST:--------(2000)---------");
testHistoryDB(2000);
Logger.i(TAG, "TEST:--------(4000)---------");
testHistoryDB(4000);
Logger.i(TAG, "TEST:--------(6000)---------");
testHistoryDB(6000);
Logger.i(TAG, "TEST:--------(100000)---------");
scope.launch(Dispatchers.Default) {
StateHistory.instance.testHistoryDB(100000);
}
*/
}
} }
fun mainAppStartedWithExternalFiles(context: Context) { fun mainAppStartedWithExternalFiles(context: Context) {
if(!Settings.instance.didFirstStart) { if(!Settings.instance.didFirstStart) {
if(StateBackup.hasAutomaticBackup()) { if(StateBackup.hasAutomaticBackup()) {

View file

@ -0,0 +1,201 @@
package com.futo.platformplayer.states
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.api.media.structures.DedupContentPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.serializers.PlatformContentSerializer
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.stores.db.types.DBSubscriptionCache
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
import kotlin.streams.asSequence
import kotlin.streams.toList
import kotlin.system.measureTimeMillis
class StateCache {
private val _subscriptionCache = ManagedDBStore.create("subscriptionCache", DBSubscriptionCache.Descriptor(), PlatformContentSerializer())
.load();
val channelCacheStartupCount = _subscriptionCache.count();
fun clear() {
_subscriptionCache.deleteAll();
}
fun clearToday() {
val today = _subscriptionCache.queryGreater(DBSubscriptionCache.Index::datetime, OffsetDateTime.now().toEpochSecond());
for(content in today)
_subscriptionCache.delete(content);
}
fun getChannelCachePager(channelUrl: String): IPager<IPlatformContent> {
val result: IPager<IPlatformContent>;
val time = measureTimeMillis {
result = _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, channelUrl, 20) {
it.obj;
}
}
return result;
}
fun getAllChannelCachePager(channelUrls: List<String>): IPager<IPlatformContent> {
val result: IPager<IPlatformContent>;
val time = measureTimeMillis {
result = _subscriptionCache.queryInPager(DBSubscriptionCache.Index::channelUrl, channelUrls, 20) {
it.obj;
}
}
return result;
}
fun getChannelCachePager(channelUrls: List<String>): IPager<IPlatformContent> {
val pagers = MultiChronoContentPager(channelUrls.map { _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, it, 20) {
it.obj;
} }, false, 20);
return DedupContentPager(pagers, StatePlatform.instance.getEnabledClients().map { it.id });
}
fun getSubscriptionCachePager(): DedupContentPager {
Logger.i(TAG, "Subscriptions CachePager get subscriptions");
val subs = StateSubscriptions.instance.getSubscriptions();
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
val allUrls = subs.map {
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
if(!otherUrls.contains(it.channel.url))
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
else
return@map otherUrls;
}.flatten().distinct();
Logger.i(TAG, "Subscriptions CachePager get pagers");
val pagers: List<IPager<IPlatformContent>>;
val timeCacheRetrieving = measureTimeMillis {
pagers = listOf(getAllChannelCachePager(allUrls));
/*allUrls.parallelStream()
.map {
getChannelCachePager(it)
}
.asSequence()
.toList();*/
}
Logger.i(TAG, "Subscriptions CachePager compiling (retrieved in ${timeCacheRetrieving}ms)");
val pager = MultiChronoContentPager(pagers, false, 20);
pager.initialize();
Logger.i(TAG, "Subscriptions CachePager compiled");
return DedupContentPager(pager, StatePlatform.instance.getEnabledClients().map { it.id });
}
fun getCachedContent(url: String): DBSubscriptionCache.Index? {
return _subscriptionCache.query(DBSubscriptionCache.Index::url, url).firstOrNull();
}
fun uncacheContent(content: SerializedPlatformContent) {
val item = getCachedContent(content.url);
if(item != null)
_subscriptionCache.delete(item);
}
fun cacheContents(contents: List<IPlatformContent>, doUpdate: Boolean = false): List<IPlatformContent> {
return contents.filter { cacheContent(it, doUpdate) };
}
fun cacheContent(content: IPlatformContent, doUpdate: Boolean = false): Boolean {
if(content.author.url.isEmpty())
return false;
val serialized = SerializedPlatformContent.fromContent(content);
val existing = getCachedContent(content.url);
if(existing != null && doUpdate) {
_subscriptionCache.update(existing.id!!, serialized);
return true;
}
else if(existing == null) {
_subscriptionCache.insert(serialized);
return true;
}
return false;
}
companion object {
private val TAG = "StateCache";
private var _instance : StateCache? = null;
val instance : StateCache
get(){
if(_instance == null)
_instance = StateCache();
return _instance!!;
};
fun finish() {
_instance?.let {
_instance = null;
}
}
fun cachePagerResults(scope: CoroutineScope, pager: IPager<IPlatformContent>, onNewCacheHit: ((IPlatformContent)->Unit)? = null): IPager<IPlatformContent> {
return ChannelContentCachePager(pager, scope, onNewCacheHit);
}
}
class ChannelContentCachePager(val pager: IPager<IPlatformContent>, private val scope: CoroutineScope, private val onNewCacheItem: ((IPlatformContent)->Unit)? = null): IPager<IPlatformContent> {
init {
val results = pager.getResults();
Logger.i(TAG, "Caching ${results.size} subscription initial results [${pager.hashCode()}]");
scope.launch(Dispatchers.IO) {
try {
val newCacheItems = StateCache.instance.cacheContents(results, true);
if(onNewCacheItem != null)
newCacheItems.forEach { onNewCacheItem!!(it) }
} catch (e: Throwable) {
Logger.e(TAG, "Failed to cache videos.", e);
}
}
}
override fun hasMorePages(): Boolean {
return pager.hasMorePages();
}
override fun nextPage() {
pager.nextPage();
val results = pager.getResults();
scope.launch(Dispatchers.IO) {
try {
val newCacheItemsCount: Int;
val ms = measureTimeMillis {
val newCacheItems = instance.cacheContents(results, true);
newCacheItemsCount = newCacheItems.size;
if(onNewCacheItem != null)
newCacheItems.forEach { onNewCacheItem!!(it) }
}
Logger.i(TAG, "Caching ${results.size} subscription results, updated ${newCacheItemsCount} (${ms}ms)");
} catch (e: Throwable) {
Logger.e(TAG, "Failed to cache ${results.size} videos.", e);
}
}
}
override fun getResults(): List<IPlatformContent> {
val results = pager.getResults();
return results;
}
}
}

View file

@ -0,0 +1,215 @@
package com.futo.platformplayer.states
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.stores.v2.ReconstructStore
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import kotlin.system.measureTimeMillis
class StateHistory {
//Legacy
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
.withRestore(object: ReconstructStore<HistoryVideo>() {
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): HistoryVideo
= HistoryVideo.fromReconString(backup, null);
})
.load();
private val historyIndex: ConcurrentMap<Any, DBHistory.Index> = ConcurrentHashMap();
val _historyDBStore = ManagedDBStore.create("history", DBHistory.Descriptor())
.withIndex({ it.url }, historyIndex, false, true)
.load();
var onHistoricVideoChanged = Event2<IPlatformVideo, Long>();
fun shouldMigrateLegacyHistory(): Boolean {
return _historyDBStore.count() == 0 && _historyStore.count() > 0;
}
fun migrateLegacyHistory() {
Logger.i(StatePlaylists.TAG, "Migrating legacy history");
_historyDBStore.deleteAll();
val allHistory = _historyStore.getItems();
Logger.i(StatePlaylists.TAG, "Migrating legacy history (${allHistory.size}) items");
for(item in allHistory) {
_historyDBStore.insert(item);
}
_historyStore.deleteAll();
}
fun getHistoryPosition(url: String): Long {
return historyIndex[url]?.position ?: 0;
}
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L): Long {
val pos = if(position < 0) 0 else position;
if(index.obj == null) throw IllegalStateException("Can only update history with a deserialized db item");
val historyVideo = index.obj!!;
val positionBefore = historyVideo.position;
if (updateExisting) {
var shouldUpdate = false;
if (positionBefore < 30) {
shouldUpdate = true;
} else {
if (position > 30) {
shouldUpdate = true;
}
}
if (shouldUpdate) {
//A unrecovered item
if(historyVideo.video.author.id.value == null && historyVideo.video.duration == 0L)
historyVideo.video = SerializedPlatformVideo.fromVideo(liveObj);
historyVideo.position = pos;
historyVideo.date = OffsetDateTime.now();
_historyDBStore.update(index.id!!, historyVideo);
onHistoricVideoChanged.emit(liveObj, pos);
}
return positionBefore;
}
return positionBefore;
}
fun getHistoryLegacy(): List<HistoryVideo> {
return _historyStore.getItems();
}
fun getHistory() : List<HistoryVideo> {
return _historyDBStore.getAllObjects();
//return _historyStore.getItems().sortedByDescending { it.date };
}
fun getHistoryPager(): IPager<HistoryVideo> {
return _historyDBStore.getObjectPager();
}
fun getHistorySearchPager(query: String): IPager<HistoryVideo> {
return _historyDBStore.queryLikeObjectPager(DBHistory.Index::url, "%${query}%", 10);
}
fun getHistoryIndexByUrl(url: String): DBHistory.Index? {
return historyIndex[url];
}
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
val existing = historyIndex[video.url];
if(existing != null)
return _historyDBStore.get(existing.id!!);
else if(create) {
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
val id = _historyDBStore.insert(newHistItem);
return _historyDBStore.get(id);
}
return null;
}
fun removeHistory(url: String) {
val hist = getHistoryIndexByUrl(url);
if(hist != null)
_historyDBStore.delete(hist.id!!);
/*
val hist = _historyStore.findItem { it.video.url == url };
if(hist != null)
_historyStore.delete(hist);*/
}
fun removeHistoryRange(minutesToDelete: Long) {
val now = OffsetDateTime.now().toEpochSecond();
val toDelete = _historyDBStore.getAllIndexes().filter { minutesToDelete == -1L || (now - it.datetime) < minutesToDelete * 60 };
for(item in toDelete)
_historyDBStore.delete(item);
/*
val now = OffsetDateTime.now();
val toDelete = _historyStore.findItems { minutesToDelete == -1L || ChronoUnit.MINUTES.between(it.date, now) < minutesToDelete };
for(item in toDelete)
_historyStore.delete(item);*/
}
companion object {
val TAG = "StateHistory";
private var _instance : StateHistory? = null;
val instance : StateHistory
get(){
if(_instance == null)
_instance = StateHistory();
return _instance!!;
};
fun finish() {
_instance?.let {
_instance = null;
}
}
}
fun testHistoryDB(count: Int) {
Logger.i(TAG, "TEST: Starting tests");
_historyDBStore.deleteAll();
val testHistoryItem = getHistoryLegacy().first();
val testItemJson = testHistoryItem.video.toJson();
val now = OffsetDateTime.now();
val testSet = (0..count).map { HistoryVideo(Json.decodeFromString<SerializedPlatformVideo>(testItemJson.replace(testHistoryItem.video.url, UUID.randomUUID().toString())), it.toLong(), now.minusHours(it.toLong())) }
Logger.i(TAG, "TEST: Inserting (${testSet.size})");
val insertMS = measureTimeMillis {
for(item in testSet)
_historyDBStore.insert(item);
};
Logger.i(TAG, "TEST: Inserting in ${insertMS}ms");
var fetched: List<DBHistory.Index>? = null;
val fetchMS = measureTimeMillis {
fetched = _historyDBStore.getAll();
Logger.i(TAG, "TEST: Fetched: ${fetched?.size}");
};
Logger.i(TAG, "TEST: Fetch speed ${fetchMS}MS");
val deserializeMS = measureTimeMillis {
val deserialized = _historyDBStore.convertObjects(fetched!!);
Logger.i(TAG, "TEST: Deserialized: ${deserialized.size}");
};
Logger.i(TAG, "TEST: Deserialize speed ${deserializeMS}MS");
var fetchedIndex: List<DBHistory.Index>? = null;
val fetchIndexMS = measureTimeMillis {
fetchedIndex = _historyDBStore.getAllIndexes();
Logger.i(TAG, "TEST: Fetched Index: ${fetchedIndex!!.size}");
};
Logger.i(TAG, "TEST: Fetched Index speed ${fetchIndexMS}ms");
val fetchFromIndex = measureTimeMillis {
for(preItem in testSet) {
val item = historyIndex[preItem.video.url];
if(item == null)
throw IllegalStateException("Missing item [${preItem.video.url}]");
if(item.url != preItem.video.url)
throw IllegalStateException("Mismatch item [${preItem.video.url}]");
}
};
Logger.i(TAG, "TEST: Index Lookup speed ${fetchFromIndex}ms");
val page1 = _historyDBStore.getPage(0, 20);
val page2 = _historyDBStore.getPage(1, 20);
val page3 = _historyDBStore.getPage(2, 20);
}
}

View file

@ -117,7 +117,7 @@ class StateNotifications {
.setContentText("${content.name}") .setContentText("${content.name}")
.setSubText(content.datetime?.toHumanNowDiffStringMinDay()) .setSubText(content.datetime?.toHumanNowDiffStringMinDay())
.setSilent(true) .setSilent(true)
.setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.getVideoIntent(context, content.url), .setContentIntent(PendingIntent.getActivity(context, content.hashCode(), MainActivity.getVideoIntent(context, content.url),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setChannelId(notificationChannel.id); .setChannelId(notificationChannel.id);
if(thumbnail != null) { if(thumbnail != null) {

View file

@ -42,6 +42,7 @@ import kotlinx.coroutines.*
import okhttp3.internal.concat import okhttp3.internal.concat
import java.time.OffsetDateTime import java.time.OffsetDateTime
import kotlin.reflect.jvm.internal.impl.builtins.jvm.JavaToKotlinClassMap.PlatformMutabilityMapping import kotlin.reflect.jvm.internal.impl.builtins.jvm.JavaToKotlinClassMap.PlatformMutabilityMapping
import kotlin.streams.asSequence
import kotlin.streams.toList import kotlin.streams.toList
/*** /***
@ -389,6 +390,7 @@ class StatePlatform {
} }
return@map homeResult; return@map homeResult;
} }
.asSequence()
.toList() .toList()
.associateWith { 1f }; .associateWith { 1f };
@ -709,6 +711,7 @@ class StatePlatform {
} }
return@map results; return@map results;
} }
.asSequence()
.toList(); .toList();
val pager = MultiChronoContentPager(pagers.toTypedArray()); val pager = MultiChronoContentPager(pagers.toTypedArray());

View file

@ -3,6 +3,7 @@ package com.futo.platformplayer.states
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
@ -11,6 +12,7 @@ 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.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
@ -19,6 +21,8 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.stores.v2.ReconstructStore import com.futo.platformplayer.stores.v2.ReconstructStore
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@ -26,6 +30,8 @@ import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
/*** /***
* Used to maintain playlists * Used to maintain playlists
@ -39,26 +45,17 @@ class StatePlaylists {
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails); = SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
}) })
.load(); .load();
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
.withRestore(object: ReconstructStore<HistoryVideo>() {
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): HistoryVideo
= HistoryVideo.fromReconString(backup, null);
})
.load();
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists") val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
.withRestore(PlaylistBackup()) .withRestore(PlaylistBackup())
.load(); .load();
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares"); val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
var onHistoricVideoChanged = Event2<IPlatformVideo, Long>();
val onWatchLaterChanged = Event0(); val onWatchLaterChanged = Event0();
fun toMigrateCheck(): List<ManagedStore<*>> { fun toMigrateCheck(): List<ManagedStore<*>> {
return listOf(playlistStore, _watchlistStore, _historyStore); return listOf(playlistStore, _watchlistStore);
} }
fun getWatchLater() : List<SerializedPlatformVideo> { fun getWatchLater() : List<SerializedPlatformVideo> {
synchronized(_watchlistStore) { synchronized(_watchlistStore) {
return _watchlistStore.getItems(); return _watchlistStore.getItems();
@ -99,6 +96,7 @@ class StatePlaylists {
return playlistStore.findItem { it.id == id }; return playlistStore.findItem { it.id == id };
} }
fun didPlay(playlistId: String) { fun didPlay(playlistId: String) {
val playlist = getPlaylist(playlistId); val playlist = getPlaylist(playlistId);
if(playlist != null) { if(playlist != null) {
@ -107,66 +105,6 @@ class StatePlaylists {
} }
} }
fun getHistoryPosition(url: String): Long {
val histVideo = _historyStore.findItem { it.video.url == url };
if(histVideo != null)
return histVideo.position;
return 0;
}
fun updateHistoryPosition(video: IPlatformVideo, updateExisting: Boolean, position: Long = -1L): Long {
val pos = if(position < 0) 0 else position;
val historyVideo = _historyStore.findItem { it.video.url == video.url };
if (historyVideo != null) {
val positionBefore = historyVideo.position;
if (updateExisting) {
var shouldUpdate = false;
if (positionBefore < 30) {
shouldUpdate = true;
} else {
if (position > 30) {
shouldUpdate = true;
}
}
if (shouldUpdate) {
//A unrecovered item
if(historyVideo.video.author.id.value == null && historyVideo.video.duration == 0L)
historyVideo.video = SerializedPlatformVideo.fromVideo(video);
historyVideo.position = pos;
historyVideo.date = OffsetDateTime.now();
_historyStore.saveAsync(historyVideo);
onHistoricVideoChanged.emit(video, pos);
}
}
return positionBefore;
} else {
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), pos, OffsetDateTime.now());
_historyStore.saveAsync(newHistItem);
return 0;
}
}
fun getHistory() : List<HistoryVideo> {
return _historyStore.getItems().sortedByDescending { it.date };
}
fun removeHistory(url: String) {
val hist = _historyStore.findItem { it.video.url == url };
if(hist != null)
_historyStore.delete(hist);
}
fun removeHistoryRange(minutesToDelete: Long) {
val now = OffsetDateTime.now();
val toDelete = _historyStore.findItems { minutesToDelete == -1L || ChronoUnit.MINUTES.between(it.date, now) < minutesToDelete };
for(item in toDelete)
_historyStore.delete(item);
}
suspend fun createPlaylistFromChannel(channelUrl: String, onPage: (Int) -> Unit): Playlist { suspend fun createPlaylistFromChannel(channelUrl: String, onPage: (Int) -> Unit): Playlist {
val channel = StatePlatform.instance.getChannel(channelUrl).await(); val channel = StatePlatform.instance.getChannel(channelUrl).await();
return createPlaylistFromChannel(channel, onPage); return createPlaylistFromChannel(channel, onPage);

View file

@ -10,7 +10,6 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.* import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
@ -39,6 +38,7 @@ import java.util.concurrent.ForkJoinTask
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlin.streams.asSequence
import kotlin.streams.toList import kotlin.streams.toList
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@ -259,7 +259,9 @@ class StateSubscriptions {
Pair(it, StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id)); Pair(it, StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id));
else else
Pair(it, listOf(it.channel.url)); Pair(it, listOf(it.channel.url));
}.toList().associate { it }; }.asSequence()
.toList()
.associate { it };
val result = algo.getSubscriptions(subUrls); val result = algo.getSubscriptions(subUrls);
return Pair(result.pager, result.exceptions); return Pair(result.pager, result.exceptions);

View file

@ -2,6 +2,8 @@ package com.futo.platformplayer.stores
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.stores.db.ManagedDBIndex
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.stores.v2.JsonStoreSerializer import com.futo.platformplayer.stores.v2.JsonStoreSerializer
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.stores.v2.StoreSerializer import com.futo.platformplayer.stores.v2.StoreSerializer

View file

@ -0,0 +1,7 @@
package com.futo.platformplayer.stores.db
import androidx.room.ColumnInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class ColumnIndex(val name: String = ColumnInfo.INHERIT_FIELD_NAME)

View file

@ -0,0 +1,5 @@
package com.futo.platformplayer.stores.db
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class ColumnOrdered(val priority: Int, val descending: Boolean = false);

View file

@ -0,0 +1,27 @@
package com.futo.platformplayer.stores.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
/*
@Dao
class ManagedDBContext<T, I: ManagedDBIndex<T>> {
fun get(id: Int): I;
fun gets(vararg id: Int): List<I>;
fun getAll(): List<I>;
@Insert
fun insert(index: I);
@Insert
fun insertAll(vararg indexes: I)
@Update
fun update(index: I);
@Delete
fun delete(index: I);
}*/

View file

@ -0,0 +1,11 @@
package com.futo.platformplayer.stores.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Update
@Dao
interface ManagedDBContextPaged<T, I: ManagedDBIndex<T>> {
fun getPaged(page: Int, pageSize: Int): List<I>;
}

View file

@ -0,0 +1,35 @@
package com.futo.platformplayer.stores.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
@Dao
interface ManagedDBDAOBase<T, I: ManagedDBIndex<T>> {
@RawQuery
fun get(query: SupportSQLiteQuery): I;
@RawQuery
fun getNullable(query: SupportSQLiteQuery): I?;
@RawQuery
fun getMultiple(query: SupportSQLiteQuery): List<I>;
@RawQuery
fun action(query: SupportSQLiteQuery): Int
@Insert
fun insert(index: I): Long;
@Insert
fun insertAll(vararg indexes: I)
@Update
fun update(index: I);
@Delete
fun delete(index: I);
}

View file

@ -0,0 +1,7 @@
package com.futo.platformplayer.stores.db
import androidx.room.RoomDatabase
abstract class ManagedDBDatabase<T, I: ManagedDBIndex<T>, D: ManagedDBDAOBase<T, I>>: RoomDatabase() {
abstract fun base(): D;
}

View file

@ -0,0 +1,15 @@
package com.futo.platformplayer.stores.db
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.stores.db.types.DBHistory
import kotlin.reflect.KClass
abstract class ManagedDBDescriptor<T, I: ManagedDBIndex<T>, D: ManagedDBDatabase<T, I, DA>, DA: ManagedDBDAOBase<T, I>> {
abstract val table_name: String;
abstract fun dbClass(): KClass<D>;
abstract fun create(obj: T): I;
abstract fun indexClass(): KClass<I>;
}

View file

@ -1,5 +1,29 @@
package com.futo.platformplayer.stores.db package com.futo.platformplayer.stores.db
class ManagedDBIndex { import androidx.room.ColumnInfo
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.futo.platformplayer.api.media.Serializer
open class ManagedDBIndex<T> {
@ColumnIndex
@PrimaryKey(true)
open var id: Long? = null;
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var serialized: ByteArray? = null;
@Ignore
private var _obj: T? = null;
@Ignore
var isCorrupted: Boolean = false;
@get:Ignore
val obj: T get() = _obj ?: throw IllegalStateException("Attempted to access serialized object on a index-only instance");
@get:Ignore
val objOrNull: T? get() = _obj;
fun setInstance(obj: T) {
this._obj = obj;
}
} }

View file

@ -0,0 +1,11 @@
package com.futo.platformplayer.stores.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Update
@Dao
interface ManagedDBIndexOnly<T, I: ManagedDBIndex<T>> {
fun getIndex(): List<I>;
}

View file

@ -1,5 +1,457 @@
package com.futo.platformplayer.stores.db package com.futo.platformplayer.stores.db
class ManagedDBStore { import android.content.Context
import androidx.room.ColumnInfo
import androidx.room.Room
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.api.media.structures.AdhocPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.assume
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
import com.futo.platformplayer.stores.v2.StoreSerializer
import kotlinx.serialization.KSerializer
import java.lang.reflect.Field
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA: ManagedDBDAOBase<T, I>> {
private val _class: KType;
private val _name: String;
private val _serializer: StoreSerializer<T>;
private var _db: ManagedDBDatabase<T, I, *>? = null;
private var _dbDaoBase: ManagedDBDAOBase<T, I>? = null;
val dbDaoBase: ManagedDBDAOBase<T, I> get() = _dbDaoBase ?: throw IllegalStateException("Not initialized db [${name}]");
val descriptor: ManagedDBDescriptor<T, I, D, DA>;
private val _columnInfo: List<ColumnMetadata>;
private val _sqlGet: (Long)-> SimpleSQLiteQuery;
private val _sqlGetIndex: (Long)-> SimpleSQLiteQuery;
private val _sqlGetAll: (LongArray)-> SimpleSQLiteQuery;
private val _sqlAll: SimpleSQLiteQuery;
private val _sqlCount: SimpleSQLiteQuery;
private val _sqlDeleteAll: SimpleSQLiteQuery;
private val _sqlDeleteById: (Long) -> SimpleSQLiteQuery;
private var _sqlIndexed: SimpleSQLiteQuery? = null;
private var _sqlPage: ((Int, Int) -> SimpleSQLiteQuery)? = null;
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
val name: String;
private val _indexes: ArrayList<IndexDescriptor<I>> = arrayListOf();
private val _indexCollection = ConcurrentHashMap<Long, I>();
private var _withUnique: Pair<(I)->Any, ConcurrentMap<Any, I>>? = null;
private val _orderSQL: String?;
constructor(name: String, descriptor: ManagedDBDescriptor<T, I, D, DA>, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
this.descriptor = descriptor;
_name = name;
this.name = niceName ?: name.let {
if(it.isNotEmpty())
return@let it[0].uppercase() + it.substring(1);
return@let name;
};
_serializer = serializer;
_class = clazz;
_columnInfo = this.descriptor.indexClass().memberProperties
.filter { it.hasAnnotation<ColumnIndex>() && it.name != "serialized" }
.map { ColumnMetadata(it.javaField!!, it.findAnnotation<ColumnIndex>()!!, it.findAnnotation<ColumnOrdered>()) };
val indexColumnNames = _columnInfo.map { it.name };
val orderedColumns = _columnInfo.filter { it.ordered != null }.sortedBy { it.ordered!!.priority };
_orderSQL = if(orderedColumns.size > 0)
" ORDER BY " + orderedColumns.map { "${it.name} ${if(it.ordered!!.descending) "DESC" else "ASC"}" }.joinToString(", ");
else "";
_sqlGet = { SimpleSQLiteQuery("SELECT * FROM ${this.descriptor.table_name} WHERE id = ?", arrayOf(it)) };
_sqlGetIndex = { SimpleSQLiteQuery("SELECT ${indexColumnNames.joinToString(", ")} FROM ${this.descriptor.table_name} WHERE id = ?", arrayOf(it)) };
_sqlGetAll = { SimpleSQLiteQuery("SELECT * FROM ${this.descriptor.table_name} WHERE id IN (?)", arrayOf(it)) };
_sqlAll = SimpleSQLiteQuery("SELECT * FROM ${this.descriptor.table_name} ${_orderSQL}");
_sqlCount = SimpleSQLiteQuery("SELECT COUNT(id) FROM ${this.descriptor.table_name}");
_sqlDeleteAll = SimpleSQLiteQuery("DELETE FROM ${this.descriptor.table_name}");
_sqlDeleteById = { id -> SimpleSQLiteQuery("DELETE FROM ${this.descriptor.table_name} WHERE id = :id", arrayOf(id)) };
_sqlIndexed = SimpleSQLiteQuery("SELECT ${indexColumnNames.joinToString(", ")} FROM ${this.descriptor.table_name}");
if(orderedColumns.size > 0) {
_sqlPage = { page, length ->
SimpleSQLiteQuery("SELECT * FROM ${this.descriptor.table_name} ${_orderSQL} LIMIT ? OFFSET ?", arrayOf(length, page * length));
}
}
}
fun withIndex(keySelector: (I)->Any, indexContainer: ConcurrentMap<Any, I>, allowChange: Boolean = false, withUnique: Boolean = false): ManagedDBStore<I, T, D, DA> {
if(_sqlIndexed == null)
throw IllegalStateException("Can only create indexes if sqlIndexOnly is implemented");
_indexes.add(IndexDescriptor(keySelector, indexContainer, allowChange));
if(withUnique)
withUnique(keySelector, indexContainer);
return this;
}
fun withUnique(keySelector: (I)->Any, indexContainer: ConcurrentMap<Any, I>): ManagedDBStore<I, T, D, DA> {
if(_withUnique != null)
throw IllegalStateException("Only 1 unique property is allowed");
_withUnique = Pair(keySelector, indexContainer);
return this;
}
fun load(context: Context? = null, inMemory: Boolean = false): ManagedDBStore<I, T, D, DA> {
_db = (if(!inMemory)
Room.databaseBuilder(context ?: StateApp.instance.context, descriptor.dbClass().java, _name)
else
Room.inMemoryDatabaseBuilder(context ?: StateApp.instance.context, descriptor.dbClass().java))
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
_dbDaoBase = _db!!.base() as ManagedDBDAOBase<T, I>;
if(_indexes.any()) {
val allItems = _dbDaoBase!!.getMultiple(_sqlIndexed!!);
for(index in _indexes)
index.collection.putAll(allItems.associateBy(index.keySelector));
}
return this;
}
fun shutdown() {
val db = _db;
_db = null;
_dbDaoBase = null;
db?.close();
}
fun getUnique(obj: I): I? {
if(_withUnique == null)
throw IllegalStateException("Unique is not configured for ${name}");
val key = _withUnique!!.first.invoke(obj);
return _withUnique!!.second[key];
}
fun isUnique(obj: I): Boolean {
if(_withUnique == null)
throw IllegalStateException("Unique is not configured for ${name}");
val key = _withUnique!!.first.invoke(obj);
return !_withUnique!!.second.containsKey(key);
}
fun count(): Int {
return dbDaoBase.action(_sqlCount);
}
fun insert(obj: T): Long {
val newIndex = descriptor.create(obj);
if(_withUnique != null) {
val unique = getUnique(newIndex);
if (unique != null)
return unique.id!!;
}
newIndex.serialized = serialize(obj);
newIndex.id = dbDaoBase.insert(newIndex);
newIndex.serialized = null;
if(!_indexes.isEmpty()) {
for (index in _indexes) {
val key = index.keySelector(newIndex);
index.collection.put(key, newIndex);
}
}
return newIndex.id!!;
}
fun update(id: Long, obj: T) {
val existing = if(_indexes.any { it.checkChange }) _dbDaoBase!!.getNullable(_sqlGetIndex(id)) else null
val newIndex = descriptor.create(obj);
newIndex.id = id;
newIndex.serialized = serialize(obj);
dbDaoBase.update(newIndex);
newIndex.serialized = null;
if(!_indexes.isEmpty()) {
for (index in _indexes) {
val key = index.keySelector(newIndex);
if(index.checkChange && existing != null) {
val keyExisting = index.keySelector(existing);
if(keyExisting != key)
index.collection.remove(keyExisting);
}
index.collection.put(key, newIndex);
}
}
}
fun getAllIndexes(): List<I> {
if(_sqlIndexed == null)
throw IllegalStateException("Can only create indexes if sqlIndexOnly is implemented");
return dbDaoBase.getMultiple(_sqlIndexed!!);
}
fun getAllObjects(): List<T> = convertObjects(getAll());
fun getAll(): List<I> {
return deserializeIndexes(dbDaoBase.getMultiple(_sqlAll));
}
fun getObject(id: Long) = get(id).obj!!;
fun get(id: Long): I {
return deserializeIndex(dbDaoBase.get(_sqlGet(id)));
}
fun getOrNull(id: Long): I? {
val result = dbDaoBase.getNullable(_sqlGet(id));
if(result == null)
return null;
return deserializeIndex(result);
}
fun getIndexOnlyOrNull(id: Long): I? {
return dbDaoBase.get(_sqlGetIndex(id));
}
fun getAllObjects(vararg id: Long): List<T> = getAll(*id).map { it.obj!! };
fun getAll(vararg id: Long): List<I> {
return deserializeIndexes(dbDaoBase.getMultiple(_sqlGetAll(id)));
}
fun query(field: KProperty<*>, obj: Any): List<I> = query(validateFieldName(field), obj);
fun query(field: String, obj: Any): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} = ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj));
return deserializeIndexes(dbDaoBase.getMultiple(query));
}
fun queryLike(field: KProperty<*>, obj: String): List<I> = queryLike(validateFieldName(field), obj);
fun queryLike(field: String, obj: String): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj));
return deserializeIndexes(dbDaoBase.getMultiple(query));
}
fun queryGreater(field: KProperty<*>, obj: Any): List<I> = queryGreater(validateFieldName(field), obj);
fun queryGreater(field: String, obj: Any): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} > ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj));
return deserializeIndexes(dbDaoBase.getMultiple(query));
}
fun querySmaller(field: KProperty<*>, obj: Any): List<I> = querySmaller(validateFieldName(field), obj);
fun querySmaller(field: String, obj: Any): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} < ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj));
return deserializeIndexes(dbDaoBase.getMultiple(query));
}
fun queryBetween(field: KProperty<*>, greaterThan: Any, smallerThan: Any): List<I> = queryBetween(validateFieldName(field), greaterThan, smallerThan);
fun queryBetween(field: String, greaterThan: Any, smallerThan: Any): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} > ? AND ${field} < ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(greaterThan, smallerThan));
return deserializeIndexes(dbDaoBase.getMultiple(query));
}
//Query Pages
fun queryPage(field: KProperty<*>, obj: Any, page: Int, pageSize: Int): List<I> = queryPage(validateFieldName(field), obj, page, pageSize);
fun queryPage(field: String, obj: Any, page: Int, pageSize: Int): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} = ? ${_orderSQL} LIMIT ? OFFSET ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, pageSize, page * pageSize));
return deserializeIndexes(dbDaoBase.getMultiple(query));
}
fun queryLikePage(field: KProperty<*>, obj: String, page: Int, pageSize: Int): List<I> = queryLikePage(validateFieldName(field), obj, page, pageSize);
fun queryLikePage(field: String, obj: String, page: Int, pageSize: Int): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ? ${_orderSQL} LIMIT ? OFFSET ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, pageSize, page * pageSize));
return deserializeIndexes(dbDaoBase.getMultiple(query));
}
fun queryLikeObjectPage(field: String, obj: String, page: Int, pageSize: Int): List<T> {
return convertObjects(queryLikePage(field, obj, page, pageSize));
}
//Query Page Objects
fun queryPageObjects(field: String, obj: Any, page: Int, pageSize: Int): List<T> = convertObjects(queryPage(field, obj, page, pageSize));
fun queryPageObjects(field: KProperty<*>, obj: Any, page: Int, pageSize: Int): List<T> = queryPageObjects(validateFieldName(field), obj, page, pageSize);
//Query Pager
fun queryPager(field: KProperty<*>, obj: Any, pageSize: Int): IPager<I> = queryPager(validateFieldName(field), obj, pageSize);
fun queryPager(field: String, obj: Any, pageSize: Int): IPager<I> {
return AdhocPager({
Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}");
queryPage(field, obj, it - 1, pageSize);
});
}
fun queryInPage(field: KProperty<*>, obj: List<String>, page: Int, pageSize: Int): List<I> = queryInPage(validateFieldName(field), obj, page, pageSize);
fun queryInPage(field: String, obj: List<String>, page: Int, pageSize: Int): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} IN (${obj.joinToString(",") { "?" }}) ${_orderSQL} LIMIT ? OFFSET ?";
val query = SimpleSQLiteQuery(queryStr, (obj + arrayOf(pageSize, page * pageSize)).toTypedArray());
return deserializeIndexes(dbDaoBase.getMultiple(query));
}
fun queryInObjectPage(field: String, obj: List<String>, page: Int, pageSize: Int): List<T> {
return convertObjects(queryInPage(field, obj, page, pageSize));
}
fun queryInPager(field: KProperty<*>, obj: List<String>, pageSize: Int): IPager<I> = queryInPager(validateFieldName(field), obj, pageSize);
fun queryInPager(field: String, obj: List<String>, pageSize: Int): IPager<I> {
return AdhocPager({
Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}");
queryInPage(field, obj, it - 1, pageSize);
});
}
fun queryInObjectPager(field: KProperty<*>, obj: List<String>, pageSize: Int): IPager<T> = queryInObjectPager(validateFieldName(field), obj, pageSize);
fun queryInObjectPager(field: String, obj: List<String>, pageSize: Int): IPager<T> {
return AdhocPager({
Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}");
queryInObjectPage(field, obj, it - 1, pageSize);
});
}
fun <X> queryInPager(field: KProperty<*>, obj: List<String>, pageSize: Int, convert: (I)->X): IPager<X> = queryInPager(validateFieldName(field), obj, pageSize, convert);
fun <X> queryInPager(field: String, obj: List<String>, pageSize: Int, convert: (I)->X): IPager<X> {
return AdhocPager({
queryInPage(field, obj, it - 1, pageSize).map(convert);
});
}
fun queryLikePager(field: KProperty<*>, obj: String, pageSize: Int): IPager<I> = queryLikePager(validateFieldName(field), obj, pageSize);
fun queryLikePager(field: String, obj: String, pageSize: Int): IPager<I> {
return AdhocPager({
Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}");
queryLikePage(field, obj, it - 1, pageSize);
});
}
fun queryLikeObjectPager(field: KProperty<*>, obj: String, pageSize: Int): IPager<T> = queryLikeObjectPager(validateFieldName(field), obj, pageSize);
fun queryLikeObjectPager(field: String, obj: String, pageSize: Int): IPager<T> {
return AdhocPager({
Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}");
queryLikeObjectPage(field, obj, it - 1, pageSize);
});
}
//Query Pager with convert
fun <X> queryPager(field: KProperty<*>, obj: Any, pageSize: Int, convert: (I)->X): IPager<X> = queryPager(validateFieldName(field), obj, pageSize, convert);
fun <X> queryPager(field: String, obj: Any, pageSize: Int, convert: (I)->X): IPager<X> {
return AdhocPager({
queryPage(field, obj, it - 1, pageSize).map(convert);
});
}
//Query Object Pager
fun queryObjectPager(field: KProperty<*>, obj: Any, pageSize: Int): IPager<T> = queryObjectPager(validateFieldName(field), obj, pageSize);
fun queryObjectPager(field: String, obj: Any, pageSize: Int): IPager<T> {
return AdhocPager({
queryPageObjects(field, obj, it - 1, pageSize);
});
}
//Page
fun getPage(page: Int, length: Int): List<I> {
if(_sqlPage == null)
throw IllegalStateException("DB Store [${name}] does not have ordered fields to provide pages");
val query = _sqlPage!!(page, length) ?: throw IllegalStateException("Paged db not setup for ${_name}");
return deserializeIndexes(dbDaoBase.getMultiple(query));
}
fun getPageObjects(page: Int, length: Int): List<T> = convertObjects(getPage(page, length));
fun getPager(pageLength: Int = 20): IPager<I> {
return AdhocPager({
getPage(it - 1, pageLength);
});
}
fun getObjectPager(pageLength: Int = 20): IPager<T> {
return AdhocPager({
getPageObjects(it - 1, pageLength);
});
}
fun delete(item: I) {
dbDaoBase.delete(item);
for(index in _indexes)
index.collection.remove(index.keySelector(item));
}
fun delete(id: Long) {
dbDaoBase.action(_sqlDeleteById(id));
for(index in _indexes)
index.collection.values.removeIf { it.id == id }
}
fun deleteAll() {
dbDaoBase.action(_sqlDeleteAll);
_indexCollection.clear();
for(index in _indexes)
index.collection.clear();
}
fun convertObject(index: I): T? {
return index.objOrNull ?: deserializeIndex(index).obj;
}
fun convertObjects(indexes: List<I>): List<T> {
return indexes.mapNotNull { it.objOrNull ?: convertObject(it) };
}
fun deserializeIndex(index: I): I {
if(index.isCorrupted)
return index;
if(index.serialized == null) throw IllegalStateException("Cannot deserialize index-only items from [${name}]");
try {
val obj = _serializer.deserialize(_class, index.serialized!!);
index.setInstance(obj);
}
catch(ex: Throwable) {
if(index.serialized != null && index.serialized!!.size > 0) {
Logger.w("ManagedDBStore", "Corrupted object in ${name} found [${index.id}], deleting due to ${ex.message}", ex);
index.isCorrupted = true;
delete(index.id!!);
}
}
index.serialized = null;
return index;
}
fun deserializeIndexes(indexes: List<I>): List<I> {
for(index in indexes)
deserializeIndex(index);
return indexes.filter { !it.isCorrupted }
}
fun serialize(obj: T): ByteArray {
return _serializer.serialize(_class, obj);
}
private fun validateFieldName(prop: KProperty<*>): String {
val declaringClass = prop.javaField?.declaringClass;
if(declaringClass != descriptor.indexClass().java)
throw IllegalStateException("Cannot query by property [${prop.name}] from ${declaringClass?.simpleName} not part of ${descriptor.indexClass().simpleName}");
return prop.name;
}
companion object {
inline fun <reified T, I: ManagedDBIndex<T>, D: ManagedDBDatabase<T, I, DA>, DA: ManagedDBDAOBase<T, I>> create(name: String, descriptor: ManagedDBDescriptor<T, I, D, DA>, serializer: KSerializer<T>? = null)
= ManagedDBStore(name, descriptor, kotlin.reflect.typeOf<T>(), JsonStoreSerializer.create(serializer));
}
//Pair<(I)->Any, ConcurrentMap<Any, I>>
class IndexDescriptor<I>(
val keySelector: (I) -> Any,
val collection: ConcurrentMap<Any, I>,
val checkChange: Boolean
)
class ColumnMetadata(
val field: Field,
val info: ColumnIndex,
val ordered: ColumnOrdered?
) {
val name get() = if(info.name == ColumnInfo.INHERIT_FIELD_NAME) field.name else info.name;
}
} }

View file

@ -0,0 +1,73 @@
package com.futo.platformplayer.stores.db.types
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.stores.db.ColumnIndex
import com.futo.platformplayer.stores.db.ColumnOrdered
import com.futo.platformplayer.stores.db.ManagedDBDAOBase
import com.futo.platformplayer.stores.db.ManagedDBDatabase
import com.futo.platformplayer.stores.db.ManagedDBDescriptor
import com.futo.platformplayer.stores.db.ManagedDBIndex
import com.futo.platformplayer.stores.db.ManagedDBStore
import kotlin.reflect.KClass
import kotlin.reflect.KType
class DBHistory {
companion object {
const val TABLE_NAME = "history";
}
//These classes solely exist for bounding generics for type erasure
@Dao
interface DBDAO: ManagedDBDAOBase<HistoryVideo, Index> {}
@Database(entities = [Index::class], version = 3)
abstract class DB: ManagedDBDatabase<HistoryVideo, Index, DBDAO>() {
abstract override fun base(): DBDAO;
}
class Descriptor: ManagedDBDescriptor<HistoryVideo, Index, DB, DBDAO>() {
override val table_name: String = TABLE_NAME;
override fun create(obj: HistoryVideo): Index = Index(obj);
override fun dbClass(): KClass<DB> = DB::class;
override fun indexClass(): KClass<Index> = Index::class;
}
@Entity(TABLE_NAME, indices = [
androidx.room.Index(value = ["url"]),
androidx.room.Index(value = ["name"]),
androidx.room.Index(value = ["datetime"], orders = [androidx.room.Index.Order.DESC])
])
class Index(): ManagedDBIndex<HistoryVideo>() {
@PrimaryKey(true)
@ColumnOrdered(1)
@ColumnIndex
override var id: Long? = null;
@ColumnIndex
var url: String = "";
@ColumnIndex
var position: Long = 0;
@ColumnIndex
@ColumnOrdered(0, true)
var datetime: Long = 0;
@ColumnIndex
var name: String = "";
constructor(historyVideo: HistoryVideo) : this() {
id = null;
serialized = null;
url = historyVideo.video.url;
position = historyVideo.position;
datetime = historyVideo.date.toEpochSecond();
name = historyVideo.video.name;
}
}
}

View file

@ -0,0 +1,67 @@
package com.futo.platformplayer.stores.db.types
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.stores.db.ColumnIndex
import com.futo.platformplayer.stores.db.ColumnOrdered
import com.futo.platformplayer.stores.db.ManagedDBDAOBase
import com.futo.platformplayer.stores.db.ManagedDBDatabase
import com.futo.platformplayer.stores.db.ManagedDBDescriptor
import com.futo.platformplayer.stores.db.ManagedDBIndex
import kotlin.reflect.KClass
class DBSubscriptionCache {
companion object {
const val TABLE_NAME = "subscription_cache";
}
//These classes solely exist for bounding generics for type erasure
@Dao
interface DBDAO: ManagedDBDAOBase<SerializedPlatformContent, Index> {}
@Database(entities = [Index::class], version = 5)
abstract class DB: ManagedDBDatabase<SerializedPlatformContent, Index, DBDAO>() {
abstract override fun base(): DBDAO;
}
class Descriptor: ManagedDBDescriptor<SerializedPlatformContent, Index, DB, DBDAO>() {
override val table_name: String = TABLE_NAME;
override fun create(obj: SerializedPlatformContent): Index = Index(obj);
override fun dbClass(): KClass<DB> = DB::class;
override fun indexClass(): KClass<Index> = Index::class;
}
@Entity(TABLE_NAME, indices = [
androidx.room.Index(value = ["url"]),
androidx.room.Index(value = ["channelUrl"]),
androidx.room.Index(value = ["datetime"], orders = [androidx.room.Index.Order.DESC])
])
class Index: ManagedDBIndex<SerializedPlatformContent> {
@ColumnIndex
@PrimaryKey(true)
@ColumnOrdered(1)
override var id: Long? = null;
@ColumnIndex
var url: String? = null;
@ColumnIndex
var channelUrl: String? = null;
@ColumnIndex
@ColumnOrdered(0, true)
var datetime: Long? = null;
constructor() {}
constructor(sCache: SerializedPlatformContent) {
id = null;
serialized = null;
url = sCache.url;
channelUrl = sCache.author.url;
datetime = sCache.datetime?.toEpochSecond();
}
}
}

View file

@ -5,10 +5,10 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.DedupContentPager import com.futo.platformplayer.api.media.structures.DedupContentPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.PlatformContentPager import com.futo.platformplayer.api.media.structures.PlatformContentPager
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.toSafeFileName import com.futo.platformplayer.toSafeFileName
@ -27,13 +27,16 @@ class CachedSubscriptionAlgorithm(pageSize: Int = 150, scope: CoroutineScope, al
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result { override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
val validSubIds = subs.flatMap { it.value } .map { it.toSafeFileName() }.toHashSet(); val validSubIds = subs.flatMap { it.value } .map { it.toSafeFileName() }.toHashSet();
val validStores = ChannelContentCache.instance._channelContents /*
val validStores = StateCache.instance._channelContents
.filter { validSubIds.contains(it.key) } .filter { validSubIds.contains(it.key) }
.map { it.value }; .map { it.value };*/
/*
val items = validStores.flatMap { it.getItems() } val items = validStores.flatMap { it.getItems() }
.sortedByDescending { it.datetime }; .sortedByDescending { it.datetime };
*/
return Result(DedupContentPager(PlatformContentPager(items, Math.min(_pageSize, items.size)), StatePlatform.instance.getEnabledClients().map { it.id }), listOf()); return Result(DedupContentPager(StateCache.instance.getChannelCachePager(subs.flatMap { it.value }.distinct()), StatePlatform.instance.getEnabledClients().map { it.id }), listOf());
} }
} }

View file

@ -8,7 +8,6 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.DedupContentPager import com.futo.platformplayer.api.media.structures.DedupContentPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
@ -17,6 +16,7 @@ import com.futo.platformplayer.findNonRuntimeException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
@ -157,7 +157,7 @@ class SimpleSubscriptionAlgorithm(
val time = measureTimeMillis { val time = measureTimeMillis {
pager = StatePlatform.instance.getChannelContent(platformClient, url, true, threadPool.poolSize, toIgnore); pager = StatePlatform.instance.getChannelContent(platformClient, url, true, threadPool.poolSize, toIgnore);
pager = ChannelContentCache.cachePagerResults(scope, pager!!) { pager = StateCache.cachePagerResults(scope, pager!!) {
onNewCacheHit.emit(sub, it); onNewCacheHit.emit(sub, it);
}; };
@ -176,7 +176,7 @@ class SimpleSubscriptionAlgorithm(
throw channelEx; throw channelEx;
else { else {
Logger.i(StateSubscriptions.TAG, "Channel ${sub.channel.name} failed, substituting with cache"); Logger.i(StateSubscriptions.TAG, "Channel ${sub.channel.name} failed, substituting with cache");
pager = ChannelContentCache.instance.getChannelCachePager(sub.channel.url); pager = StateCache.instance.getChannelCachePager(sub.channel.url);
} }
} }
} }

View file

@ -11,7 +11,6 @@ import com.futo.platformplayer.api.media.structures.DedupContentPager
import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
@ -21,6 +20,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragm
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -108,7 +108,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
val sub = if(!entry.value.isEmpty()) entry.value[0].task.sub else null; val sub = if(!entry.value.isEmpty()) entry.value[0].task.sub else null;
val liveTasks = entry.value.filter { !it.task.fromCache }; val liveTasks = entry.value.filter { !it.task.fromCache };
val cachedTasks = entry.value.filter { it.task.fromCache }; val cachedTasks = entry.value.filter { it.task.fromCache };
val livePager = if(!liveTasks.isEmpty()) ChannelContentCache.cachePagerResults(scope, MultiChronoContentPager(liveTasks.map { it.pager!! }, true).apply { this.initialize() }, { val livePager = if(!liveTasks.isEmpty()) StateCache.cachePagerResults(scope, MultiChronoContentPager(liveTasks.map { it.pager!! }, true).apply { this.initialize() }, {
onNewCacheHit.emit(sub!!, it); onNewCacheHit.emit(sub!!, it);
}) else null; }) else null;
val cachedPager = if(!cachedTasks.isEmpty()) MultiChronoContentPager(cachedTasks.map { it.pager!! }, true).apply { this.initialize() } else null; val cachedPager = if(!cachedTasks.isEmpty()) MultiChronoContentPager(cachedTasks.map { it.pager!! }, true).apply { this.initialize() } else null;
@ -142,7 +142,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
return@submit SubscriptionTaskResult(task, null, null); return@submit SubscriptionTaskResult(task, null, null);
else { else {
cachedChannels.add(task.url); cachedChannels.add(task.url);
return@submit SubscriptionTaskResult(task, ChannelContentCache.instance.getChannelCachePager(task.url), null); return@submit SubscriptionTaskResult(task, StateCache.instance.getChannelCachePager(task.url), null);
} }
} }
} }
@ -197,7 +197,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
throw channelEx; throw channelEx;
else { else {
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache"); Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
pager = ChannelContentCache.instance.getChannelCachePager(task.sub.channel.url); pager = StateCache.instance.getChannelCachePager(task.sub.channel.url);
taskEx = ex; taskEx = ex;
return@submit SubscriptionTaskResult(task, pager, taskEx); return@submit SubscriptionTaskResult(task, pager, taskEx);
} }

View file

@ -0,0 +1,47 @@
package com.futo.platformplayer.testing
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
import com.futo.platformplayer.stores.db.ColumnIndex
import com.futo.platformplayer.stores.db.ColumnOrdered
import com.futo.platformplayer.stores.db.ManagedDBDAOBase
import com.futo.platformplayer.stores.db.ManagedDBDatabase
import com.futo.platformplayer.stores.db.ManagedDBIndex
import kotlinx.serialization.Serializable
import java.util.Random
import java.util.UUID
class DBTOs {
@Dao
interface DBDAO: ManagedDBDAOBase<TestObject, TestIndex> {}
@Database(entities = [TestIndex::class], version = 3)
abstract class DB: ManagedDBDatabase<TestObject, TestIndex, DBDAO>() {
abstract override fun base(): DBDAO;
}
@Entity("testing")
class TestIndex(): ManagedDBIndex<TestObject>() {
@ColumnIndex
var someString: String = "";
@ColumnIndex
@ColumnOrdered(0)
var someNum: Int = 0;
constructor(obj: TestObject, customInt: Int? = null) : this() {
someString = obj.someStr;
someNum = customInt ?: obj.someNum;
}
}
@Serializable
class TestObject {
var someStr = UUID.randomUUID().toString();
var someNum = random.nextInt();
}
companion object {
val random = Random();
}
}

View file

@ -4,9 +4,15 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.HistoryVideo import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> { class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
private lateinit var _filteredVideos: MutableList<HistoryVideo>; private lateinit var _filteredVideos: MutableList<HistoryVideo>;
@ -17,17 +23,19 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
constructor() : super() { constructor() : super() {
updateFilteredVideos(); updateFilteredVideos();
StatePlaylists.instance.onHistoricVideoChanged.subscribe(this) { video, position -> StateHistory.instance.onHistoricVideoChanged.subscribe(this) { video, position ->
val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url }; StateApp.instance.scope.launch(Dispatchers.Main) {
if (index == -1) { val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url };
return@subscribe; if (index == -1) {
} return@launch;
}
_filteredVideos[index].position = position; _filteredVideos[index].position = position;
if (index < _filteredVideos.size - 2) { if (index < _filteredVideos.size - 2) {
notifyItemRangeChanged(index, 2); notifyItemRangeChanged(index, 2);
} else { } else {
notifyItemChanged(index); notifyItemChanged(index);
}
} }
}; };
} }
@ -38,7 +46,10 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
} }
fun updateFilteredVideos() { fun updateFilteredVideos() {
val videos = StatePlaylists.instance.getHistory(); val videos = StateHistory.instance.getHistory();
val pager = StateHistory.instance.getHistoryPager();
//filtered val pager = StateHistory.instance.getHistorySearchPager("querrryyyyy");
if (_query.isBlank()) { if (_query.isBlank()) {
_filteredVideos = videos.toMutableList(); _filteredVideos = videos.toMutableList();
} else { } else {
@ -49,7 +60,7 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
} }
fun cleanup() { fun cleanup() {
StatePlaylists.instance.onHistoricVideoChanged.remove(this); StateHistory.instance.onHistoricVideoChanged.remove(this);
} }
override fun getItemCount() = _filteredVideos.size; override fun getItemCount() = _filteredVideos.size;
@ -65,7 +76,7 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
return@subscribe; return@subscribe;
} }
StatePlaylists.instance.removeHistory(v.video.url); StateHistory.instance.removeHistory(v.video.url);
_filteredVideos.removeAt(index); _filteredVideos.removeAt(index);
notifyItemRemoved(index); notifyItemRemoved(index);
}; };

View file

@ -27,6 +27,7 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.others.CreatorThumbnail
@ -251,7 +252,7 @@ open class PreviewVideoView : LinearLayout {
val timeBar = _timeBar val timeBar = _timeBar
if (timeBar != null) { if (timeBar != null) {
if (shouldShowTimeBar) { if (shouldShowTimeBar) {
val historyPosition = StatePlaylists.instance.getHistoryPosition(video.url) val historyPosition = StateHistory.instance.getHistoryPosition(video.url)
timeBar.visibility = if (historyPosition > 0) VISIBLE else GONE timeBar.visibility = if (historyPosition > 0) VISIBLE else GONE
timeBar.progress = historyPosition.toFloat() / video.duration.toFloat() timeBar.progress = historyPosition.toFloat() / video.duration.toFloat()
} else { } else {

View file

@ -90,7 +90,11 @@ class GestureControlView : LinearLayout {
override fun onDown(p0: MotionEvent): Boolean { return false; } override fun onDown(p0: MotionEvent): Boolean { return false; }
override fun onShowPress(p0: MotionEvent) = Unit; override fun onShowPress(p0: MotionEvent) = Unit;
override fun onSingleTapUp(p0: MotionEvent): Boolean { return false; } override fun onSingleTapUp(p0: MotionEvent): Boolean { return false; }
override fun onScroll(p0: MotionEvent, p1: MotionEvent, distanceX: Float, distanceY: Float): Boolean { override fun onFling(p0: MotionEvent?, p1: MotionEvent, p2: Float, p3: Float): Boolean { return false; }
override fun onScroll(p0: MotionEvent?, p1: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
if(p0 == null)
return false;
if (_isFullScreen && _adjustingBrightness) { if (_isFullScreen && _adjustingBrightness) {
val adjustAmount = (distanceY * 2) / height; val adjustAmount = (distanceY * 2) / height;
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f); _brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
@ -129,8 +133,7 @@ class GestureControlView : LinearLayout {
return true; return true;
} }
override fun onLongPress(p0: MotionEvent) = Unit; override fun onLongPress(p0: MotionEvent) = Unit
override fun onFling(p0: MotionEvent, p1: MotionEvent, p2: Float, p3: Float): Boolean { return false; }
}); });
_gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener { _gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {

View file

@ -40,6 +40,8 @@ open class BigButton : LinearLayout {
_root.apply { _root.apply {
isClickable = true; isClickable = true;
setOnClickListener { setOnClickListener {
if(!isEnabled)
return@setOnClickListener;
action(); action();
onClick.emit(); onClick.emit();
}; };
@ -54,6 +56,8 @@ open class BigButton : LinearLayout {
_root.apply { _root.apply {
isClickable = true; isClickable = true;
setOnClickListener { setOnClickListener {
if(!isEnabled)
return@setOnClickListener;
onClick.emit(); onClick.emit();
}; };
} }
@ -144,4 +148,17 @@ open class BigButton : LinearLayout {
return this; return this;
} }
fun setButtonEnabled(enabled: Boolean) {
if(enabled) {
alpha = 1f;
isEnabled = true;
isClickable = true;
}
else {
alpha = 0.5f;
isEnabled = false;
isClickable = false;
}
}
} }

View file

@ -57,6 +57,8 @@ class ButtonField : BigButton, IField {
}; };
super.onClick.subscribe { super.onClick.subscribe {
if(!isEnabled)
return@subscribe;
if(_method?.parameterCount == 1) if(_method?.parameterCount == 1)
_method?.invoke(_obj, context); _method?.invoke(_obj, context);
else if(_method?.parameterCount == 2) else if(_method?.parameterCount == 2)

View file

@ -409,6 +409,7 @@
<string name="version_code">Version Code</string> <string name="version_code">Version Code</string>
<string name="version_name">Version Name</string> <string name="version_name">Version Name</string>
<string name="version_type">Version Type</string> <string name="version_type">Version Type</string>
<string name="dev_info_channel_cache_size">Channel Cache Size (Startup)</string>
<string name="when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code">When watching a video in preview mode, resume at the position when opening the video code</string> <string name="when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code">When watching a video in preview mode, resume at the position when opening the video code</string>
<string name="please_enable_logging_to_submit_logs">Please enable logging to submit logs</string> <string name="please_enable_logging_to_submit_logs">Please enable logging to submit logs</string>
<string name="embedded_plugins_reinstalled_a_reboot_is_recommended">Embedded plugins reinstalled, a reboot is recommended</string> <string name="embedded_plugins_reinstalled_a_reboot_is_recommended">Embedded plugins reinstalled, a reboot is recommended</string>
@ -434,6 +435,7 @@
<string name="developer_mode">Developer Mode</string> <string name="developer_mode">Developer Mode</string>
<string name="development_server">Development Server</string> <string name="development_server">Development Server</string>
<string name="experimental">Experimental</string> <string name="experimental">Experimental</string>
<string name="cache">Cache</string>
<string name="fill_storage_till_error">Fill storage till error</string> <string name="fill_storage_till_error">Fill storage till error</string>
<string name="inject">Inject</string> <string name="inject">Inject</string>
<string name="injects_a_test_source_config_local_into_v8">Injects a test source config (local) into V8</string> <string name="injects_a_test_source_config_local_into_v8">Injects a test source config (local) into V8</string>
@ -442,6 +444,8 @@
<string name="removes_all_subscriptions">Removes all subscriptions</string> <string name="removes_all_subscriptions">Removes all subscriptions</string>
<string name="settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities">Settings related to development server, be careful as it may open your phone to security vulnerabilities</string> <string name="settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities">Settings related to development server, be careful as it may open your phone to security vulnerabilities</string>
<string name="start_server">Start Server</string> <string name="start_server">Start Server</string>
<string name="subscriptions_cache_5000">Subscriptions Cache 5000</string>
<string name="history_cache_100">History Cache 100</string>
<string name="start_server_on_boot">Start Server on boot</string> <string name="start_server_on_boot">Start Server on boot</string>
<string name="starts_a_devServer_on_port_11337_may_expose_vulnerabilities">Starts a DevServer on port 11337, may expose vulnerabilities.</string> <string name="starts_a_devServer_on_port_11337_may_expose_vulnerabilities">Starts a DevServer on port 11337, may expose vulnerabilities.</string>
<string name="test_v8_communication_speed">Test V8 Communication speed</string> <string name="test_v8_communication_speed">Test V8 Communication speed</string>

View file

@ -0,0 +1,24 @@
{
"name": "Testing",
"description": "Just for testing.",
"author": "FUTO",
"authorUrl": "https://futo.org",
"platformUrl": "https://odysee.com",
"sourceUrl": "https://plugins.grayjay.app/Test/TestConfig.json",
"repositoryUrl": "https://futo.org",
"scriptUrl": "./TestScript.js",
"version": 31,
"iconUrl": "./odysee.png",
"id": "1c05bfc3-08b9-42d0-93d3-6d52e0fd34d8",
"scriptSignature": "",
"scriptPublicKey": "",
"packages": ["Http"],
"allowEval": false,
"allowUrls": [],
"supportedClaimTypes": []
}

View file

@ -0,0 +1,45 @@
var config = {};
//Source Methods
source.enable = function(conf){
config = conf ?? {};
//log(config);
}
source.getHome = function() {
return new ContentPager([
source.getContentDetails("whatever")
]);
};
//Video
source.isContentDetailsUrl = function(url) {
return REGEX_DETAILS_URL.test(url)
};
source.getContentDetails = function(url) {
return new PlatformVideoDetails({
id: new PlatformID("Test", "Something", config.id),
name: "Test Video",
thumbnails: new Thumbnails([]),
author: new PlatformAuthorLink(new PlatformID("Test", "TestID", config.id),
"TestAuthor",
"None",
""),
datetime: parseInt(new Date().getTime() / 1000),
duration: 0,
viewCount: 0,
url: "",
isLive: false,
description: "",
rating: new RatingLikes(0),
video: new VideoSourceDescriptor([
new HLSSource({
name: "HLS",
url: "",
duration: 0,
priority: true
})
])
});
};
log("LOADED");

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB