mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-07 08:39:30 +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 'org.ajoberstar.grgit' version '1.7.2'
|
||||||
id 'com.google.protobuf'
|
id 'com.google.protobuf'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
|
id 'kotlin-kapt'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -38,7 +39,7 @@ protobuf {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.futo.platformplayer'
|
namespace 'com.futo.platformplayer'
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
flavorDimensions "buildType"
|
flavorDimensions "buildType"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
stable {
|
stable {
|
||||||
|
@ -194,6 +195,12 @@ dependencies {
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||||
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
||||||
|
|
||||||
|
//Database
|
||||||
|
implementation("androidx.room:room-runtime:2.6.0")
|
||||||
|
annotationProcessor("androidx.room:room-compiler:2.6.0")
|
||||||
|
kapt("androidx.room:room-compiler:2.6.0")
|
||||||
|
implementation("androidx.room:room-ktx:2.6.0")
|
||||||
|
|
||||||
//Payment
|
//Payment
|
||||||
implementation 'com.stripe:stripe-android:20.28.3'
|
implementation 'com.stripe:stripe-android:20.28.3'
|
||||||
|
|
||||||
|
|
|
@ -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 androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.activities.*
|
import com.futo.platformplayer.activities.*
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
@ -299,7 +298,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 14)
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 14)
|
||||||
fun clearChannelCache() {
|
fun clearChannelCache() {
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
||||||
ChannelContentCache.instance.clear();
|
StateCache.instance.clear();
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.futo.platformplayer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
@ -12,25 +13,31 @@ import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
|
import com.futo.platformplayer.activities.DeveloperActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.background.BackgroundWorker
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
|
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||||
|
import com.futo.platformplayer.views.fields.ButtonField
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -39,6 +46,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.stream.IntStream.range
|
import java.util.stream.IntStream.range
|
||||||
|
@ -82,26 +90,153 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||||
var backgroundSubscriptionFetching: Boolean = false;
|
var backgroundSubscriptionFetching: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.cache, FieldForm.GROUP, -1, 3)
|
||||||
|
val cache: Cache = Cache();
|
||||||
|
@Serializable
|
||||||
|
class Cache {
|
||||||
|
|
||||||
|
@FormField(R.string.subscriptions_cache_5000, FieldForm.BUTTON, -1, 1, "subscription_cache_button")
|
||||||
|
fun subscriptionsCache5000() {
|
||||||
|
Logger.i("SettingsDev", "Started caching 5000 sub items");
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Started caching 5000 sub items"
|
||||||
|
);
|
||||||
|
val button = DeveloperActivity.getActivity()?.getField("subscription_cache_button");
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(false);
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val subsCache =
|
||||||
|
StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(cacheScope = this)?.first;
|
||||||
|
|
||||||
|
var total = 0;
|
||||||
|
var page = 0;
|
||||||
|
var lastToast = System.currentTimeMillis();
|
||||||
|
while(subsCache!!.hasMorePages() && total < 5000) {
|
||||||
|
subsCache!!.nextPage();
|
||||||
|
total += subsCache!!.getResults().size;
|
||||||
|
page++;
|
||||||
|
|
||||||
|
if(page % 10 == 0)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
|
lastToast = System.currentTimeMillis();
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Thread.sleep(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SettingsDev", ex.message, ex);
|
||||||
|
Logger.i("SettingsDev", "Failed: ${ex.message}");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.history_cache_100, FieldForm.BUTTON, -1, 1, "history_cache_button")
|
||||||
|
fun historyCache100() {
|
||||||
|
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Started caching 100 history items (from home)"
|
||||||
|
);
|
||||||
|
val button = DeveloperActivity.getActivity()?.getField("history_cache_button");
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(false);
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val subsCache = StatePlatform.instance.getHome();
|
||||||
|
|
||||||
|
var num = 0;
|
||||||
|
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
|
||||||
|
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = 0;
|
||||||
|
var page = 0;
|
||||||
|
var lastToast = System.currentTimeMillis();
|
||||||
|
while(subsCache!!.hasMorePages() && total < 5000) {
|
||||||
|
subsCache!!.nextPage();
|
||||||
|
total += subsCache!!.getResults().size;
|
||||||
|
page++;
|
||||||
|
|
||||||
|
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
|
||||||
|
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(page % 4 == 0)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
|
lastToast = System.currentTimeMillis();
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Thread.sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SettingsDev", ex.message, ex);
|
||||||
|
Logger.i("SettingsDev", "Failed: ${ex.message}");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@FormField(R.string.crash_me, FieldForm.BUTTON,
|
@FormField(R.string.crash_me, FieldForm.BUTTON,
|
||||||
R.string.crashes_the_application_on_purpose, 2)
|
R.string.crashes_the_application_on_purpose, 3)
|
||||||
fun crashMe() {
|
fun crashMe() {
|
||||||
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
|
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.delete_announcements, FieldForm.BUTTON,
|
@FormField(R.string.delete_announcements, FieldForm.BUTTON,
|
||||||
R.string.delete_all_announcements, 2)
|
R.string.delete_all_announcements, 3)
|
||||||
fun deleteAnnouncements() {
|
fun deleteAnnouncements() {
|
||||||
StateAnnouncement.instance.deleteAllAnnouncements();
|
StateAnnouncement.instance.deleteAllAnnouncements();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.clear_cookies, FieldForm.BUTTON,
|
@FormField(R.string.clear_cookies, FieldForm.BUTTON,
|
||||||
R.string.clear_all_cookies_from_the_cookieManager, 2)
|
R.string.clear_all_cookies_from_the_cookieManager, 3)
|
||||||
fun clearCookies() {
|
fun clearCookies() {
|
||||||
val cookieManager: CookieManager = CookieManager.getInstance()
|
val cookieManager: CookieManager = CookieManager.getInstance()
|
||||||
cookieManager.removeAllCookies(null);
|
cookieManager.removeAllCookies(null);
|
||||||
}
|
}
|
||||||
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
||||||
R.string.test_background_worker_description, 3)
|
R.string.test_background_worker_description, 4)
|
||||||
fun triggerBackgroundUpdate() {
|
fun triggerBackgroundUpdate() {
|
||||||
val act = SettingsActivity.getActivity()!!;
|
val act = SettingsActivity.getActivity()!!;
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
||||||
|
@ -113,10 +248,10 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||||
wm.enqueue(req);
|
wm.enqueue(req);
|
||||||
}
|
}
|
||||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
||||||
R.string.test_background_worker_description, 3)
|
R.string.test_background_worker_description, 4)
|
||||||
fun clearChannelContentCache() {
|
fun clearChannelContentCache() {
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
|
||||||
ChannelContentCache.instance.clearToday();
|
StateCache.instance.clearToday();
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,6 +498,17 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
|
||||||
|
var info = Info();
|
||||||
|
@Serializable
|
||||||
|
class Info {
|
||||||
|
@FormField(R.string.dev_info_channel_cache_size, FieldForm.READONLYTEXT, -1, 1, "channelCacheSize")
|
||||||
|
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
|
||||||
|
}
|
||||||
|
|
||||||
//region BOILERPLATE
|
//region BOILERPLATE
|
||||||
override fun encode(): String {
|
override fun encode(): String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
|
import com.futo.platformplayer.views.fields.IField
|
||||||
|
|
||||||
class DeveloperActivity : AppCompatActivity() {
|
class DeveloperActivity : AppCompatActivity() {
|
||||||
private lateinit var _form: FieldForm;
|
private lateinit var _form: FieldForm;
|
||||||
private lateinit var _buttonBack: ImageButton;
|
private lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
|
fun getField(id: String): IField? {
|
||||||
|
return _form.findField(id);
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
DeveloperActivity._lastActivity = this;
|
||||||
setContentView(R.layout.activity_dev);
|
setContentView(R.layout.activity_dev);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
@ -33,4 +40,19 @@ class DeveloperActivity : AppCompatActivity() {
|
||||||
super.finish()
|
super.finish()
|
||||||
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
//TODO: Temporary for solving Settings issues
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private var _lastActivity: DeveloperActivity? = null;
|
||||||
|
|
||||||
|
fun getActivity(): DeveloperActivity? {
|
||||||
|
val act = _lastActivity;
|
||||||
|
if(act != null)
|
||||||
|
return act;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -6,9 +6,13 @@ import com.futo.platformplayer.api.media.models.locked.IPlatformLockedContent
|
||||||
import com.futo.platformplayer.api.media.models.nested.IPlatformNestedContent
|
import com.futo.platformplayer.api.media.models.nested.IPlatformNestedContent
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
||||||
|
import kotlinx.serialization.EncodeDefault
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable(with = PlatformContentSerializer::class)
|
@kotlinx.serialization.Serializable(with = PlatformContentSerializer::class)
|
||||||
interface SerializedPlatformContent: IPlatformContent {
|
interface SerializedPlatformContent: IPlatformContent {
|
||||||
|
override val contentType: ContentType;
|
||||||
|
|
||||||
fun toJson() : String;
|
fun toJson() : String;
|
||||||
fun fromJson(str : String) : SerializedPlatformContent;
|
fun fromJson(str : String) : SerializedPlatformContent;
|
||||||
fun fromJsonArray(str : String) : Array<SerializedPlatformContent>;
|
fun fromJsonArray(str : String) : Array<SerializedPlatformContent>;
|
||||||
|
|
|
@ -30,7 +30,7 @@ open class SerializedPlatformLockedContent(
|
||||||
override val unlockUrl: String? = null,
|
override val unlockUrl: String? = null,
|
||||||
override val contentThumbnails: Thumbnails
|
override val contentThumbnails: Thumbnails
|
||||||
) : IPlatformLockedContent, SerializedPlatformContent {
|
) : IPlatformLockedContent, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.LOCKED;
|
override val contentType: ContentType = ContentType.LOCKED;
|
||||||
|
|
||||||
override fun toJson() : String {
|
override fun toJson() : String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
|
|
|
@ -30,7 +30,7 @@ open class SerializedPlatformNestedContent(
|
||||||
override val contentProvider: String?,
|
override val contentProvider: String?,
|
||||||
override val contentThumbnails: Thumbnails
|
override val contentThumbnails: Thumbnails
|
||||||
) : IPlatformNestedContent, SerializedPlatformContent {
|
) : IPlatformNestedContent, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.NESTED_VIDEO;
|
final override val contentType: ContentType = ContentType.NESTED_VIDEO;
|
||||||
|
|
||||||
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
|
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
|
||||||
override val contentSupported: Boolean get() = contentPlugin != null;
|
override val contentSupported: Boolean get() = contentPlugin != null;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.polycentric.core.combineHashCodes
|
import com.futo.polycentric.core.combineHashCodes
|
||||||
|
import kotlinx.serialization.EncodeDefault
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
@ -26,7 +27,7 @@ open class SerializedPlatformPost(
|
||||||
override val thumbnails: List<Thumbnails?>,
|
override val thumbnails: List<Thumbnails?>,
|
||||||
override val images: List<String>
|
override val images: List<String>
|
||||||
) : IPlatformPost, SerializedPlatformContent {
|
) : IPlatformPost, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.POST;
|
override val contentType: ContentType = ContentType.POST;
|
||||||
|
|
||||||
override fun toJson() : String {
|
override fun toJson() : String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
|
|
|
@ -26,7 +26,7 @@ open class SerializedPlatformVideo(
|
||||||
override val duration: Long,
|
override val duration: Long,
|
||||||
override val viewCount: Long,
|
override val viewCount: Long,
|
||||||
) : IPlatformVideo, SerializedPlatformContent {
|
) : IPlatformVideo, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
override val contentType: ContentType = ContentType.MEDIA;
|
||||||
|
|
||||||
override val isLive: Boolean = false;
|
override val isLive: Boolean = false;
|
||||||
|
|
||||||
|
|
|
@ -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
|
//Only for testing notifications
|
||||||
val testNotifs = 0;
|
val testNotifs = 0;
|
||||||
if(contentNotifs.size == 0 && testNotifs > 0) {
|
if(contentNotifs.size == 0 && testNotifs > 0) {
|
||||||
results.first.getResults().filter { it is IPlatformVideo && it.datetime?.let { it < now } == true }
|
results.first.getResults().filter { it is IPlatformVideo }
|
||||||
.take(testNotifs).forEach {
|
.take(testNotifs).forEach {
|
||||||
contentNotifs.add(Pair(StateSubscriptions.instance.getSubscriptions().first(), it));
|
contentNotifs.add(Pair(StateSubscriptions.instance.getSubscriptions().first(), it));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 com.futo.platformplayer.getOrThrow
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
import kotlin.streams.asSequence
|
||||||
import kotlin.streams.toList
|
import kotlin.streams.toList
|
||||||
|
|
||||||
class PackageHttp: V8Package {
|
class PackageHttp: V8Package {
|
||||||
|
@ -171,7 +172,9 @@ class PackageHttp: V8Package {
|
||||||
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers);
|
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers);
|
||||||
else
|
else
|
||||||
return@map it.first.request(it.second.method, it.second.url, it.second.headers);
|
return@map it.first.request(it.second.method, it.second.url, it.second.headers);
|
||||||
}.toList();
|
}
|
||||||
|
.asSequence()
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.IRefreshPager
|
import com.futo.platformplayer.api.media.structures.IRefreshPager
|
||||||
import com.futo.platformplayer.api.media.structures.IReplacerPager
|
import com.futo.platformplayer.api.media.structures.IReplacerPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiPager
|
import com.futo.platformplayer.api.media.structures.MultiPager
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
@ -32,6 +31,7 @@ import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
@ -78,7 +78,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||||
private val _taskLoadVideos = TaskHandler<IPlatformChannel, IPager<IPlatformContent>>({lifecycleScope}, {
|
private val _taskLoadVideos = TaskHandler<IPlatformChannel, IPager<IPlatformContent>>({lifecycleScope}, {
|
||||||
val livePager = getContentPager(it);
|
val livePager = getContentPager(it);
|
||||||
return@TaskHandler if(_channel?.let { StateSubscriptions.instance.isSubscribed(it) } == true)
|
return@TaskHandler if(_channel?.let { StateSubscriptions.instance.isSubscribed(it) } == true)
|
||||||
ChannelContentCache.cachePagerResults(lifecycleScope, livePager);
|
StateCache.cachePagerResults(lifecycleScope, livePager);
|
||||||
else livePager;
|
else livePager;
|
||||||
}).success { livePager ->
|
}).success { livePager ->
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -106,7 +106,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
val posBefore = _results.size;
|
val posBefore = _results.size;
|
||||||
val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo };
|
val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
|
||||||
_results.addAll(toAdd);
|
_results.addAll(toAdd);
|
||||||
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); };
|
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); };
|
||||||
}.exception<Throwable> {
|
}.exception<Throwable> {
|
||||||
|
|
|
@ -372,6 +372,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPagerInternal(pager: TPager, cache: ItemCache<TResult>? = null) {
|
private fun loadPagerInternal(pager: TPager, cache: ItemCache<TResult>? = null) {
|
||||||
|
Logger.i(TAG, "Setting new internal pager on feed");
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
|
|
||||||
detachPagerEvents();
|
detachPagerEvents();
|
||||||
|
@ -418,6 +419,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _lastNextPage = false;
|
||||||
private fun loadNextPage() {
|
private fun loadNextPage() {
|
||||||
synchronized(_pager_lock) {
|
synchronized(_pager_lock) {
|
||||||
val pager: TPager = recyclerData.pager ?: return;
|
val pager: TPager = recyclerData.pager ?: return;
|
||||||
|
@ -426,9 +428,14 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||||
|
|
||||||
//loadCachedPage();
|
//loadCachedPage();
|
||||||
if (pager.hasMorePages()) {
|
if (pager.hasMorePages()) {
|
||||||
|
_lastNextPage = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
_nextPageHandler.run(pager);
|
_nextPageHandler.run(pager);
|
||||||
}
|
}
|
||||||
|
else if(_lastNextPage) {
|
||||||
|
Logger.i(TAG, "End of page reached");
|
||||||
|
_lastNextPage = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.views.others.TagsView
|
import com.futo.platformplayer.views.others.TagsView
|
||||||
|
@ -58,7 +59,7 @@ class HistoryFragment : MainFragment() {
|
||||||
|
|
||||||
tagsView.onClick.subscribe { timeMinutesToErase ->
|
tagsView.onClick.subscribe { timeMinutesToErase ->
|
||||||
UIDialogs.showConfirmationDialog(requireContext(), getString(R.string.are_you_sure_delete_historical), {
|
UIDialogs.showConfirmationDialog(requireContext(), getString(R.string.are_you_sure_delete_historical), {
|
||||||
StatePlaylists.instance.removeHistoryRange(timeMinutesToErase.second as Long);
|
StateHistory.instance.removeHistoryRange(timeMinutesToErase.second as Long);
|
||||||
UIDialogs.toast(view.context, timeMinutesToErase.first + " " + getString(R.string.removed));
|
UIDialogs.toast(view.context, timeMinutesToErase.first + " " + getString(R.string.removed));
|
||||||
adapter.updateFilteredVideos();
|
adapter.updateFilteredVideos();
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
|
|
|
@ -15,13 +15,13 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.exceptions.RateLimitException
|
import com.futo.platformplayer.exceptions.RateLimitException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
@ -40,6 +40,8 @@ import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
import kotlin.time.measureTime
|
||||||
|
|
||||||
class SubscriptionsFeedFragment : MainFragment() {
|
class SubscriptionsFeedFragment : MainFragment() {
|
||||||
override val isMainView : Boolean = true;
|
override val isMainView : Boolean = true;
|
||||||
|
@ -132,8 +134,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
|
|
||||||
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen)
|
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen)
|
||||||
loadResults(false);
|
loadResults(false);
|
||||||
else if(recyclerData.results.size == 0)
|
else if(recyclerData.results.size == 0) {
|
||||||
loadCache();
|
loadCache();
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val announcementsView = _announcementsView;
|
val announcementsView = _announcementsView;
|
||||||
|
@ -306,12 +310,21 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
|
|
||||||
|
|
||||||
private fun loadCache() {
|
private fun loadCache() {
|
||||||
Logger.i(TAG, "Subscriptions load cache");
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
val cachePager: IPager<IPlatformContent>;
|
||||||
val results = cachePager.getResults();
|
Logger.i(TAG, "Subscriptions retrieving cache");
|
||||||
Logger.i(TAG, "Subscriptions show cache (${results.size})");
|
val time = measureTimeMillis {
|
||||||
setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
cachePager = StateCache.instance.getSubscriptionCachePager();
|
||||||
setPager(cachePager);
|
}
|
||||||
|
Logger.i(TAG, "Subscriptions retrieved cache (${time}ms)");
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val results = cachePager.getResults();
|
||||||
|
Logger.i(TAG, "Subscriptions show cache (${results.size})");
|
||||||
|
setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
||||||
|
setPager(cachePager);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private fun loadResults(withRefetch: Boolean = false) {
|
private fun loadResults(withRefetch: Boolean = false) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
|
@ -72,6 +72,7 @@ import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.states.*
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
|
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||||
import com.futo.platformplayer.views.MonetizationView
|
import com.futo.platformplayer.views.MonetizationView
|
||||||
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
||||||
import com.futo.platformplayer.views.casting.CastView
|
import com.futo.platformplayer.views.casting.CastView
|
||||||
|
@ -122,6 +123,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
private set;
|
private set;
|
||||||
var videoLocal: VideoLocal? = null;
|
var videoLocal: VideoLocal? = null;
|
||||||
private var _playbackTracker: IPlaybackTracker? = null;
|
private var _playbackTracker: IPlaybackTracker? = null;
|
||||||
|
private var _historyIndex: DBHistory.Index? = null;
|
||||||
|
|
||||||
val currentUrl get() = video?.url ?: _searchVideo?.url ?: _url;
|
val currentUrl get() = video?.url ?: _searchVideo?.url ?: _url;
|
||||||
|
|
||||||
|
@ -769,6 +771,15 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
|
||||||
|
val current = _historyIndex;
|
||||||
|
if(current == null || current.url != video.url) {
|
||||||
|
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
|
||||||
|
_historyIndex = index;
|
||||||
|
return@withContext index;
|
||||||
|
}
|
||||||
|
return@withContext current;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//Lifecycle
|
//Lifecycle
|
||||||
|
@ -1274,24 +1285,30 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
updateQueueState();
|
updateQueueState();
|
||||||
|
|
||||||
_historicalPosition = StatePlaylists.instance.updateHistoryPosition(video, false, (toResume.toFloat() / 1000.0f).toLong());
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
|
val historyItem = getHistoryIndex(videoDetail);
|
||||||
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
|
|
||||||
_layoutResume.visibility = View.VISIBLE;
|
|
||||||
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
|
||||||
|
|
||||||
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
try {
|
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||||
delay(8000);
|
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
|
||||||
|
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
|
||||||
|
_layoutResume.visibility = View.VISIBLE;
|
||||||
|
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
||||||
|
|
||||||
|
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
delay(8000);
|
||||||
|
_layoutResume.visibility = View.GONE;
|
||||||
|
_textResume.text = "";
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to set resume changes.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
_layoutResume.visibility = View.GONE;
|
_layoutResume.visibility = View.GONE;
|
||||||
_textResume.text = "";
|
_textResume.text = "";
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to set resume changes.", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_layoutResume.visibility = View.GONE;
|
|
||||||
_textResume.text = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2087,7 +2104,10 @@ class VideoDetailView : ConstraintLayout {
|
||||||
val v = video ?: return;
|
val v = video ?: return;
|
||||||
val currentTime = System.currentTimeMillis();
|
val currentTime = System.currentTimeMillis();
|
||||||
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
||||||
StatePlaylists.instance.updateHistoryPosition(v, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val history = getHistoryIndex(v);
|
||||||
|
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||||
|
}
|
||||||
_lastPositionSaveTime = currentTime;
|
_lastPositionSaveTime = currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.futo.platformplayer.serializers
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformNestedContent
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformNestedContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformPost
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import kotlinx.serialization.DeserializationStrategy
|
import kotlinx.serialization.DeserializationStrategy
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
|
@ -22,7 +23,7 @@ class PlatformContentSerializer() : JsonContentPolymorphicSerializer<SerializedP
|
||||||
"MEDIA" -> SerializedPlatformVideo.serializer();
|
"MEDIA" -> SerializedPlatformVideo.serializer();
|
||||||
"NESTED_VIDEO" -> SerializedPlatformNestedContent.serializer();
|
"NESTED_VIDEO" -> SerializedPlatformNestedContent.serializer();
|
||||||
"ARTICLE" -> throw NotImplementedError("Articles not yet implemented");
|
"ARTICLE" -> throw NotImplementedError("Articles not yet implemented");
|
||||||
"POST" -> throw NotImplementedError("Post not yet implemented");
|
"POST" -> SerializedPlatformPost.serializer();
|
||||||
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.contentOrNull}")
|
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.contentOrNull}")
|
||||||
};
|
};
|
||||||
else
|
else
|
||||||
|
@ -30,7 +31,7 @@ class PlatformContentSerializer() : JsonContentPolymorphicSerializer<SerializedP
|
||||||
ContentType.MEDIA.value -> SerializedPlatformVideo.serializer();
|
ContentType.MEDIA.value -> SerializedPlatformVideo.serializer();
|
||||||
ContentType.NESTED_VIDEO.value -> SerializedPlatformNestedContent.serializer();
|
ContentType.NESTED_VIDEO.value -> SerializedPlatformNestedContent.serializer();
|
||||||
ContentType.ARTICLE.value -> throw NotImplementedError("Articles not yet implemented");
|
ContentType.ARTICLE.value -> throw NotImplementedError("Articles not yet implemented");
|
||||||
ContentType.POST.value -> throw NotImplementedError("Post not yet implemented");
|
ContentType.POST.value -> SerializedPlatformPost.serializer();
|
||||||
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.int}")
|
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.int}")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,19 @@
|
||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import androidx.activity.ComponentActivity
|
import android.util.Xml
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
@ -28,12 +23,11 @@ import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.activities.CaptchaActivity
|
import com.futo.platformplayer.activities.CaptchaActivity
|
||||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
|
||||||
import com.futo.platformplayer.background.BackgroundWorker
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
|
@ -43,20 +37,23 @@ import com.futo.platformplayer.logging.AndroidLogConsumer
|
||||||
import com.futo.platformplayer.logging.FileLogConsumer
|
import com.futo.platformplayer.logging.FileLogConsumer
|
||||||
import com.futo.platformplayer.logging.LogLevel
|
import com.futo.platformplayer.logging.LogLevel
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.HistoryVideo
|
||||||
import com.futo.platformplayer.receivers.AudioNoisyReceiver
|
import com.futo.platformplayer.receivers.AudioNoisyReceiver
|
||||||
|
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
||||||
import com.futo.platformplayer.services.DownloadService
|
import com.futo.platformplayer.services.DownloadService
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||||
|
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||||
|
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.stripe.android.core.utils.encodeToJson
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
import kotlin.time.measureTime
|
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* This class contains global context for unconventional cases where obtaining context is hard.
|
* This class contains global context for unconventional cases where obtaining context is hard.
|
||||||
|
@ -66,20 +63,6 @@ import kotlin.time.measureTime
|
||||||
class StateApp {
|
class StateApp {
|
||||||
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
|
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
|
||||||
|
|
||||||
/*
|
|
||||||
private val externalRootDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "Grayjay");
|
|
||||||
|
|
||||||
fun getExternalRootDirectory(): File? {
|
|
||||||
if(!externalRootDirectory.exists()) {
|
|
||||||
val result = externalRootDirectory.mkdirs();
|
|
||||||
if(!result)
|
|
||||||
return null;
|
|
||||||
return externalRootDirectory;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return externalRootDirectory;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
||||||
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||||
if(isValidStorageUri(context, generalUri))
|
if(isValidStorageUri(context, generalUri))
|
||||||
|
@ -427,7 +410,7 @@ class StateApp {
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "MainApp Started: Initializing [ChannelContentCache]");
|
Logger.i(TAG, "MainApp Started: Initializing [ChannelContentCache]");
|
||||||
val time = measureTimeMillis {
|
val time = measureTimeMillis {
|
||||||
ChannelContentCache.instance;
|
StateCache.instance;
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "ChannelContentCache initialized in ${time}ms");
|
Logger.i(TAG, "ChannelContentCache initialized in ${time}ms");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
@ -565,7 +548,34 @@ class StateApp {
|
||||||
StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
|
StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
|
||||||
StateAnnouncement.instance.registerDidYouKnow();
|
StateAnnouncement.instance.registerDidYouKnow();
|
||||||
Logger.i(TAG, "MainApp Started: Finished");
|
Logger.i(TAG, "MainApp Started: Finished");
|
||||||
|
|
||||||
|
StatePlaylists.instance.toMigrateCheck();
|
||||||
|
|
||||||
|
if(StateHistory.instance.shouldMigrateLegacyHistory())
|
||||||
|
StateHistory.instance.migrateLegacyHistory();
|
||||||
|
|
||||||
|
|
||||||
|
if(false) {
|
||||||
|
/*
|
||||||
|
Logger.i(TAG, "TEST:--------(200)---------");
|
||||||
|
testHistoryDB(200);
|
||||||
|
Logger.i(TAG, "TEST:--------(1000)---------");
|
||||||
|
testHistoryDB(1000);
|
||||||
|
Logger.i(TAG, "TEST:--------(2000)---------");
|
||||||
|
testHistoryDB(2000);
|
||||||
|
Logger.i(TAG, "TEST:--------(4000)---------");
|
||||||
|
testHistoryDB(4000);
|
||||||
|
Logger.i(TAG, "TEST:--------(6000)---------");
|
||||||
|
testHistoryDB(6000);
|
||||||
|
Logger.i(TAG, "TEST:--------(100000)---------");
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
StateHistory.instance.testHistoryDB(100000);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mainAppStartedWithExternalFiles(context: Context) {
|
fun mainAppStartedWithExternalFiles(context: Context) {
|
||||||
if(!Settings.instance.didFirstStart) {
|
if(!Settings.instance.didFirstStart) {
|
||||||
if(StateBackup.hasAutomaticBackup()) {
|
if(StateBackup.hasAutomaticBackup()) {
|
||||||
|
|
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}")
|
.setContentText("${content.name}")
|
||||||
.setSubText(content.datetime?.toHumanNowDiffStringMinDay())
|
.setSubText(content.datetime?.toHumanNowDiffStringMinDay())
|
||||||
.setSilent(true)
|
.setSilent(true)
|
||||||
.setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.getVideoIntent(context, content.url),
|
.setContentIntent(PendingIntent.getActivity(context, content.hashCode(), MainActivity.getVideoIntent(context, content.url),
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
||||||
.setChannelId(notificationChannel.id);
|
.setChannelId(notificationChannel.id);
|
||||||
if(thumbnail != null) {
|
if(thumbnail != null) {
|
||||||
|
|
|
@ -42,6 +42,7 @@ import kotlinx.coroutines.*
|
||||||
import okhttp3.internal.concat
|
import okhttp3.internal.concat
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.reflect.jvm.internal.impl.builtins.jvm.JavaToKotlinClassMap.PlatformMutabilityMapping
|
import kotlin.reflect.jvm.internal.impl.builtins.jvm.JavaToKotlinClassMap.PlatformMutabilityMapping
|
||||||
|
import kotlin.streams.asSequence
|
||||||
import kotlin.streams.toList
|
import kotlin.streams.toList
|
||||||
|
|
||||||
/***
|
/***
|
||||||
|
@ -389,6 +390,7 @@ class StatePlatform {
|
||||||
}
|
}
|
||||||
return@map homeResult;
|
return@map homeResult;
|
||||||
}
|
}
|
||||||
|
.asSequence()
|
||||||
.toList()
|
.toList()
|
||||||
.associateWith { 1f };
|
.associateWith { 1f };
|
||||||
|
|
||||||
|
@ -709,6 +711,7 @@ class StatePlatform {
|
||||||
}
|
}
|
||||||
return@map results;
|
return@map results;
|
||||||
}
|
}
|
||||||
|
.asSequence()
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
val pager = MultiChronoContentPager(pagers.toTypedArray());
|
val pager = MultiChronoContentPager(pagers.toTypedArray());
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.futo.platformplayer.states
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
|
@ -11,6 +12,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
|
@ -19,6 +21,8 @@ import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.HistoryVideo
|
import com.futo.platformplayer.models.HistoryVideo
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||||
|
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
@ -26,6 +30,8 @@ import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ConcurrentMap
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Used to maintain playlists
|
* Used to maintain playlists
|
||||||
|
@ -39,26 +45,17 @@ class StatePlaylists {
|
||||||
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
|
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
|
||||||
})
|
})
|
||||||
.load();
|
.load();
|
||||||
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
|
|
||||||
.withRestore(object: ReconstructStore<HistoryVideo>() {
|
|
||||||
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
|
|
||||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): HistoryVideo
|
|
||||||
= HistoryVideo.fromReconString(backup, null);
|
|
||||||
})
|
|
||||||
.load();
|
|
||||||
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
|
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
|
||||||
.withRestore(PlaylistBackup())
|
.withRestore(PlaylistBackup())
|
||||||
.load();
|
.load();
|
||||||
|
|
||||||
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
|
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
|
||||||
|
|
||||||
var onHistoricVideoChanged = Event2<IPlatformVideo, Long>();
|
|
||||||
val onWatchLaterChanged = Event0();
|
val onWatchLaterChanged = Event0();
|
||||||
|
|
||||||
fun toMigrateCheck(): List<ManagedStore<*>> {
|
fun toMigrateCheck(): List<ManagedStore<*>> {
|
||||||
return listOf(playlistStore, _watchlistStore, _historyStore);
|
return listOf(playlistStore, _watchlistStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getWatchLater() : List<SerializedPlatformVideo> {
|
fun getWatchLater() : List<SerializedPlatformVideo> {
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
return _watchlistStore.getItems();
|
return _watchlistStore.getItems();
|
||||||
|
@ -99,6 +96,7 @@ class StatePlaylists {
|
||||||
return playlistStore.findItem { it.id == id };
|
return playlistStore.findItem { it.id == id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun didPlay(playlistId: String) {
|
fun didPlay(playlistId: String) {
|
||||||
val playlist = getPlaylist(playlistId);
|
val playlist = getPlaylist(playlistId);
|
||||||
if(playlist != null) {
|
if(playlist != null) {
|
||||||
|
@ -107,66 +105,6 @@ class StatePlaylists {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHistoryPosition(url: String): Long {
|
|
||||||
val histVideo = _historyStore.findItem { it.video.url == url };
|
|
||||||
if(histVideo != null)
|
|
||||||
return histVideo.position;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
fun updateHistoryPosition(video: IPlatformVideo, updateExisting: Boolean, position: Long = -1L): Long {
|
|
||||||
val pos = if(position < 0) 0 else position;
|
|
||||||
val historyVideo = _historyStore.findItem { it.video.url == video.url };
|
|
||||||
if (historyVideo != null) {
|
|
||||||
val positionBefore = historyVideo.position;
|
|
||||||
if (updateExisting) {
|
|
||||||
var shouldUpdate = false;
|
|
||||||
if (positionBefore < 30) {
|
|
||||||
shouldUpdate = true;
|
|
||||||
} else {
|
|
||||||
if (position > 30) {
|
|
||||||
shouldUpdate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldUpdate) {
|
|
||||||
|
|
||||||
//A unrecovered item
|
|
||||||
if(historyVideo.video.author.id.value == null && historyVideo.video.duration == 0L)
|
|
||||||
historyVideo.video = SerializedPlatformVideo.fromVideo(video);
|
|
||||||
|
|
||||||
historyVideo.position = pos;
|
|
||||||
historyVideo.date = OffsetDateTime.now();
|
|
||||||
_historyStore.saveAsync(historyVideo);
|
|
||||||
onHistoricVideoChanged.emit(video, pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return positionBefore;
|
|
||||||
} else {
|
|
||||||
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), pos, OffsetDateTime.now());
|
|
||||||
_historyStore.saveAsync(newHistItem);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHistory() : List<HistoryVideo> {
|
|
||||||
return _historyStore.getItems().sortedByDescending { it.date };
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeHistory(url: String) {
|
|
||||||
val hist = _historyStore.findItem { it.video.url == url };
|
|
||||||
if(hist != null)
|
|
||||||
_historyStore.delete(hist);
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeHistoryRange(minutesToDelete: Long) {
|
|
||||||
val now = OffsetDateTime.now();
|
|
||||||
val toDelete = _historyStore.findItems { minutesToDelete == -1L || ChronoUnit.MINUTES.between(it.date, now) < minutesToDelete };
|
|
||||||
|
|
||||||
for(item in toDelete)
|
|
||||||
_historyStore.delete(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun createPlaylistFromChannel(channelUrl: String, onPage: (Int) -> Unit): Playlist {
|
suspend fun createPlaylistFromChannel(channelUrl: String, onPage: (Int) -> Unit): Playlist {
|
||||||
val channel = StatePlatform.instance.getChannel(channelUrl).await();
|
val channel = StatePlatform.instance.getChannel(channelUrl).await();
|
||||||
return createPlaylistFromChannel(channel, onPage);
|
return createPlaylistFromChannel(channel, onPage);
|
||||||
|
|
|
@ -10,7 +10,6 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.*
|
import com.futo.platformplayer.api.media.structures.*
|
||||||
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
@ -39,6 +38,7 @@ import java.util.concurrent.ForkJoinTask
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
import kotlin.streams.asSequence
|
||||||
import kotlin.streams.toList
|
import kotlin.streams.toList
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
|
@ -259,7 +259,9 @@ class StateSubscriptions {
|
||||||
Pair(it, StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id));
|
Pair(it, StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id));
|
||||||
else
|
else
|
||||||
Pair(it, listOf(it.channel.url));
|
Pair(it, listOf(it.channel.url));
|
||||||
}.toList().associate { it };
|
}.asSequence()
|
||||||
|
.toList()
|
||||||
|
.associate { it };
|
||||||
|
|
||||||
val result = algo.getSubscriptions(subUrls);
|
val result = algo.getSubscriptions(subUrls);
|
||||||
return Pair(result.pager, result.exceptions);
|
return Pair(result.pager, result.exceptions);
|
||||||
|
|
|
@ -2,6 +2,8 @@ package com.futo.platformplayer.stores
|
||||||
|
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.stores.db.ManagedDBIndex
|
||||||
|
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||||
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.stores.v2.StoreSerializer
|
import com.futo.platformplayer.stores.v2.StoreSerializer
|
||||||
|
|
|
@ -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
|
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
|
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.DedupContentPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.PlatformContentPager
|
import com.futo.platformplayer.api.media.structures.PlatformContentPager
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.toSafeFileName
|
import com.futo.platformplayer.toSafeFileName
|
||||||
|
@ -27,13 +27,16 @@ class CachedSubscriptionAlgorithm(pageSize: Int = 150, scope: CoroutineScope, al
|
||||||
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
||||||
val validSubIds = subs.flatMap { it.value } .map { it.toSafeFileName() }.toHashSet();
|
val validSubIds = subs.flatMap { it.value } .map { it.toSafeFileName() }.toHashSet();
|
||||||
|
|
||||||
val validStores = ChannelContentCache.instance._channelContents
|
/*
|
||||||
|
val validStores = StateCache.instance._channelContents
|
||||||
.filter { validSubIds.contains(it.key) }
|
.filter { validSubIds.contains(it.key) }
|
||||||
.map { it.value };
|
.map { it.value };*/
|
||||||
|
|
||||||
|
/*
|
||||||
val items = validStores.flatMap { it.getItems() }
|
val items = validStores.flatMap { it.getItems() }
|
||||||
.sortedByDescending { it.datetime };
|
.sortedByDescending { it.datetime };
|
||||||
|
*/
|
||||||
|
|
||||||
return Result(DedupContentPager(PlatformContentPager(items, Math.min(_pageSize, items.size)), StatePlatform.instance.getEnabledClients().map { it.id }), listOf());
|
return Result(DedupContentPager(StateCache.instance.getChannelCachePager(subs.flatMap { it.value }.distinct()), StatePlatform.instance.getEnabledClients().map { it.id }), listOf());
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,7 +8,6 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||||
|
@ -17,6 +16,7 @@ import com.futo.platformplayer.findNonRuntimeException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
@ -157,7 +157,7 @@ class SimpleSubscriptionAlgorithm(
|
||||||
val time = measureTimeMillis {
|
val time = measureTimeMillis {
|
||||||
pager = StatePlatform.instance.getChannelContent(platformClient, url, true, threadPool.poolSize, toIgnore);
|
pager = StatePlatform.instance.getChannelContent(platformClient, url, true, threadPool.poolSize, toIgnore);
|
||||||
|
|
||||||
pager = ChannelContentCache.cachePagerResults(scope, pager!!) {
|
pager = StateCache.cachePagerResults(scope, pager!!) {
|
||||||
onNewCacheHit.emit(sub, it);
|
onNewCacheHit.emit(sub, it);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ class SimpleSubscriptionAlgorithm(
|
||||||
throw channelEx;
|
throw channelEx;
|
||||||
else {
|
else {
|
||||||
Logger.i(StateSubscriptions.TAG, "Channel ${sub.channel.name} failed, substituting with cache");
|
Logger.i(StateSubscriptions.TAG, "Channel ${sub.channel.name} failed, substituting with cache");
|
||||||
pager = ChannelContentCache.instance.getChannelCachePager(sub.channel.url);
|
pager = StateCache.instance.getChannelCachePager(sub.channel.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import com.futo.platformplayer.api.media.structures.DedupContentPager
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||||
|
@ -21,6 +20,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragm
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -108,7 +108,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||||
val sub = if(!entry.value.isEmpty()) entry.value[0].task.sub else null;
|
val sub = if(!entry.value.isEmpty()) entry.value[0].task.sub else null;
|
||||||
val liveTasks = entry.value.filter { !it.task.fromCache };
|
val liveTasks = entry.value.filter { !it.task.fromCache };
|
||||||
val cachedTasks = entry.value.filter { it.task.fromCache };
|
val cachedTasks = entry.value.filter { it.task.fromCache };
|
||||||
val livePager = if(!liveTasks.isEmpty()) ChannelContentCache.cachePagerResults(scope, MultiChronoContentPager(liveTasks.map { it.pager!! }, true).apply { this.initialize() }, {
|
val livePager = if(!liveTasks.isEmpty()) StateCache.cachePagerResults(scope, MultiChronoContentPager(liveTasks.map { it.pager!! }, true).apply { this.initialize() }, {
|
||||||
onNewCacheHit.emit(sub!!, it);
|
onNewCacheHit.emit(sub!!, it);
|
||||||
}) else null;
|
}) else null;
|
||||||
val cachedPager = if(!cachedTasks.isEmpty()) MultiChronoContentPager(cachedTasks.map { it.pager!! }, true).apply { this.initialize() } else null;
|
val cachedPager = if(!cachedTasks.isEmpty()) MultiChronoContentPager(cachedTasks.map { it.pager!! }, true).apply { this.initialize() } else null;
|
||||||
|
@ -142,7 +142,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||||
return@submit SubscriptionTaskResult(task, null, null);
|
return@submit SubscriptionTaskResult(task, null, null);
|
||||||
else {
|
else {
|
||||||
cachedChannels.add(task.url);
|
cachedChannels.add(task.url);
|
||||||
return@submit SubscriptionTaskResult(task, ChannelContentCache.instance.getChannelCachePager(task.url), null);
|
return@submit SubscriptionTaskResult(task, StateCache.instance.getChannelCachePager(task.url), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,7 +197,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||||
throw channelEx;
|
throw channelEx;
|
||||||
else {
|
else {
|
||||||
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
|
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
|
||||||
pager = ChannelContentCache.instance.getChannelCachePager(task.sub.channel.url);
|
pager = StateCache.instance.getChannelCachePager(task.sub.channel.url);
|
||||||
taskEx = ex;
|
taskEx = ex;
|
||||||
return@submit SubscriptionTaskResult(task, pager, taskEx);
|
return@submit SubscriptionTaskResult(task, pager, taskEx);
|
||||||
}
|
}
|
||||||
|
|
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 android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.models.HistoryVideo
|
import com.futo.platformplayer.models.HistoryVideo
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
|
class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
|
||||||
private lateinit var _filteredVideos: MutableList<HistoryVideo>;
|
private lateinit var _filteredVideos: MutableList<HistoryVideo>;
|
||||||
|
@ -17,17 +23,19 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
|
||||||
constructor() : super() {
|
constructor() : super() {
|
||||||
updateFilteredVideos();
|
updateFilteredVideos();
|
||||||
|
|
||||||
StatePlaylists.instance.onHistoricVideoChanged.subscribe(this) { video, position ->
|
StateHistory.instance.onHistoricVideoChanged.subscribe(this) { video, position ->
|
||||||
val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url };
|
StateApp.instance.scope.launch(Dispatchers.Main) {
|
||||||
if (index == -1) {
|
val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url };
|
||||||
return@subscribe;
|
if (index == -1) {
|
||||||
}
|
return@launch;
|
||||||
|
}
|
||||||
|
|
||||||
_filteredVideos[index].position = position;
|
_filteredVideos[index].position = position;
|
||||||
if (index < _filteredVideos.size - 2) {
|
if (index < _filteredVideos.size - 2) {
|
||||||
notifyItemRangeChanged(index, 2);
|
notifyItemRangeChanged(index, 2);
|
||||||
} else {
|
} else {
|
||||||
notifyItemChanged(index);
|
notifyItemChanged(index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -38,7 +46,10 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFilteredVideos() {
|
fun updateFilteredVideos() {
|
||||||
val videos = StatePlaylists.instance.getHistory();
|
val videos = StateHistory.instance.getHistory();
|
||||||
|
val pager = StateHistory.instance.getHistoryPager();
|
||||||
|
//filtered val pager = StateHistory.instance.getHistorySearchPager("querrryyyyy");
|
||||||
|
|
||||||
if (_query.isBlank()) {
|
if (_query.isBlank()) {
|
||||||
_filteredVideos = videos.toMutableList();
|
_filteredVideos = videos.toMutableList();
|
||||||
} else {
|
} else {
|
||||||
|
@ -49,7 +60,7 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
StatePlaylists.instance.onHistoricVideoChanged.remove(this);
|
StateHistory.instance.onHistoricVideoChanged.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = _filteredVideos.size;
|
override fun getItemCount() = _filteredVideos.size;
|
||||||
|
@ -65,7 +76,7 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePlaylists.instance.removeHistory(v.video.url);
|
StateHistory.instance.removeHistory(v.video.url);
|
||||||
_filteredVideos.removeAt(index);
|
_filteredVideos.removeAt(index);
|
||||||
notifyItemRemoved(index);
|
notifyItemRemoved(index);
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.video.PlayerManager
|
import com.futo.platformplayer.video.PlayerManager
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
@ -251,7 +252,7 @@ open class PreviewVideoView : LinearLayout {
|
||||||
val timeBar = _timeBar
|
val timeBar = _timeBar
|
||||||
if (timeBar != null) {
|
if (timeBar != null) {
|
||||||
if (shouldShowTimeBar) {
|
if (shouldShowTimeBar) {
|
||||||
val historyPosition = StatePlaylists.instance.getHistoryPosition(video.url)
|
val historyPosition = StateHistory.instance.getHistoryPosition(video.url)
|
||||||
timeBar.visibility = if (historyPosition > 0) VISIBLE else GONE
|
timeBar.visibility = if (historyPosition > 0) VISIBLE else GONE
|
||||||
timeBar.progress = historyPosition.toFloat() / video.duration.toFloat()
|
timeBar.progress = historyPosition.toFloat() / video.duration.toFloat()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -90,7 +90,11 @@ class GestureControlView : LinearLayout {
|
||||||
override fun onDown(p0: MotionEvent): Boolean { return false; }
|
override fun onDown(p0: MotionEvent): Boolean { return false; }
|
||||||
override fun onShowPress(p0: MotionEvent) = Unit;
|
override fun onShowPress(p0: MotionEvent) = Unit;
|
||||||
override fun onSingleTapUp(p0: MotionEvent): Boolean { return false; }
|
override fun onSingleTapUp(p0: MotionEvent): Boolean { return false; }
|
||||||
override fun onScroll(p0: MotionEvent, p1: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
override fun onFling(p0: MotionEvent?, p1: MotionEvent, p2: Float, p3: Float): Boolean { return false; }
|
||||||
|
override fun onScroll(p0: MotionEvent?, p1: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
||||||
|
if(p0 == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (_isFullScreen && _adjustingBrightness) {
|
if (_isFullScreen && _adjustingBrightness) {
|
||||||
val adjustAmount = (distanceY * 2) / height;
|
val adjustAmount = (distanceY * 2) / height;
|
||||||
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||||
|
@ -129,8 +133,7 @@ class GestureControlView : LinearLayout {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
override fun onLongPress(p0: MotionEvent) = Unit;
|
override fun onLongPress(p0: MotionEvent) = Unit
|
||||||
override fun onFling(p0: MotionEvent, p1: MotionEvent, p2: Float, p3: Float): Boolean { return false; }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
_gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
|
_gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
|
||||||
|
|
|
@ -40,6 +40,8 @@ open class BigButton : LinearLayout {
|
||||||
_root.apply {
|
_root.apply {
|
||||||
isClickable = true;
|
isClickable = true;
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
|
if(!isEnabled)
|
||||||
|
return@setOnClickListener;
|
||||||
action();
|
action();
|
||||||
onClick.emit();
|
onClick.emit();
|
||||||
};
|
};
|
||||||
|
@ -54,6 +56,8 @@ open class BigButton : LinearLayout {
|
||||||
_root.apply {
|
_root.apply {
|
||||||
isClickable = true;
|
isClickable = true;
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
|
if(!isEnabled)
|
||||||
|
return@setOnClickListener;
|
||||||
onClick.emit();
|
onClick.emit();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -144,4 +148,17 @@ open class BigButton : LinearLayout {
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setButtonEnabled(enabled: Boolean) {
|
||||||
|
if(enabled) {
|
||||||
|
alpha = 1f;
|
||||||
|
isEnabled = true;
|
||||||
|
isClickable = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alpha = 0.5f;
|
||||||
|
isEnabled = false;
|
||||||
|
isClickable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -57,6 +57,8 @@ class ButtonField : BigButton, IField {
|
||||||
};
|
};
|
||||||
|
|
||||||
super.onClick.subscribe {
|
super.onClick.subscribe {
|
||||||
|
if(!isEnabled)
|
||||||
|
return@subscribe;
|
||||||
if(_method?.parameterCount == 1)
|
if(_method?.parameterCount == 1)
|
||||||
_method?.invoke(_obj, context);
|
_method?.invoke(_obj, context);
|
||||||
else if(_method?.parameterCount == 2)
|
else if(_method?.parameterCount == 2)
|
||||||
|
|
|
@ -409,6 +409,7 @@
|
||||||
<string name="version_code">Version Code</string>
|
<string name="version_code">Version Code</string>
|
||||||
<string name="version_name">Version Name</string>
|
<string name="version_name">Version Name</string>
|
||||||
<string name="version_type">Version Type</string>
|
<string name="version_type">Version Type</string>
|
||||||
|
<string name="dev_info_channel_cache_size">Channel Cache Size (Startup)</string>
|
||||||
<string name="when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code">When watching a video in preview mode, resume at the position when opening the video code</string>
|
<string name="when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code">When watching a video in preview mode, resume at the position when opening the video code</string>
|
||||||
<string name="please_enable_logging_to_submit_logs">Please enable logging to submit logs</string>
|
<string name="please_enable_logging_to_submit_logs">Please enable logging to submit logs</string>
|
||||||
<string name="embedded_plugins_reinstalled_a_reboot_is_recommended">Embedded plugins reinstalled, a reboot is recommended</string>
|
<string name="embedded_plugins_reinstalled_a_reboot_is_recommended">Embedded plugins reinstalled, a reboot is recommended</string>
|
||||||
|
@ -434,6 +435,7 @@
|
||||||
<string name="developer_mode">Developer Mode</string>
|
<string name="developer_mode">Developer Mode</string>
|
||||||
<string name="development_server">Development Server</string>
|
<string name="development_server">Development Server</string>
|
||||||
<string name="experimental">Experimental</string>
|
<string name="experimental">Experimental</string>
|
||||||
|
<string name="cache">Cache</string>
|
||||||
<string name="fill_storage_till_error">Fill storage till error</string>
|
<string name="fill_storage_till_error">Fill storage till error</string>
|
||||||
<string name="inject">Inject</string>
|
<string name="inject">Inject</string>
|
||||||
<string name="injects_a_test_source_config_local_into_v8">Injects a test source config (local) into V8</string>
|
<string name="injects_a_test_source_config_local_into_v8">Injects a test source config (local) into V8</string>
|
||||||
|
@ -442,6 +444,8 @@
|
||||||
<string name="removes_all_subscriptions">Removes all subscriptions</string>
|
<string name="removes_all_subscriptions">Removes all subscriptions</string>
|
||||||
<string name="settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities">Settings related to development server, be careful as it may open your phone to security vulnerabilities</string>
|
<string name="settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities">Settings related to development server, be careful as it may open your phone to security vulnerabilities</string>
|
||||||
<string name="start_server">Start Server</string>
|
<string name="start_server">Start Server</string>
|
||||||
|
<string name="subscriptions_cache_5000">Subscriptions Cache 5000</string>
|
||||||
|
<string name="history_cache_100">History Cache 100</string>
|
||||||
<string name="start_server_on_boot">Start Server on boot</string>
|
<string name="start_server_on_boot">Start Server on boot</string>
|
||||||
<string name="starts_a_devServer_on_port_11337_may_expose_vulnerabilities">Starts a DevServer on port 11337, may expose vulnerabilities.</string>
|
<string name="starts_a_devServer_on_port_11337_may_expose_vulnerabilities">Starts a DevServer on port 11337, may expose vulnerabilities.</string>
|
||||||
<string name="test_v8_communication_speed">Test V8 Communication speed</string>
|
<string name="test_v8_communication_speed">Test V8 Communication speed</string>
|
||||||
|
|
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
Add a link
Reference in a new issue