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

This commit is contained in:
Koen 2024-03-08 08:17:51 +01:00
commit 092b20041e
15 changed files with 155 additions and 40 deletions

View file

@ -13,6 +13,8 @@ import java.text.DecimalFormat
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.roundToLong
//Long //Long
@ -119,7 +121,8 @@ fun OffsetDateTime.getNowDiffMonths(): Long {
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now()); return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
} }
fun OffsetDateTime.getNowDiffYears(): Long { fun OffsetDateTime.getNowDiffYears(): Long {
return ChronoUnit.YEARS.between(this, OffsetDateTime.now()); val diff = ChronoUnit.MONTHS.between(this, OffsetDateTime.now()) / 12.0;
return diff.roundToLong();
} }
fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long { fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long {
@ -150,6 +153,7 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
if(value >= secondsInYear) { if(value >= secondsInYear) {
value = getNowDiffYears(); value = getNowDiffYears();
if(abs) value = abs(value); if(abs) value = abs(value);
value = Math.max(1, value);
unit = "year"; unit = "year";
} }
else if(value >= secondsInMonth) { else if(value >= secondsInMonth) {

View file

@ -34,6 +34,7 @@ import com.futo.platformplayer.dialogs.MigrateDialog
import com.futo.platformplayer.dialogs.ProgressDialog import com.futo.platformplayer.dialogs.ProgressDialog
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
@ -343,8 +344,8 @@ class UIDialogs {
} }
} }
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) { fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded); val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
registerDialogOpened(dialog); registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();

View file

@ -41,6 +41,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.listeners.OrientationManager import com.futo.platformplayer.listeners.OrientationManager
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.UrlVideoWithTime import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.* import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
@ -603,7 +604,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
UIDialogs.showSingleButtonDialog( UIDialogs.showSingleButtonDialog(
this, this,
R.drawable.ic_play, R.drawable.ic_play,
getString(R.string.unknown_content_format) + " [${url}]", getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
"Ok", "Ok",
{ }); { });
} }
@ -693,10 +694,22 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(!recon.trim().startsWith("[")) if(!recon.trim().startsWith("["))
return handleUnknownJson(recon); return handleUnknownJson(recon);
val reconLines = Json.decodeFromString<List<String>>(recon); var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null;
try {
if(cacheStr != null)
cache = Json.decodeFromString(cacheStr);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache");
}
recon = reconLines.joinToString("\n"); recon = reconLines.joinToString("\n");
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}"); Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon); handleReconstruction(recon, cache);
return true; return true;
} }
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") { else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
@ -711,12 +724,25 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
fun handleFile(file: String): Boolean { fun handleFile(file: String): Boolean {
Logger.i(TAG, "handleFile(url=$file)"); Logger.i(TAG, "handleFile(url=$file)");
if(file.lowercase().endsWith(".json")) { if(file.lowercase().endsWith(".json")) {
val recon = String(readSharedFile(file)); var recon = String(readSharedFile(file));
if(!recon.startsWith("[")) if(!recon.startsWith("["))
return handleUnknownJson(recon); return handleUnknownJson(recon);
var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null;
try {
if(cacheStr != null)
cache = Json.decodeFromString(cacheStr);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache");
}
recon = reconLines.joinToString("\n");
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}"); Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon); handleReconstruction(recon, cache);
return true; return true;
} }
else if(file.lowercase().endsWith(".zip")) { else if(file.lowercase().endsWith(".zip")) {
@ -728,7 +754,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
return false; return false;
} }
fun handleReconstruction(recon: String) { fun handleReconstruction(recon: String, cache: ImportCache? = null) {
val type = ManagedStore.getReconstructionIdentifier(recon); val type = ManagedStore.getReconstructionIdentifier(recon);
val store: ManagedStore<*> = when(type) { val store: ManagedStore<*> = when(type) {
"Playlist" -> StatePlaylists.instance.playlistStore "Playlist" -> StatePlaylists.instance.playlistStore
@ -745,7 +771,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(!type.isNullOrEmpty()) { if(!type.isNullOrEmpty()) {
UIDialogs.showImportDialog(this, store, name, listOf(recon)) { UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) {
} }
} }

View file

@ -37,6 +37,10 @@ class SerializedChannel(
TODO("Not yet implemented") TODO("Not yet implemented")
} }
fun isSameUrl(url: String): Boolean {
return this.url == url || urlAlternatives.contains(url);
}
companion object { companion object {
fun fromChannel(channel: IPlatformChannel): SerializedChannel { fun fromChannel(channel: IPlatformChannel): SerializedChannel {
return SerializedChannel( return SerializedChannel(

View file

@ -22,7 +22,9 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
import com.futo.platformplayer.assume import com.futo.platformplayer.assume
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -66,13 +68,15 @@ class ImportDialog : AlertDialog {
private val _name: String; private val _name: String;
private val _toImport: List<String>; private val _toImport: List<String>;
private val _cache: ImportCache?;
constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, onConcluded: ()->Unit): super(context) { constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, cache: ImportCache?, onConcluded: ()->Unit): super(context) {
_context = context; _context = context;
_store = importStore; _store = importStore;
_onConcluded = onConcluded; _onConcluded = onConcluded;
_name = name; _name = name;
_toImport = ArrayList(toReconstruct); _toImport = ArrayList(toReconstruct);
_cache = cache;
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -146,7 +150,7 @@ class ImportDialog : AlertDialog {
val scope = StateApp.instance.scopeOrNull; val scope = StateApp.instance.scopeOrNull;
scope?.launch(Dispatchers.IO) { scope?.launch(Dispatchers.IO) {
try { try {
val migrationResult = _store.importReconstructions(_toImport) { finished, total -> val migrationResult = _store.importReconstructions(_toImport, _cache) { finished, total ->
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
_textProgress.text = "${finished}/${total}"; _textProgress.text = "${finished}/${total}";
} }

View file

@ -30,7 +30,7 @@ class HistoryVideo {
} }
companion object { companion object {
fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo)? = null): HistoryVideo { fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo?)? = null): HistoryVideo {
var index = str.indexOf("|||"); var index = str.indexOf("|||");
if(index < 0) throw IllegalArgumentException("Invalid history string: " + str); if(index < 0) throw IllegalArgumentException("Invalid history string: " + str);
val url = str.substring(0, index); val url = str.substring(0, index);

View file

@ -0,0 +1,11 @@
package com.futo.platformplayer.models
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import kotlinx.serialization.Serializable
@Serializable
class ImportCache(
var videos: List<SerializedPlatformVideo>? = null,
var channels: List<SerializedChannel>? = null
);

View file

@ -10,6 +10,7 @@ import com.futo.platformplayer.UIDialogs
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.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.copyTo import com.futo.platformplayer.copyTo
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
@ -17,6 +18,7 @@ import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
import com.futo.platformplayer.getNowDiffHours import com.futo.platformplayer.getNowDiffHours
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.readBytes import com.futo.platformplayer.readBytes
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
@ -58,6 +60,19 @@ class StateBackup {
StatePlaylists.instance.toMigrateCheck() StatePlaylists.instance.toMigrateCheck()
).flatten(); ).flatten();
fun getCache(): ImportCache {
val allPlaylists = StatePlaylists.instance.getPlaylists();
val videos = allPlaylists.flatMap { it.videos }.distinctBy { it.url };
val allSubscriptions = StateSubscriptions.instance.getSubscriptions();
val channels = allSubscriptions.map { it.channel };
return ImportCache(
videos = videos,
channels = channels
);
}
private fun getAutomaticBackupPassword(customPassword: String? = null): String { private fun getAutomaticBackupPassword(customPassword: String? = null): String {
val password = customPassword ?: Settings.instance.backup.autoBackupPassword ?: ""; val password = customPassword ?: Settings.instance.backup.autoBackupPassword ?: "";
@ -233,11 +248,10 @@ class StateBackup {
.associateBy { it.config.id } .associateBy { it.config.id }
.mapValues { it.value.config.sourceUrl!! }; .mapValues { it.value.config.sourceUrl!! };
val cache = getCache();
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings, cache);
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings);
//export.videoCache = StatePlaylists.instance.getHistory()
// .distinctBy { it.video.url }
// .map { it.video };
return export; return export;
} }
@ -324,7 +338,7 @@ class StateBackup {
continue; continue;
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
UIDialogs.showImportDialog(context, relevantStore, store.key, store.value) { UIDialogs.showImportDialog(context, relevantStore, store.key, store.value, export.cache) {
synchronized(toAwait) { synchronized(toAwait) {
toAwait.remove(store.key); toAwait.remove(store.key);
if(toAwait.isEmpty()) if(toAwait.isEmpty())
@ -453,8 +467,8 @@ class StateBackup {
val stores: Map<String, List<String>>, val stores: Map<String, List<String>>,
val plugins: Map<String, String>, val plugins: Map<String, String>,
val pluginSettings: Map<String, Map<String, String?>>, val pluginSettings: Map<String, Map<String, String?>>,
var cache: ImportCache? = null
) { ) {
var videoCache: List<SerializedPlatformVideo>? = null;
fun asZip(): ByteArray { fun asZip(): ByteArray {
return ByteArrayOutputStream().use { byteStream -> return ByteArrayOutputStream().use { byteStream ->
@ -478,6 +492,17 @@ class StateBackup {
zipStream.putNextEntry(ZipEntry("plugin_settings")); zipStream.putNextEntry(ZipEntry("plugin_settings"));
zipStream.write(Json.encodeToString(pluginSettings).toByteArray()); zipStream.write(Json.encodeToString(pluginSettings).toByteArray());
if(cache != null) {
if(cache?.videos != null) {
zipStream.putNextEntry(ZipEntry("cache_videos"));
zipStream.write(Json.encodeToString(cache!!.videos).toByteArray());
}
if(cache?.channels != null) {
zipStream.putNextEntry(ZipEntry("cache_channels"));
zipStream.write(Json.encodeToString(cache!!.channels).toByteArray());
}
}
}; };
return byteStream.toByteArray(); return byteStream.toByteArray();
} }
@ -492,6 +517,8 @@ class StateBackup {
val stores: MutableMap<String, List<String>> = mutableMapOf(); val stores: MutableMap<String, List<String>> = mutableMapOf();
var plugins: Map<String, String> = mapOf(); var plugins: Map<String, String> = mapOf();
var pluginSettings: Map<String, Map<String, String?>> = mapOf(); var pluginSettings: Map<String, Map<String, String?>> = mapOf();
var videoCache: List<SerializedPlatformVideo>? = null
var channelCache: List<SerializedChannel>? = null
while (zipStream.nextEntry.also { entry = it } != null) { while (zipStream.nextEntry.also { entry = it } != null) {
if(entry!!.isDirectory) if(entry!!.isDirectory)
@ -503,6 +530,22 @@ class StateBackup {
"settings" -> settings = String(zipStream.readBytes()); "settings" -> settings = String(zipStream.readBytes());
"plugins" -> plugins = Json.decodeFromString(String(zipStream.readBytes())); "plugins" -> plugins = Json.decodeFromString(String(zipStream.readBytes()));
"plugin_settings" -> pluginSettings = Json.decodeFromString(String(zipStream.readBytes())); "plugin_settings" -> pluginSettings = Json.decodeFromString(String(zipStream.readBytes()));
"cache_videos" -> {
try {
videoCache = Json.decodeFromString(String(zipStream.readBytes()));
}
catch(ex: Exception) {
Logger.e(TAG, "Couldn't deserialize video cache", ex);
}
};
"cache_channels" -> {
try {
channelCache = Json.decodeFromString(String(zipStream.readBytes()));
}
catch(ex: Exception) {
Logger.e(TAG, "Couldn't deserialize channel cache", ex);
}
};
} }
else else
stores[entry!!.name.substring("stores/".length)] = Json.decodeFromString(String(zipStream.readBytes())); stores[entry!!.name.substring("stores/".length)] = Json.decodeFromString(String(zipStream.readBytes()));
@ -511,7 +554,10 @@ class StateBackup {
throw IllegalStateException("Failed to parse zip [${entry?.name}] due to ${ex.message}"); throw IllegalStateException("Failed to parse zip [${entry?.name}] due to ${ex.message}");
} }
} }
return ExportStructure(exportInfo, settings, stores, plugins, pluginSettings); return ExportStructure(exportInfo, settings, stores, plugins, pluginSettings, ImportCache(
videos = videoCache,
channels = channelCache
));
} }
} }
} }

View file

@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.models.ImportCache
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.ManagedDBStore
import com.futo.platformplayer.stores.db.types.DBHistory import com.futo.platformplayer.stores.db.types.DBHistory
@ -20,8 +21,8 @@ class StateHistory {
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history") private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
.withRestore(object: ReconstructStore<HistoryVideo>() { .withRestore(object: ReconstructStore<HistoryVideo>() {
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString(); override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): HistoryVideo override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, cache: ImportCache?): HistoryVideo
= HistoryVideo.fromReconString(backup, null); = HistoryVideo.fromReconString(backup) { url -> cache?.videos?.find { it.url == url } };
}) })
.load(); .load();

View file

@ -14,6 +14,7 @@ import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.ReconstructionException import com.futo.platformplayer.exceptions.ReconstructionException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
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.StringArrayStorage import com.futo.platformplayer.stores.StringArrayStorage
@ -32,8 +33,10 @@ class StatePlaylists {
.withUnique { it.url } .withUnique { it.url }
.withRestore(object: ReconstructStore<SerializedPlatformVideo>() { .withRestore(object: ReconstructStore<SerializedPlatformVideo>() {
override fun toReconstruction(obj: SerializedPlatformVideo): String = obj.url; override fun toReconstruction(obj: SerializedPlatformVideo): String = obj.url;
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): SerializedPlatformVideo override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): SerializedPlatformVideo
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails); = SerializedPlatformVideo.fromVideo(
importCache?.videos?.find { it.url == backup }?.let { Logger.i(TAG, "Reconstruction [${backup}] from cache"); return@let it; } ?:
StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
}) })
.load(); .load();
private val _watchlistOrderStore = FragmentedStorage.get<StringArrayStorage>("watchListOrder"); //Temporary workaround to add order.. private val _watchlistOrderStore = FragmentedStorage.get<StringArrayStorage>("watchListOrder"); //Temporary workaround to add order..
@ -154,7 +157,11 @@ class StatePlaylists {
val reconstruction = playlistStore.getReconstructionString(playlist, true); val reconstruction = playlistStore.getReconstructionString(playlist, true);
val newFile = File(playlistShareDir, playlist.name + ".json"); val newFile = File(playlistShareDir, playlist.name + ".json");
newFile.writeText(Json.encodeToString(reconstruction.split("\n")), Charsets.UTF_8); newFile.writeText(Json.encodeToString(reconstruction.split("\n") + listOf(
"__CACHE:" + Json.encodeToString(ImportCache(
videos = playlist.videos.toList()
))
)), Charsets.UTF_8);
return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile); return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile);
} }
@ -185,7 +192,7 @@ class StatePlaylists {
items.addAll(obj.videos.map { it.url }); items.addAll(obj.videos.map { it.url });
return items.map { it.replace("\n","") }.joinToString("\n"); return items.map { it.replace("\n","") }.joinToString("\n");
} }
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Playlist { override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Playlist {
val items = backup.split("\n"); val items = backup.split("\n");
if(items.size <= 0) { if(items.size <= 0) {
throw IllegalStateException("Cannot reconstructor playlist ${id}"); throw IllegalStateException("Cannot reconstructor playlist ${id}");
@ -194,10 +201,17 @@ class StatePlaylists {
val name = items[0]; val name = items[0];
val videos = items.drop(1).filter { it.isNotEmpty() }.map { val videos = items.drop(1).filter { it.isNotEmpty() }.map {
try { try {
val video = StatePlatform.instance.getContentDetails(it).await(); val videoUrl = it;
val video = importCache?.videos?.find { it.url == videoUrl } ?:
StatePlatform.instance.getContentDetails(it).await();
if (video is IPlatformVideoDetails) { if (video is IPlatformVideoDetails) {
return@map SerializedPlatformVideo.fromVideo(video); return@map SerializedPlatformVideo.fromVideo(video);
} else { }
else if(video is SerializedPlatformVideo) {
Logger.i(TAG, "Reconstruction [${it}] from cache");
return@map video;
}
else {
return@map null return@map null
} }
} }

View file

@ -12,6 +12,7 @@ import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.Event3 import com.futo.platformplayer.constructs.Event3
import com.futo.platformplayer.functional.CentralizedFeed import com.futo.platformplayer.functional.CentralizedFeed
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricCache
@ -38,8 +39,8 @@ class StateSubscriptions {
.withRestore(object: ReconstructStore<Subscription>(){ .withRestore(object: ReconstructStore<Subscription>(){
override fun toReconstruction(obj: Subscription): String = override fun toReconstruction(obj: Subscription): String =
obj.channel.url; obj.channel.url;
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Subscription = override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Subscription =
Subscription(SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false))); Subscription(importCache?.channels?.find { it.isSameUrl(backup) } ?: SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
}).load(); }).load();
private val _subscriptionOthers = FragmentedStorage.storeJson<Subscription>("subscriptions_others") private val _subscriptionOthers = FragmentedStorage.storeJson<Subscription>("subscriptions_others")
.withUnique { it.channel.url } .withUnique { it.channel.url }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.stores.v2
import com.futo.platformplayer.assume import com.futo.platformplayer.assume
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -105,7 +106,7 @@ class ManagedStore<T>{
_toReconstruct.clear(); _toReconstruct.clear();
} }
} }
suspend fun importReconstructions(items: List<String>, onProgress: ((Int, Int)->Unit)? = null): ReconstructionResult { suspend fun importReconstructions(items: List<String>, cache: ImportCache? = null, onProgress: ((Int, Int)->Unit)? = null): ReconstructionResult {
var successes = 0; var successes = 0;
val exs = ArrayList<Throwable>(); val exs = ArrayList<Throwable>();
@ -120,7 +121,7 @@ class ManagedStore<T>{
for (i in 0 .. 1) { for (i in 0 .. 1) {
try { try {
Logger.i(TAG, "Importing ${logName(recon)}"); Logger.i(TAG, "Importing ${logName(recon)}");
val reconId = createFromReconstruction(recon, builder); val reconId = createFromReconstruction(recon, builder, cache);
successes++; successes++;
Logger.i(TAG, "Imported ${logName(reconId)}"); Logger.i(TAG, "Imported ${logName(reconId)}");
break; break;
@ -272,12 +273,12 @@ class ManagedStore<T>{
save(obj, withReconstruction, onlyExisting); save(obj, withReconstruction, onlyExisting);
} }
suspend fun createFromReconstruction(reconstruction: String, builder: ReconstructStore.Builder): String { suspend fun createFromReconstruction(reconstruction: String, builder: ReconstructStore.Builder, cache: ImportCache? = null): String {
if(_reconstructStore == null) if(_reconstructStore == null)
throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type"); throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type");
val id = UUID.randomUUID().toString(); val id = UUID.randomUUID().toString();
val reconstruct = _reconstructStore!!.toObjectWithHeader(id, reconstruction, builder); val reconstruct = _reconstructStore!!.toObjectWithHeader(id, reconstruction, builder, cache);
save(reconstruct); save(reconstruct);
return id; return id;
} }

View file

@ -1,5 +1,7 @@
package com.futo.platformplayer.stores.v2 package com.futo.platformplayer.stores.v2
import com.futo.platformplayer.models.ImportCache
abstract class ReconstructStore<T> { abstract class ReconstructStore<T> {
open val backupOnSave: Boolean = false; open val backupOnSave: Boolean = false;
open val backupOnCreate: Boolean = true; open val backupOnCreate: Boolean = true;
@ -11,18 +13,18 @@ abstract class ReconstructStore<T> {
} }
abstract fun toReconstruction(obj: T): String; abstract fun toReconstruction(obj: T): String;
abstract suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): T; abstract suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache? = null): T;
fun toReconstructionWithHeader(obj: T, fallbackName: String): String { fun toReconstructionWithHeader(obj: T, fallbackName: String): String {
val identifier = identifierName ?: fallbackName; val identifier = identifierName ?: fallbackName;
return "@/${identifier}\n${toReconstruction(obj)}"; return "@/${identifier}\n${toReconstruction(obj)}";
} }
suspend fun toObjectWithHeader(id: String, backup: String, builder: Builder): T { suspend fun toObjectWithHeader(id: String, backup: String, builder: Builder, importCache: ImportCache? = null): T {
if(backup.startsWith("@/") && backup.contains("\n")) if(backup.startsWith("@/") && backup.contains("\n"))
return toObject(id, backup.substring(backup.indexOf("\n") + 1), builder); return toObject(id, backup.substring(backup.indexOf("\n") + 1), builder, importCache);
else else
return toObject(id, backup, builder); return toObject(id, backup, builder, importCache);
} }

@ -1 +1 @@
Subproject commit 83cccf8ba5ae87d8a437775d9b341a9e3be07e74 Subproject commit bef199baa9df5cb3192c7a3f8baf8c57e9fbdaea

@ -1 +1 @@
Subproject commit 83cccf8ba5ae87d8a437775d9b341a9e3be07e74 Subproject commit bef199baa9df5cb3192c7a3f8baf8c57e9fbdaea