Worked on Syncing Logic

Created Syncer Generic Class to handle Monitoring, and Syncing between
local database, and remote REST server.
Temporarily changed Program.cs from an Avalonia UI app, to a Console App
for testing and debugging purposes.
This commit is contained in:
Mario Steele 2025-07-21 17:12:24 -05:00
parent 44a89ad589
commit 24fae2b7ac
10 changed files with 162 additions and 109 deletions

View file

@ -41,6 +41,7 @@ public class DBSyncWatcher
var data = File.ReadAllText(e.FullPath);
foreach (var line in data.Split('\n'))
{
if (line == "") continue;
var type = WatchFiles[dbName];
var item = JsonSerializer.Deserialize(line, type);
if (item == null) continue;

View file

@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class History
public class History : IDataModel
{
public string _id { get; set; } = string.Empty;
public string videoId { get; set; } = string.Empty;
@ -20,4 +20,7 @@ public class History
public string type { get; set; } = string.Empty;
public string lastViewedPlaylistType { get; set; } = string.Empty;
public string? lastViewedPlaylistItemId { get; set; }
public string Id() => _id;
public bool EqualId(string oid) => _id == oid;
}

View file

@ -0,0 +1,7 @@
namespace FreeTubeSyncer.Models.DatabaseModels;
public interface IDataModel
{
string Id();
bool EqualId(string oid);
}

View file

@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class Playlist
public class Playlist : IDataModel
{
public string _id { get; set; } = string.Empty;
public string playlistName { get; set; } = string.Empty;
@ -12,4 +12,7 @@ public class Playlist
public List<Video> videos { get; set; } = [];
public long createdAt { get; set; }
public long lastUpdatedAt { get; set; }
public string Id() => _id;
public bool EqualId(string oid) => _id == oid;
}

View file

@ -4,11 +4,14 @@ using System.Diagnostics.CodeAnalysis;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class Profile
public class Profile : IDataModel
{
public string _id { get; set; } = string.Empty;
public string name { get; set; } = string.Empty;
public string bgColor { get; set; } = string.Empty;
public string textColor { get; set; } = string.Empty;
public List<Subscription> subscriptions { get; set; } = [];
public string Id() => _id;
public bool EqualId(string oid) => _id == oid;
}

View file

@ -3,8 +3,11 @@ using System.Diagnostics.CodeAnalysis;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class SearchHistory
public class SearchHistory : IDataModel
{
public string _id { get; set; } = string.Empty;
public long lastUpdatedAt { get; set; }
public string Id() => _id;
public bool EqualId(string oid) => _id == oid;
}

View file

@ -4,7 +4,7 @@ using System.Text.Json;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class Setting
public class Setting : IDataModel
{
#pragma warning disable CS8618
public string _id { get; set; } = string.Empty;
@ -18,4 +18,7 @@ public class Setting
#pragma warning restore CS8603
set => ValueJson = JsonSerializer.Serialize(value);
}
public string Id() => _id;
public bool EqualId(string oid) => _id == oid;
}

View file

@ -1,5 +1,14 @@
using Avalonia;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using FreeTubeSyncer.Library;
using FreeTubeSyncer.Models.DatabaseModels;
using FreeTubeSyncer.REST;
namespace FreeTubeSyncer;
@ -9,8 +18,43 @@ class Program
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
public static void Main(string[] args)
{
var path = "/home/eumario/.var/app/io.freetubeapp.FreeTube/config/FreeTube/";
var dbWatcher = new DBSyncWatcher(path);
var historySyncer = new Syncer<History>(dbWatcher, Path.Join(path, "history.db"), "history.db", "/history");
var playlistSyncer = new Syncer<Playlist>(dbWatcher, Path.Join(path, "playlists.db"), "playlists.db", "/playlist");
var profileSyncer = new Syncer<Profile>(dbWatcher, Path.Join(path, "profiles.db"), "profiles.db", "/profile");
var searchHistorySyncer = new Syncer<SearchHistory>(dbWatcher, Path.Join(path, "search-history.db"), "search-history.db", "/search");
var settingsSyncer = new Syncer<Setting>(dbWatcher, Path.Join(path, "settings.db"), "settings.db", "/settings");
Task.Run(() => CheckCanSync(historySyncer, playlistSyncer, profileSyncer, searchHistorySyncer, settingsSyncer));
Console.WriteLine("Watching databases... Press return to exit...");
Console.ReadLine();
}
private static void CheckCanSync(Syncer<History>? historySyncer = null, Syncer<Playlist>? playlistSyncer = null,
Syncer<Profile>? profileSyncer = null, Syncer<SearchHistory>? searchHistorySyncer = null,
Syncer<Setting>? settingsSyncer = null)
{
while (true)
{
Thread.Sleep(100);
if (Process.GetProcessesByName("FreeTube").Length > 0) continue;
if (historySyncer is { IsDirty: true })
historySyncer.Sync();
if (playlistSyncer is { IsDirty: true })
playlistSyncer.Sync();
if (profileSyncer is { IsDirty: true})
profileSyncer.Sync();
if (searchHistorySyncer is { IsDirty: true})
searchHistorySyncer.Sync();
if (settingsSyncer is { IsDirty: true})
settingsSyncer.Sync();
}
}
// public static void Main(string[] args) => BuildAvaloniaApp()
// .StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()

View file

@ -1,102 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using FreeTubeSyncer.Library;
using FreeTubeSyncer.Models.DatabaseModels;
using RestSharp;
namespace FreeTubeSyncer.REST;
public class RestSync
{
private List<History> _history = [];
private List<Playlist> _playlist = [];
private List<Profile> _profile = [];
private List<SearchHistory> _searchHistory = [];
private List<Setting> _setting = [];
private bool _dirtyHistory = false;
private bool _dirtyPlaylist = false;
private bool _dirtyProfile = false;
private bool _dirtySearchHistory = false;
private bool _dirtySetting = false;
private RestClient _client;
public RestSync(DBSyncWatcher watcher)
{
watcher.OnDatabaseChange += HandleDatabaseChange;
var options = new RestClientOptions
{
BaseUrl = new Uri("http://localhost:5183/")
};
_client = new RestClient(options);
PrePopulate();
CheckCanSync();
}
private async Task PrePopulate()
{
var resHistory = await _client.GetAsync<List<History>>("/history");
_history = resHistory;
var resPlaylist = await _client.GetAsync<List<Playlist>>("/playlist");
_playlist = resPlaylist;
var resProfile = await _client.GetAsync<List<Profile>>("/profile");
_profile = resProfile;
var resSearchHistory = await _client.GetAsync<List<SearchHistory>>("/searchHistory");
_searchHistory = resSearchHistory;
var resSetting = await _client.GetAsync<List<Setting>>("/setting");
_setting = resSetting;
}
private void CheckCanSync()
{
if (Process.GetProcessesByName("FreeTube").ToList().Count > 0)
{
Console.WriteLine("FreeTube is running, awaiting till we can sync.");
}
}
private void HandleDatabaseChange(string dbName, object obj)
{
switch (dbName)
{
case "history.db":
var history = (History?)obj;
if (history == null) return;
if (_history.Any(x => x._id == history._id)) return;
_history.Add(history);
_dirtyHistory = true;
break;
case "playlist.db":
var playlist = (Playlist?)obj;
if (playlist == null) return;
if (_playlist.Any(x => x._id == playlist._id)) return;
_playlist.Add(playlist);
_dirtyPlaylist = true;
break;
case "profile.db":
var profile = (Profile?)obj;
if (profile == null) return;
if (_profile.Any(x => x._id == profile._id)) return;
_profile.Add(profile);
_dirtyProfile = true;
break;
case "search.db":
var search = (SearchHistory?)obj;
if (search == null) return;
if (_searchHistory.Any(x => x._id == search._id)) return;
_searchHistory.Add(search);
_dirtySearchHistory = true;
break;
case "setting.db":
var setting = (Setting?)obj;
if (setting == null) return;
if (_setting.Any(x => x._id == setting._id)) return;
_setting.Add(setting);
_dirtySetting = true;
break;
}
}
}

View file

@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using FreeTubeSyncer.Library;
using FreeTubeSyncer.Models.DatabaseModels;
using RestSharp;
namespace FreeTubeSyncer.REST;
public class Syncer<T> where T : class, IDataModel
{
private List<T> _entries = new List<T>();
private RestClient _client;
private string _dbPath;
private string _restEndpoint;
public bool IsDirty = false;
public Syncer(DBSyncWatcher watcher, string dbPath, string dbName, string restEndpoint)
{
watcher.WatchFiles[dbName] = typeof(T);
watcher.OnDatabaseChange += HandleDatabaseChange;
_client = new RestClient(new RestClientOptions("http://localhost:5183"));
_dbPath = dbPath;
_restEndpoint = restEndpoint;
ReadDatabase().Wait();
FetchDatabase().Wait();
}
private async Task ReadDatabase()
{
var lines = File.ReadAllLines(_dbPath);
foreach (var entry in lines)
{
if (entry == "") continue;
try
{
var item = JsonSerializer.Deserialize<T>(entry);
if (item == null) continue;
if (_entries.Any(x => x.EqualId(item.Id())))
_entries.RemoveAll(x => x.EqualId(item.Id()));
_entries.Add(item);
Console.WriteLine($"Posting {item.Id()}");
await _client.PostJsonAsync<T>(_restEndpoint, item);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to parse: {entry}");
}
}
}
private async Task FetchDatabase()
{
var entries = await _client.GetAsync<IEnumerable<T>>(_restEndpoint);
if (entries == null) return;
foreach (var entry in entries)
{
if (_entries.Any(x => x.EqualId(entry.Id())))
_entries.RemoveAll(x => x.EqualId(entry.Id()));
_entries.Add(entry);
}
}
private async void HandleDatabaseChange(string dbName, object entryObject)
{
var entry = (T)entryObject;
if (_entries.Any(x => x.EqualId(entry.Id())))
_entries.RemoveAll(x => x.EqualId(entry.Id()));
_entries.Add(entry);
await _client.PostJsonAsync<T>(_restEndpoint, entry);
IsDirty = true;
}
public void Sync()
{
var json = new List<string>();
foreach (var entry in _entries)
json.Add(JsonSerializer.Serialize(entry));
File.WriteAllLines(_dbPath, json);
Console.WriteLine($"Updated {_dbPath}");
IsDirty = false;
}
}