mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
commit
aee5b75c2f
53 changed files with 2059 additions and 387 deletions
|
@ -5,6 +5,7 @@ plugins {
|
|||
id 'org.ajoberstar.grgit' version '1.7.2'
|
||||
id 'com.google.protobuf'
|
||||
id 'kotlin-parcelize'
|
||||
id 'kotlin-kapt'
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -38,7 +39,7 @@ protobuf {
|
|||
|
||||
android {
|
||||
namespace 'com.futo.platformplayer'
|
||||
compileSdk 33
|
||||
compileSdk 34
|
||||
flavorDimensions "buildType"
|
||||
productFlavors {
|
||||
stable {
|
||||
|
@ -194,6 +195,12 @@ dependencies {
|
|||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
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
|
||||
implementation 'com.stripe:stripe-android:20.28.3'
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ import android.webkit.CookieManager
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.activities.*
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.cache.ChannelContentCache
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||
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)
|
||||
fun clearChannelCache() {
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
||||
ChannelContentCache.instance.clear();
|
||||
StateCache.instance.clear();
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.futo.platformplayer
|
|||
|
||||
import android.content.Context
|
||||
import android.webkit.CookieManager
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
|
@ -12,25 +13,31 @@ import androidx.work.WorkManager
|
|||
import androidx.work.WorkerParameters
|
||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.futo.platformplayer.activities.DeveloperActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
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.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.background.BackgroundWorker
|
||||
import com.futo.platformplayer.cache.ChannelContentCache
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
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.stores.FragmentedStorage
|
||||
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.FormField
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -39,6 +46,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.*
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.stream.IntStream.range
|
||||
|
@ -82,26 +90,153 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||
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,
|
||||
R.string.crashes_the_application_on_purpose, 2)
|
||||
R.string.crashes_the_application_on_purpose, 3)
|
||||
fun crashMe() {
|
||||
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
|
||||
}
|
||||
|
||||
@FormField(R.string.delete_announcements, FieldForm.BUTTON,
|
||||
R.string.delete_all_announcements, 2)
|
||||
R.string.delete_all_announcements, 3)
|
||||
fun deleteAnnouncements() {
|
||||
StateAnnouncement.instance.deleteAllAnnouncements();
|
||||
}
|
||||
|
||||
@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() {
|
||||
val cookieManager: CookieManager = CookieManager.getInstance()
|
||||
cookieManager.removeAllCookies(null);
|
||||
}
|
||||
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
||||
R.string.test_background_worker_description, 3)
|
||||
R.string.test_background_worker_description, 4)
|
||||
fun triggerBackgroundUpdate() {
|
||||
val act = SettingsActivity.getActivity()!!;
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
||||
|
@ -113,10 +248,10 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||
wm.enqueue(req);
|
||||
}
|
||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
||||
R.string.test_background_worker_description, 3)
|
||||
R.string.test_background_worker_description, 4)
|
||||
fun clearChannelContentCache() {
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
|
||||
ChannelContentCache.instance.clearToday();
|
||||
StateCache.instance.clearToday();
|
||||
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
|
||||
override fun encode(): String {
|
||||
return Json.encodeToString(this);
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.widget.ImageButton
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.IField
|
||||
|
||||
class DeveloperActivity : AppCompatActivity() {
|
||||
private lateinit var _form: FieldForm;
|
||||
private lateinit var _buttonBack: ImageButton;
|
||||
|
||||
fun getField(id: String): IField? {
|
||||
return _form.findField(id);
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
DeveloperActivity._lastActivity = this;
|
||||
setContentView(R.layout.activity_dev);
|
||||
setNavigationBarColorAndIcons();
|
||||
|
||||
|
@ -33,4 +40,19 @@ class DeveloperActivity : AppCompatActivity() {
|
|||
super.finish()
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.post.IPlatformPost
|
||||
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.SerialName
|
||||
|
||||
@kotlinx.serialization.Serializable(with = PlatformContentSerializer::class)
|
||||
interface SerializedPlatformContent: IPlatformContent {
|
||||
override val contentType: ContentType;
|
||||
|
||||
fun toJson() : String;
|
||||
fun fromJson(str : String) : SerializedPlatformContent;
|
||||
fun fromJsonArray(str : String) : Array<SerializedPlatformContent>;
|
||||
|
|
|
@ -30,7 +30,7 @@ open class SerializedPlatformLockedContent(
|
|||
override val unlockUrl: String? = null,
|
||||
override val contentThumbnails: Thumbnails
|
||||
) : IPlatformLockedContent, SerializedPlatformContent {
|
||||
final override val contentType: ContentType get() = ContentType.LOCKED;
|
||||
override val contentType: ContentType = ContentType.LOCKED;
|
||||
|
||||
override fun toJson() : String {
|
||||
return Json.encodeToString(this);
|
||||
|
|
|
@ -30,7 +30,7 @@ open class SerializedPlatformNestedContent(
|
|||
override val contentProvider: String?,
|
||||
override val contentThumbnails: Thumbnails
|
||||
) : 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 contentSupported: Boolean get() = contentPlugin != null;
|
||||
|
|
|
@ -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.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.polycentric.core.combineHashCodes
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
@ -26,7 +27,7 @@ open class SerializedPlatformPost(
|
|||
override val thumbnails: List<Thumbnails?>,
|
||||
override val images: List<String>
|
||||
) : IPlatformPost, SerializedPlatformContent {
|
||||
final override val contentType: ContentType get() = ContentType.POST;
|
||||
override val contentType: ContentType = ContentType.POST;
|
||||
|
||||
override fun toJson() : String {
|
||||
return Json.encodeToString(this);
|
||||
|
|
|
@ -26,7 +26,7 @@ open class SerializedPlatformVideo(
|
|||
override val duration: Long,
|
||||
override val viewCount: Long,
|
||||
) : IPlatformVideo, SerializedPlatformContent {
|
||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||
override val contentType: ContentType = ContentType.MEDIA;
|
||||
|
||||
override val isLive: Boolean = false;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -122,7 +122,7 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
|
|||
//Only for testing notifications
|
||||
val 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 {
|
||||
contentNotifs.add(Pair(StateSubscriptions.instance.getSubscriptions().first(), it));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import com.futo.platformplayer.engine.internal.V8BindObject
|
|||
import com.futo.platformplayer.getOrThrow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.net.SocketTimeoutException
|
||||
import kotlin.streams.asSequence
|
||||
import kotlin.streams.toList
|
||||
|
||||
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);
|
||||
else
|
||||
return@map it.first.request(it.second.method, it.second.url, it.second.headers);
|
||||
}.toList();
|
||||
}
|
||||
.asSequence()
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.IReplacerPager
|
||||
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.Event2
|
||||
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.fragment.mainactivity.main.FeedView
|
||||
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.StateSubscriptions
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
|
@ -78,7 +78,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||
private val _taskLoadVideos = TaskHandler<IPlatformChannel, IPager<IPlatformContent>>({lifecycleScope}, {
|
||||
val livePager = getContentPager(it);
|
||||
return@TaskHandler if(_channel?.let { StateSubscriptions.instance.isSubscribed(it) } == true)
|
||||
ChannelContentCache.cachePagerResults(lifecycleScope, livePager);
|
||||
StateCache.cachePagerResults(lifecycleScope, livePager);
|
||||
else livePager;
|
||||
}).success { livePager ->
|
||||
setLoading(false);
|
||||
|
@ -106,7 +106,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||
}
|
||||
|
||||
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);
|
||||
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); };
|
||||
}.exception<Throwable> {
|
||||
|
|
|
@ -372,6 +372,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||
}
|
||||
|
||||
private fun loadPagerInternal(pager: TPager, cache: ItemCache<TResult>? = null) {
|
||||
Logger.i(TAG, "Setting new internal pager on feed");
|
||||
_cache = cache;
|
||||
|
||||
detachPagerEvents();
|
||||
|
@ -418,6 +419,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||
}
|
||||
}
|
||||
|
||||
var _lastNextPage = false;
|
||||
private fun loadNextPage() {
|
||||
synchronized(_pager_lock) {
|
||||
val pager: TPager = recyclerData.pager ?: return;
|
||||
|
@ -426,9 +428,14 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||
|
||||
//loadCachedPage();
|
||||
if (pager.hasMorePages()) {
|
||||
_lastNextPage = true;
|
||||
setLoading(true);
|
||||
_nextPageHandler.run(pager);
|
||||
}
|
||||
else if(_lastNextPage) {
|
||||
Logger.i(TAG, "End of page reached");
|
||||
_lastNextPage = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.core.widget.addTextChangedListener
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.views.others.TagsView
|
||||
|
@ -58,7 +59,7 @@ class HistoryFragment : MainFragment() {
|
|||
|
||||
tagsView.onClick.subscribe { timeMinutesToErase ->
|
||||
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));
|
||||
adapter.updateFilteredVideos();
|
||||
adapter.notifyDataSetChanged();
|
||||
|
|
|
@ -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.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.cache.ChannelContentCache
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.exceptions.RateLimitException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
|
@ -40,6 +40,8 @@ import kotlinx.serialization.Serializable
|
|||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlin.time.measureTime
|
||||
|
||||
class SubscriptionsFeedFragment : MainFragment() {
|
||||
override val isMainView : Boolean = true;
|
||||
|
@ -132,8 +134,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||
|
||||
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen)
|
||||
loadResults(false);
|
||||
else if(recyclerData.results.size == 0)
|
||||
else if(recyclerData.results.size == 0) {
|
||||
loadCache();
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
val announcementsView = _announcementsView;
|
||||
|
@ -306,12 +310,21 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||
|
||||
|
||||
private fun loadCache() {
|
||||
Logger.i(TAG, "Subscriptions load cache");
|
||||
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
||||
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);
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val cachePager: IPager<IPlatformContent>;
|
||||
Logger.i(TAG, "Subscriptions retrieving cache");
|
||||
val time = measureTimeMillis {
|
||||
cachePager = StateCache.instance.getSubscriptionCachePager();
|
||||
}
|
||||
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) {
|
||||
setLoading(true);
|
||||
|
|
|
@ -72,6 +72,7 @@ import com.futo.platformplayer.receivers.MediaControlReceiver
|
|||
import com.futo.platformplayer.states.*
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
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.behavior.TouchInterceptFrameLayout
|
||||
import com.futo.platformplayer.views.casting.CastView
|
||||
|
@ -122,6 +123,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
private set;
|
||||
var videoLocal: VideoLocal? = null;
|
||||
private var _playbackTracker: IPlaybackTracker? = null;
|
||||
private var _historyIndex: DBHistory.Index? = null;
|
||||
|
||||
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
|
||||
|
@ -1274,24 +1285,30 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
updateQueueState();
|
||||
|
||||
_historicalPosition = StatePlaylists.instance.updateHistoryPosition(video, false, (toResume.toFloat() / 1000.0f).toLong());
|
||||
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)}";
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val historyItem = getHistoryIndex(videoDetail);
|
||||
|
||||
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
delay(8000);
|
||||
withContext(Dispatchers.Main) {
|
||||
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||
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;
|
||||
_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 currentTime = System.currentTimeMillis();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.video.SerializedPlatformContent
|
||||
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 kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.json.*
|
||||
|
@ -22,7 +23,7 @@ class PlatformContentSerializer() : JsonContentPolymorphicSerializer<SerializedP
|
|||
"MEDIA" -> SerializedPlatformVideo.serializer();
|
||||
"NESTED_VIDEO" -> SerializedPlatformNestedContent.serializer();
|
||||
"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
|
||||
|
@ -30,7 +31,7 @@ class PlatformContentSerializer() : JsonContentPolymorphicSerializer<SerializedP
|
|||
ContentType.MEDIA.value -> SerializedPlatformVideo.serializer();
|
||||
ContentType.NESTED_VIDEO.value -> SerializedPlatformNestedContent.serializer();
|
||||
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}")
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
package com.futo.platformplayer.states
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.AudioManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.net.toUri
|
||||
import android.util.Xml
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -28,12 +23,11 @@ import com.futo.platformplayer.R
|
|||
import com.futo.platformplayer.activities.CaptchaActivity
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
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.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.background.BackgroundWorker
|
||||
import com.futo.platformplayer.cache.ChannelContentCache
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
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.LogLevel
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.HistoryVideo
|
||||
import com.futo.platformplayer.receivers.AudioNoisyReceiver
|
||||
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
||||
import com.futo.platformplayer.services.DownloadService
|
||||
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.stripe.android.core.utils.encodeToJson
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlin.time.measureTime
|
||||
|
||||
/***
|
||||
* This class contains global context for unconventional cases where obtaining context is hard.
|
||||
|
@ -66,20 +63,6 @@ import kotlin.time.measureTime
|
|||
class StateApp {
|
||||
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? {
|
||||
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||
if(isValidStorageUri(context, generalUri))
|
||||
|
@ -427,7 +410,7 @@ class StateApp {
|
|||
try {
|
||||
Logger.i(TAG, "MainApp Started: Initializing [ChannelContentCache]");
|
||||
val time = measureTimeMillis {
|
||||
ChannelContentCache.instance;
|
||||
StateCache.instance;
|
||||
}
|
||||
Logger.i(TAG, "ChannelContentCache initialized in ${time}ms");
|
||||
} catch (e: Throwable) {
|
||||
|
@ -565,7 +548,34 @@ class StateApp {
|
|||
StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
|
||||
StateAnnouncement.instance.registerDidYouKnow();
|
||||
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) {
|
||||
if(!Settings.instance.didFirstStart) {
|
||||
if(StateBackup.hasAutomaticBackup()) {
|
||||
|
|
201
app/src/main/java/com/futo/platformplayer/states/StateCache.kt
Normal file
201
app/src/main/java/com/futo/platformplayer/states/StateCache.kt
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
215
app/src/main/java/com/futo/platformplayer/states/StateHistory.kt
Normal file
215
app/src/main/java/com/futo/platformplayer/states/StateHistory.kt
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -117,7 +117,7 @@ class StateNotifications {
|
|||
.setContentText("${content.name}")
|
||||
.setSubText(content.datetime?.toHumanNowDiffStringMinDay())
|
||||
.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))
|
||||
.setChannelId(notificationChannel.id);
|
||||
if(thumbnail != null) {
|
||||
|
|
|
@ -42,6 +42,7 @@ import kotlinx.coroutines.*
|
|||
import okhttp3.internal.concat
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.reflect.jvm.internal.impl.builtins.jvm.JavaToKotlinClassMap.PlatformMutabilityMapping
|
||||
import kotlin.streams.asSequence
|
||||
import kotlin.streams.toList
|
||||
|
||||
/***
|
||||
|
@ -389,6 +390,7 @@ class StatePlatform {
|
|||
}
|
||||
return@map homeResult;
|
||||
}
|
||||
.asSequence()
|
||||
.toList()
|
||||
.associateWith { 1f };
|
||||
|
||||
|
@ -709,6 +711,7 @@ class StatePlatform {
|
|||
}
|
||||
return@map results;
|
||||
}
|
||||
.asSequence()
|
||||
.toList();
|
||||
|
||||
val pager = MultiChronoContentPager(pagers.toTypedArray());
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.futo.platformplayer.states
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
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.IPlatformVideoDetails
|
||||
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.Event2
|
||||
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.Playlist
|
||||
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.ReconstructStore
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
@ -26,6 +30,8 @@ import kotlinx.serialization.json.Json
|
|||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
|
||||
/***
|
||||
* Used to maintain playlists
|
||||
|
@ -39,26 +45,17 @@ class StatePlaylists {
|
|||
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
|
||||
})
|
||||
.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")
|
||||
.withRestore(PlaylistBackup())
|
||||
.load();
|
||||
|
||||
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
|
||||
|
||||
var onHistoricVideoChanged = Event2<IPlatformVideo, Long>();
|
||||
val onWatchLaterChanged = Event0();
|
||||
|
||||
fun toMigrateCheck(): List<ManagedStore<*>> {
|
||||
return listOf(playlistStore, _watchlistStore, _historyStore);
|
||||
return listOf(playlistStore, _watchlistStore);
|
||||
}
|
||||
|
||||
fun getWatchLater() : List<SerializedPlatformVideo> {
|
||||
synchronized(_watchlistStore) {
|
||||
return _watchlistStore.getItems();
|
||||
|
@ -99,6 +96,7 @@ class StatePlaylists {
|
|||
return playlistStore.findItem { it.id == id };
|
||||
}
|
||||
|
||||
|
||||
fun didPlay(playlistId: String) {
|
||||
val playlist = getPlaylist(playlistId);
|
||||
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 {
|
||||
val channel = StatePlatform.instance.getChannel(channelUrl).await();
|
||||
return createPlaylistFromChannel(channel, onPage);
|
||||
|
|
|
@ -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.structures.*
|
||||
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.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
|
@ -39,6 +38,7 @@ import java.util.concurrent.ForkJoinTask
|
|||
import kotlin.collections.ArrayList
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.streams.asSequence
|
||||
import kotlin.streams.toList
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
|
@ -259,7 +259,9 @@ class StateSubscriptions {
|
|||
Pair(it, StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id));
|
||||
else
|
||||
Pair(it, listOf(it.channel.url));
|
||||
}.toList().associate { it };
|
||||
}.asSequence()
|
||||
.toList()
|
||||
.associate { it };
|
||||
|
||||
val result = algo.getSubscriptions(subUrls);
|
||||
return Pair(result.pager, result.exceptions);
|
||||
|
|
|
@ -2,6 +2,8 @@ package com.futo.platformplayer.stores
|
|||
|
||||
import com.futo.platformplayer.Settings
|
||||
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.ManagedStore
|
||||
import com.futo.platformplayer.stores.v2.StoreSerializer
|
||||
|
|
|
@ -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)
|
|
@ -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);
|
|
@ -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);
|
||||
}*/
|
|
@ -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>;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -1,5 +1,29 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -1,5 +1,457 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.IPager
|
||||
import com.futo.platformplayer.api.media.structures.PlatformContentPager
|
||||
import com.futo.platformplayer.cache.ChannelContentCache
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
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 {
|
||||
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) }
|
||||
.map { it.value };
|
||||
.map { it.value };*/
|
||||
|
||||
/*
|
||||
val items = validStores.flatMap { it.getItems() }
|
||||
.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());
|
||||
}
|
||||
}
|
|
@ -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.IPager
|
||||
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.ScriptCaptchaRequiredException
|
||||
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.models.Subscription
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
|
@ -157,7 +157,7 @@ class SimpleSubscriptionAlgorithm(
|
|||
val time = measureTimeMillis {
|
||||
pager = StatePlatform.instance.getChannelContent(platformClient, url, true, threadPool.poolSize, toIgnore);
|
||||
|
||||
pager = ChannelContentCache.cachePagerResults(scope, pager!!) {
|
||||
pager = StateCache.cachePagerResults(scope, pager!!) {
|
||||
onNewCacheHit.emit(sub, it);
|
||||
};
|
||||
|
||||
|
@ -176,7 +176,7 @@ class SimpleSubscriptionAlgorithm(
|
|||
throw channelEx;
|
||||
else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.IPager
|
||||
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.ScriptCaptchaRequiredException
|
||||
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.models.Subscription
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
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 liveTasks = 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);
|
||||
}) 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);
|
||||
else {
|
||||
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;
|
||||
else {
|
||||
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;
|
||||
return@submit SubscriptionTaskResult(task, pager, taskEx);
|
||||
}
|
||||
|
|
47
app/src/main/java/com/futo/platformplayer/testing/DBTOs.kt
Normal file
47
app/src/main/java/com/futo/platformplayer/testing/DBTOs.kt
Normal 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();
|
||||
}
|
||||
}
|
|
@ -4,9 +4,15 @@ import android.view.LayoutInflater
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
|
||||
private lateinit var _filteredVideos: MutableList<HistoryVideo>;
|
||||
|
@ -17,17 +23,19 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
|
|||
constructor() : super() {
|
||||
updateFilteredVideos();
|
||||
|
||||
StatePlaylists.instance.onHistoricVideoChanged.subscribe(this) { video, position ->
|
||||
val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url };
|
||||
if (index == -1) {
|
||||
return@subscribe;
|
||||
}
|
||||
StateHistory.instance.onHistoricVideoChanged.subscribe(this) { video, position ->
|
||||
StateApp.instance.scope.launch(Dispatchers.Main) {
|
||||
val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url };
|
||||
if (index == -1) {
|
||||
return@launch;
|
||||
}
|
||||
|
||||
_filteredVideos[index].position = position;
|
||||
if (index < _filteredVideos.size - 2) {
|
||||
notifyItemRangeChanged(index, 2);
|
||||
} else {
|
||||
notifyItemChanged(index);
|
||||
_filteredVideos[index].position = position;
|
||||
if (index < _filteredVideos.size - 2) {
|
||||
notifyItemRangeChanged(index, 2);
|
||||
} else {
|
||||
notifyItemChanged(index);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -38,7 +46,10 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
|
|||
}
|
||||
|
||||
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()) {
|
||||
_filteredVideos = videos.toMutableList();
|
||||
} else {
|
||||
|
@ -49,7 +60,7 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
|
|||
}
|
||||
|
||||
fun cleanup() {
|
||||
StatePlaylists.instance.onHistoricVideoChanged.remove(this);
|
||||
StateHistory.instance.onHistoricVideoChanged.remove(this);
|
||||
}
|
||||
|
||||
override fun getItemCount() = _filteredVideos.size;
|
||||
|
@ -65,7 +76,7 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
|
|||
return@subscribe;
|
||||
}
|
||||
|
||||
StatePlaylists.instance.removeHistory(v.video.url);
|
||||
StateHistory.instance.removeHistory(v.video.url);
|
||||
_filteredVideos.removeAt(index);
|
||||
notifyItemRemoved(index);
|
||||
};
|
||||
|
|
|
@ -27,6 +27,7 @@ import com.futo.platformplayer.logging.Logger
|
|||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
|
@ -251,7 +252,7 @@ open class PreviewVideoView : LinearLayout {
|
|||
val timeBar = _timeBar
|
||||
if (timeBar != null) {
|
||||
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.progress = historyPosition.toFloat() / video.duration.toFloat()
|
||||
} else {
|
||||
|
|
|
@ -90,7 +90,11 @@ class GestureControlView : LinearLayout {
|
|||
override fun onDown(p0: MotionEvent): Boolean { return false; }
|
||||
override fun onShowPress(p0: MotionEvent) = Unit;
|
||||
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) {
|
||||
val adjustAmount = (distanceY * 2) / height;
|
||||
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
|
@ -129,8 +133,7 @@ class GestureControlView : LinearLayout {
|
|||
|
||||
return true;
|
||||
}
|
||||
override fun onLongPress(p0: MotionEvent) = Unit;
|
||||
override fun onFling(p0: MotionEvent, p1: MotionEvent, p2: Float, p3: Float): Boolean { return false; }
|
||||
override fun onLongPress(p0: MotionEvent) = Unit
|
||||
});
|
||||
|
||||
_gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
|
||||
|
|
|
@ -40,6 +40,8 @@ open class BigButton : LinearLayout {
|
|||
_root.apply {
|
||||
isClickable = true;
|
||||
setOnClickListener {
|
||||
if(!isEnabled)
|
||||
return@setOnClickListener;
|
||||
action();
|
||||
onClick.emit();
|
||||
};
|
||||
|
@ -54,6 +56,8 @@ open class BigButton : LinearLayout {
|
|||
_root.apply {
|
||||
isClickable = true;
|
||||
setOnClickListener {
|
||||
if(!isEnabled)
|
||||
return@setOnClickListener;
|
||||
onClick.emit();
|
||||
};
|
||||
}
|
||||
|
@ -144,4 +148,17 @@ open class BigButton : LinearLayout {
|
|||
|
||||
return this;
|
||||
}
|
||||
|
||||
fun setButtonEnabled(enabled: Boolean) {
|
||||
if(enabled) {
|
||||
alpha = 1f;
|
||||
isEnabled = true;
|
||||
isClickable = true;
|
||||
}
|
||||
else {
|
||||
alpha = 0.5f;
|
||||
isEnabled = false;
|
||||
isClickable = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,6 +57,8 @@ class ButtonField : BigButton, IField {
|
|||
};
|
||||
|
||||
super.onClick.subscribe {
|
||||
if(!isEnabled)
|
||||
return@subscribe;
|
||||
if(_method?.parameterCount == 1)
|
||||
_method?.invoke(_obj, context);
|
||||
else if(_method?.parameterCount == 2)
|
||||
|
|
|
@ -409,6 +409,7 @@
|
|||
<string name="version_code">Version Code</string>
|
||||
<string name="version_name">Version Name</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="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>
|
||||
|
@ -434,6 +435,7 @@
|
|||
<string name="developer_mode">Developer Mode</string>
|
||||
<string name="development_server">Development Server</string>
|
||||
<string name="experimental">Experimental</string>
|
||||
<string name="cache">Cache</string>
|
||||
<string name="fill_storage_till_error">Fill storage till error</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>
|
||||
|
@ -442,6 +444,8 @@
|
|||
<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="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="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>
|
||||
|
|
24
app/src/unstable/assets/sources/test/TestConfig.json
Normal file
24
app/src/unstable/assets/sources/test/TestConfig.json
Normal 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": []
|
||||
}
|
45
app/src/unstable/assets/sources/test/TestScript.js
Normal file
45
app/src/unstable/assets/sources/test/TestScript.js
Normal 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");
|
BIN
app/src/unstable/assets/sources/test/odysee.png
Normal file
BIN
app/src/unstable/assets/sources/test/odysee.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
Loading…
Add table
Reference in a new issue