Subscription group sync, playlist sync, send to device support mobile to desktop

This commit is contained in:
Kelvin 2024-11-07 16:36:49 +01:00
parent 790331e798
commit db7c09291f
11 changed files with 258 additions and 8 deletions

View File

@ -180,7 +180,7 @@ class SubscriptionGroupFragment : MainFragment() {
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${g.name}]?", null, 0, UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${g.name}]?", null, 0,
UIDialogs.Action("Cancel", {}), UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Delete", { UIDialogs.Action("Delete", {
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id); StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id, true);
_didDelete = true; _didDelete = true;
fragment.close(true); fragment.close(true);
}, UIDialogs.ActionStyle.DANGEROUS)) }, UIDialogs.ActionStyle.DANGEROUS))

View File

@ -60,7 +60,7 @@ class SubscriptionGroupListFragment : MainFragment() {
val loc = _subs.indexOf(group); val loc = _subs.indexOf(group);
_subs.remove(group); _subs.remove(group);
_list?.adapter?.notifyItemRangeRemoved(loc); _list?.adapter?.notifyItemRangeRemoved(loc);
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id); StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id, true);
}; };
it.onDragDrop.subscribe { it.onDragDrop.subscribe {
_touchHelper?.startDrag(it); _touchHelper?.startDrag(it);

View File

@ -111,9 +111,12 @@ import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
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.states.StateSync
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.stores.db.types.DBHistory
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
import com.futo.platformplayer.sync.models.SendToDevicePackage
import com.futo.platformplayer.toHumanBitrate import com.futo.platformplayer.toHumanBitrate
import com.futo.platformplayer.toHumanBytesSize import com.futo.platformplayer.toHumanBytesSize
import com.futo.platformplayer.toHumanNowDiffString import com.futo.platformplayer.toHumanNowDiffString
@ -637,6 +640,27 @@ class VideoDetailView : ConstraintLayout {
StatePlayer.instance.onVideoChanging.subscribe(this) { StatePlayer.instance.onVideoChanging.subscribe(this) {
setVideoOverview(it); setVideoOverview(it);
}; };
var hadDevice = false;
StateSync.instance.deviceUpdatedOrAdded.subscribe(this) { id, session ->
val hasDevice = StateSync.instance.hasAtLeastOneOnlineDevice();
if(hasDevice != hadDevice) {
hadDevice = hasDevice;
fragment.lifecycleScope.launch(Dispatchers.Main) {
updateMoreButtons();
}
}
};
StateSync.instance.deviceRemoved.subscribe(this) { id ->
val hasDevice = StateSync.instance.hasAtLeastOneOnlineDevice();
if(hasDevice != hadDevice) {
hadDevice = hasDevice;
fragment.lifecycleScope.launch(Dispatchers.Main) {
updateMoreButtons();
}
}
}
MediaControlReceiver.onLowerVolumeReceived.subscribe(this) { handleLowerVolume() }; MediaControlReceiver.onLowerVolumeReceived.subscribe(this) { handleLowerVolume() };
MediaControlReceiver.onPlayReceived.subscribe(this) { handlePlay() }; MediaControlReceiver.onPlayReceived.subscribe(this) { handlePlay() };
MediaControlReceiver.onPauseReceived.subscribe(this) { handlePause() }; MediaControlReceiver.onPauseReceived.subscribe(this) { handlePause() };
@ -878,6 +902,22 @@ class VideoDetailView : ConstraintLayout {
}; };
_slideUpOverlay?.hide(); _slideUpOverlay?.hide();
}, },
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
RoundButton(context, R.drawable.ic_device, context.getString(R.string.send_to_device), TAG_SEND_TO_DEVICE) {
val devices = StateSync.instance.getSessions();
val videoToSend = video ?: return@RoundButton;
if(devices.size > 1) {
//not implemented
}
else if(devices.size == 1){
val device = devices.first();
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non ${device.remotePublicKey}" , {
fragment.lifecycleScope.launch(Dispatchers.IO) {
device.sendJson(GJSyncOpcodes.sendToDevices, SendToDevicePackage(videoToSend.url, (lastPositionMilliseconds/1000).toInt()));
}
})
}
}} else null,
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") { RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
reloadVideo(); reloadVideo();
_slideUpOverlay?.hide(); _slideUpOverlay?.hide();
@ -1025,6 +1065,8 @@ class VideoDetailView : ConstraintLayout {
StateApp.instance.preventPictureInPicture.remove(this); StateApp.instance.preventPictureInPicture.remove(this);
StatePlayer.instance.onQueueChanged.remove(this); StatePlayer.instance.onQueueChanged.remove(this);
StatePlayer.instance.onVideoChanging.remove(this); StatePlayer.instance.onVideoChanging.remove(this);
StateSync.instance.deviceUpdatedOrAdded.remove(this);
StateSync.instance.deviceRemoved.remove(this);
MediaControlReceiver.onLowerVolumeReceived.remove(this); MediaControlReceiver.onLowerVolumeReceived.remove(this);
MediaControlReceiver.onPlayReceived.remove(this); MediaControlReceiver.onPlayReceived.remove(this);
MediaControlReceiver.onPauseReceived.remove(this); MediaControlReceiver.onPauseReceived.remove(this);
@ -2860,6 +2902,7 @@ class VideoDetailView : ConstraintLayout {
const val TAG_OVERLAY = "overlay"; const val TAG_OVERLAY = "overlay";
const val TAG_LIVECHAT = "livechat"; const val TAG_LIVECHAT = "livechat";
const val TAG_OPEN = "open"; const val TAG_OPEN = "open";
const val TAG_SEND_TO_DEVICE = "send_to_device";
const val TAG_MORE = "MORE"; const val TAG_MORE = "MORE";
private val _buttonPinStore = FragmentedStorage.get<StringArrayStorage>("videoPinnedButtons"); private val _buttonPinStore = FragmentedStorage.get<StringArrayStorage>("videoPinnedButtons");

View File

@ -1,5 +1,7 @@
package com.futo.platformplayer.models package com.futo.platformplayer.models
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import java.time.OffsetDateTime
import java.util.UUID import java.util.UUID
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@ -10,6 +12,11 @@ open class SubscriptionGroup {
var urls: MutableList<String> = mutableListOf(); var urls: MutableList<String> = mutableListOf();
var priority: Int = 99; var priority: Int = 99;
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
var lastChange : OffsetDateTime = OffsetDateTime.MIN;
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
var creationTime : OffsetDateTime = OffsetDateTime.now();
constructor(name: String) { constructor(name: String) {
this.name = name; this.name = name;
} }
@ -19,6 +26,8 @@ open class SubscriptionGroup {
this.image = parent.image; this.image = parent.image;
this.urls = parent.urls; this.urls = parent.urls;
this.priority = parent.priority; this.priority = parent.priority;
this.lastChange = parent.lastChange;
this.creationTime = parent.creationTime;
} }
class Selectable(parent: SubscriptionGroup, isSelected: Boolean = false): SubscriptionGroup(parent) { class Selectable(parent: SubscriptionGroup, isSelected: Boolean = false): SubscriptionGroup(parent) {

View File

@ -17,10 +17,18 @@ 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.ImportCache
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StateSubscriptionGroups.Companion
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.StringDateMapStorage
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 com.futo.platformplayer.sync.internal.GJSyncOpcodes
import com.futo.platformplayer.sync.models.SyncPlaylistsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
@ -45,6 +53,7 @@ class StatePlaylists {
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists") val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
.withRestore(PlaylistBackup()) .withRestore(PlaylistBackup())
.load(); .load();
private val _playlistRemoved = FragmentedStorage.get<StringDateMapStorage>("playlist_removed");
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares"); val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
@ -118,6 +127,9 @@ class StatePlaylists {
return playlistStore.findItem { it.id == id }; return playlistStore.findItem { it.id == id };
} }
fun getPlaylistRemovals(): Map<String, Long> {
return _playlistRemoved.all();
}
fun didPlay(playlistId: String) { fun didPlay(playlistId: String) {
val playlist = getPlaylist(playlistId); val playlist = getPlaylist(playlistId);
@ -148,13 +160,15 @@ class StatePlaylists {
createOrUpdatePlaylist(newPlaylist); createOrUpdatePlaylist(newPlaylist);
return newPlaylist; return newPlaylist;
} }
fun createOrUpdatePlaylist(playlist: Playlist) { fun createOrUpdatePlaylist(playlist: Playlist, isUserInteraction: Boolean = true) {
playlist.dateUpdate = OffsetDateTime.now(); playlist.dateUpdate = OffsetDateTime.now();
playlistStore.saveAsync(playlist, true); playlistStore.saveAsync(playlist, true);
if(playlist.id.isNotEmpty()) { if(playlist.id.isNotEmpty()) {
if (StateDownloads.instance.isPlaylistCached(playlist.id)) { if (StateDownloads.instance.isPlaylistCached(playlist.id)) {
StateDownloads.instance.checkForOutdatedPlaylistVideos(playlist.id); StateDownloads.instance.checkForOutdatedPlaylistVideos(playlist.id);
} }
if(isUserInteraction)
broadcastSyncPlaylist(playlist);
} }
} }
fun addToPlaylist(id: String, video: IPlatformVideo) { fun addToPlaylist(id: String, video: IPlatformVideo) {
@ -163,14 +177,41 @@ class StatePlaylists {
playlist.videos.add(SerializedPlatformVideo.fromVideo(video)); playlist.videos.add(SerializedPlatformVideo.fromVideo(video));
playlist.dateUpdate = OffsetDateTime.now(); playlist.dateUpdate = OffsetDateTime.now();
playlistStore.saveAsync(playlist, true); playlistStore.saveAsync(playlist, true);
broadcastSyncPlaylist(playlist);
} }
} }
fun removePlaylist(playlist: Playlist) { private fun broadcastSyncPlaylist(playlist: Playlist){
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
Logger.i(StateSubscriptionGroups.TAG, "SyncPlaylist (${playlist.name})");
StateSync.instance.broadcastJson(
GJSyncOpcodes.syncPlaylists,
SyncPlaylistsPackage(listOf(playlist), mapOf())
);
}
};
}
fun removePlaylist(playlist: Playlist, isUserInteraction: Boolean = true) {
playlistStore.delete(playlist); playlistStore.delete(playlist);
if(StateDownloads.instance.isPlaylistCached(playlist.id)) { if(StateDownloads.instance.isPlaylistCached(playlist.id)) {
StateDownloads.instance.deleteCachedPlaylist(playlist.id); StateDownloads.instance.deleteCachedPlaylist(playlist.id);
} }
if(isUserInteraction) {
_playlistRemoved.setAndSave(playlist.id, OffsetDateTime.now());
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
Logger.i(StateSubscriptionGroups.TAG, "SyncPlaylist (${playlist.name})");
StateSync.instance.broadcastJson(
GJSyncOpcodes.syncPlaylists,
SyncPlaylistsPackage(listOf(), mapOf(Pair(playlist.id, OffsetDateTime.now().toEpochSecond())))
);
}
};
}
} }
fun createPlaylistShareUri(context: Context, playlist: Playlist): Uri { fun createPlaylistShareUri(context: Context, playlist: Playlist): Uri {
@ -194,6 +235,16 @@ class StatePlaylists {
return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile); return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile);
} }
fun getSyncPlaylistsPackageString(): String{
return Json.encodeToString(
SyncPlaylistsPackage(
getPlaylists(),
getPlaylistRemovals()
)
);
}
companion object { companion object {
val TAG = "StatePlaylists"; val TAG = "StatePlaylists";
private var _instance : StatePlaylists? = null; private var _instance : StatePlaylists? = null;

View File

@ -25,13 +25,20 @@ 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
import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.states.StateHistory.Companion
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringDateMapStorage
import com.futo.platformplayer.stores.SubscriptionStorage import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ReconstructStore import com.futo.platformplayer.stores.v2.ReconstructStore
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
@ -51,6 +58,9 @@ class StateSubscriptionGroups {
.withUnique { it.id } .withUnique { it.id }
.load(); .load();
private val _groupsRemoved = FragmentedStorage.get<StringDateMapStorage>("group_removed");
val onGroupsChanged = Event0(); val onGroupsChanged = Event0();
fun getSubscriptionGroup(id: String): SubscriptionGroup? { fun getSubscriptionGroup(id: String): SubscriptionGroup? {
@ -59,20 +69,58 @@ class StateSubscriptionGroups {
fun getSubscriptionGroups(): List<SubscriptionGroup> { fun getSubscriptionGroups(): List<SubscriptionGroup> {
return _subGroups.getItems(); return _subGroups.getItems();
} }
fun updateSubscriptionGroup(subGroup: SubscriptionGroup, preventNotify: Boolean = false) { fun getSubscriptionGroupsRemovals(): Map<String, Long> {
return _groupsRemoved.all();
}
fun updateSubscriptionGroup(subGroup: SubscriptionGroup, preventNotify: Boolean = false, preventSync: Boolean = false) {
subGroup.lastChange = OffsetDateTime.now();
_subGroups.save(subGroup); _subGroups.save(subGroup);
if(!preventNotify) if(!preventNotify)
onGroupsChanged.emit(); onGroupsChanged.emit();
if(!preventSync) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
Logger.i(TAG, "SyncSubscriptionGroup (${subGroup.name})");
StateSync.instance.broadcastJson(
GJSyncOpcodes.syncSubscriptionGroups,
SyncSubscriptionGroupsPackage(listOf(subGroup), mapOf())
);
}
};
}
} }
fun deleteSubscriptionGroup(id: String){ fun deleteSubscriptionGroup(id: String, isUserInteraction: Boolean = true){
val group = getSubscriptionGroup(id); val group = getSubscriptionGroup(id);
if(group != null) { if(group != null) {
_subGroups.delete(group); _subGroups.delete(group);
onGroupsChanged.emit(); onGroupsChanged.emit();
if(isUserInteraction) {
_groupsRemoved.setAndSave(id, OffsetDateTime.now());
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
Logger.i(TAG, "SyncSubscriptionGroup delete (${group.name})");
StateSync.instance.broadcastJson(
GJSyncOpcodes.syncSubscriptionGroups,
SyncSubscriptionGroupsPackage(listOf(), mapOf(Pair(id, OffsetDateTime.now().toEpochSecond())))
);
}
};
}
} }
} }
fun getSyncSubscriptionGroupsPackageString(): String{
return Json.encodeToString(
SyncSubscriptionGroupsPackage(
getSubscriptionGroups(),
getSubscriptionGroupsRemovals()
)
);
}
companion object { companion object {
const val TAG = "StateSubscriptionGroups"; const val TAG = "StateSubscriptionGroups";
const val VERSION = 1; const val VERSION = 1;

View File

@ -11,5 +11,7 @@ class GJSyncOpcodes {
val syncSubscriptions: UByte = 202.toUByte(); val syncSubscriptions: UByte = 202.toUByte();
val syncHistory: UByte = 203.toUByte(); val syncHistory: UByte = 203.toUByte();
val syncSubscriptionGroups: UByte = 204.toUByte();
val syncPlaylists: UByte = 205.toUByte();
} }
} }

View File

@ -6,15 +6,20 @@ import com.futo.platformplayer.api.media.Serializer
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.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
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.states.StateHistory 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.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.states.StateSync import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.SyncSessionData import com.futo.platformplayer.sync.SyncSessionData
import com.futo.platformplayer.sync.internal.SyncSocketSession.Opcode import com.futo.platformplayer.sync.internal.SyncSocketSession.Opcode
import com.futo.platformplayer.sync.models.SendToDevicePackage import com.futo.platformplayer.sync.models.SendToDevicePackage
import com.futo.platformplayer.sync.models.SyncPlaylistsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -22,7 +27,9 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.time.Instant
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset
interface IAuthorizable { interface IAuthorizable {
val isAuthorized: Boolean val isAuthorized: Boolean
@ -158,6 +165,8 @@ class SyncSession : IAuthorizable {
send(GJSyncOpcodes.syncSubscriptions, StateSubscriptions.instance.getSyncSubscriptionsPackageString()); send(GJSyncOpcodes.syncSubscriptions, StateSubscriptions.instance.getSyncSubscriptionsPackageString());
send(GJSyncOpcodes.syncSubscriptionGroups, StateSubscriptionGroups.instance.getSyncSubscriptionGroupsPackageString());
send(GJSyncOpcodes.syncPlaylists, StatePlaylists.instance.getSyncPlaylistsPackageString())
val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory); val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory);
if(recentHistory.size > 0) if(recentHistory.size > 0)
@ -205,6 +214,67 @@ class SyncSession : IAuthorizable {
} }
} }
GJSyncOpcodes.syncSubscriptionGroups -> {
val dataBody = ByteArray(data.remaining());
data.get(dataBody);
val json = String(dataBody, Charsets.UTF_8);
val pack = Serializer.json.decodeFromString<SyncSubscriptionGroupsPackage>(json);
var lastSubgroupChange = OffsetDateTime.MIN;
for(group in pack.groups){
if(group.lastChange > lastSubgroupChange)
lastSubgroupChange = group.lastChange;
val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id);
if(existing == null)
StateSubscriptionGroups.instance.updateSubscriptionGroup(group, false, true);
else if(existing.lastChange < group.lastChange) {
existing.name = group.name;
existing.urls = group.urls;
existing.image = group.image;
existing.priority = group.priority;
existing.lastChange = group.lastChange;
StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true);
}
}
for(removal in pack.groupRemovals) {
val creation = StateSubscriptionGroups.instance.getSubscriptionGroup(removal.key);
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value, 0), ZoneOffset.UTC);
if(creation != null && creation.creationTime < removalTime)
StateSubscriptionGroups.instance.deleteSubscriptionGroup(removal.key, false);
}
}
GJSyncOpcodes.syncPlaylists -> {
val dataBody = ByteArray(data.remaining());
data.get(dataBody);
val json = String(dataBody, Charsets.UTF_8);
val pack = Serializer.json.decodeFromString<SyncPlaylistsPackage>(json);
for(playlist in pack.playlists) {
val existing = StatePlaylists.instance.getPlaylist(playlist.id);
if(existing == null)
StatePlaylists.instance.createOrUpdatePlaylist(playlist, false);
else if(existing.dateUpdate.toLocalDateTime() < playlist.dateUpdate.toLocalDateTime()) {
existing.dateUpdate = playlist.dateUpdate;
existing.name = playlist.name;
existing.videos = playlist.videos;
existing.dateCreation = playlist.dateCreation;
existing.datePlayed = playlist.datePlayed;
StatePlaylists.instance.createOrUpdatePlaylist(existing, false);
}
}
for(removal in pack.playlistRemovals) {
val creation = StatePlaylists.instance.getPlaylist(removal.key);
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value, 0), ZoneOffset.UTC);
if(creation != null && creation.dateCreation < removalTime)
StatePlaylists.instance.removePlaylist(creation, false);
}
}
GJSyncOpcodes.syncHistory -> { GJSyncOpcodes.syncHistory -> {
val dataBody = ByteArray(data.remaining()); val dataBody = ByteArray(data.remaining());
data.get(dataBody); data.get(dataBody);
@ -242,8 +312,7 @@ class SyncSession : IAuthorizable {
if(!StateSubscriptions.instance.isSubscribed(sub.channel)) { if(!StateSubscriptions.instance.isSubscribed(sub.channel)) {
val removalTime = StateSubscriptions.instance.getSubscriptionRemovalTime(sub.channel.url); val removalTime = StateSubscriptions.instance.getSubscriptionRemovalTime(sub.channel.url);
if(sub.creationTime > removalTime) { if(sub.creationTime > removalTime) {
val newSub = val newSub = StateSubscriptions.instance.addSubscription(sub.channel, sub.creationTime);
StateSubscriptions.instance.addSubscription(sub.channel, sub.creationTime);
added.add(newSub); added.add(newSub);
} }
} }

View File

@ -0,0 +1,14 @@
package com.futo.platformplayer.sync.models
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import kotlinx.serialization.Serializable
import java.time.OffsetDateTime
import java.util.Dictionary
@Serializable
class SyncPlaylistsPackage(
var playlists: List<Playlist>,
var playlistRemovals: Map<String, Long>
)

View File

@ -0,0 +1,13 @@
package com.futo.platformplayer.sync.models
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import kotlinx.serialization.Serializable
import java.time.OffsetDateTime
import java.util.Dictionary
@Serializable
class SyncSubscriptionGroupsPackage(
var groups: List<SubscriptionGroup>,
var groupRemovals: Map<String, Long>
)

View File

@ -608,6 +608,7 @@
<string name="do_you_want_to_convert_channel_channelname_to_a_playlist">Do you want to convert channel {channelName} to a playlist?</string> <string name="do_you_want_to_convert_channel_channelname_to_a_playlist">Do you want to convert channel {channelName} to a playlist?</string>
<string name="failed_to_convert_channel">Failed to convert channel</string> <string name="failed_to_convert_channel">Failed to convert channel</string>
<string name="page">Page</string> <string name="page">Page</string>
<string name="send_to_device">Sync Video</string>
<string name="hide">Hide</string> <string name="hide">Hide</string>
<string name="hide_from_home">Hide from Home</string> <string name="hide_from_home">Hide from Home</string>
<string name="hide_creator_from_home">Hide Creator from Home</string> <string name="hide_creator_from_home">Hide Creator from Home</string>