Initial Commit

This commit is contained in:
Mario Steele 2025-07-21 13:31:24 -05:00
commit 44a89ad589
21 changed files with 477 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea/
*.user

16
FreeTubeSyncer.sln Normal file
View file

@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FreeTubeSyncer", "FreeTubeSyncer\FreeTubeSyncer.csproj", "{B32732C0-EEA7-4149-90D0-933E4DACE3B0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B32732C0-EEA7-4149-90D0-933E4DACE3B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B32732C0-EEA7-4149-90D0-933E4DACE3B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B32732C0-EEA7-4149-90D0-933E4DACE3B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B32732C0-EEA7-4149-90D0-933E4DACE3B0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

25
FreeTubeSyncer/App.axaml Normal file
View file

@ -0,0 +1,25 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="FreeTubeSyncer.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
</Application.Styles>
<TrayIcon.Icons>
<TrayIcons>
<TrayIcon Icon="/Assets/freetube.png"
ToolTipText="FreeTube Syncer">
<TrayIcon.Menu>
<NativeMenu>
<NativeMenuItem Header="Show Settings"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="Quit"/>
</NativeMenu>
</TrayIcon.Menu>
</TrayIcon>
</TrayIcons>
</TrayIcon.Icons>
</Application>

View file

@ -0,0 +1,53 @@
using System;
using System.IO;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using FreeTubeSyncer.Library;
using FreeTubeSyncer.Models.DatabaseModels;
namespace FreeTubeSyncer;
public partial class App : Application
{
private DBSyncWatcher _watcher;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
//desktop.MainWindow = new MainWindow();
var path = "";
if (OperatingSystem.IsWindows())
path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FreeTube");
else if (OperatingSystem.IsLinux())
{
path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config",
"FreeTube");
if (!Path.Exists(path))
{
path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".var", "app", "io.freetubeapp.FreeTube", "config",
"FreeTube");
if (!Path.Exists(path))
Console.WriteLine("Failed to find Path for FreeTube!");
}
}
else if (OperatingSystem.IsMacOS())
path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FreeTube");
_watcher = new DBSyncWatcher(path);
_watcher.WatchFiles.Add("history.db", typeof(History));
_watcher.WatchFiles.Add("playlist.db", typeof(Playlist));
_watcher.WatchFiles.Add("profiles.db", typeof(Profile));
_watcher.WatchFiles.Add("search-history.db", typeof(SearchHistory));
_watcher.WatchFiles.Add("settings.db", typeof(Setting));
}
base.OnFrameworkInitializationCompleted();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.2"/>
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.2"/>
<PackageReference Include="Avalonia.Desktop" Version="11.3.2"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.2"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.2"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.2">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="RestSharp" Version="112.1.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\" />
<Folder Include="Views\" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\freetube.png" />
<AvaloniaXaml Include="Assets\freetube.png" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace FreeTubeSyncer.Library;
public class DBSyncWatcher
{
private FileSystemWatcher _watcher;
public Dictionary<string, Type> WatchFiles { get; set; } = [];
public delegate void DatabaseChange(string db, object value);
public event DatabaseChange OnDatabaseChange;
public DBSyncWatcher(string path)
{
_watcher = new FileSystemWatcher(path);
_watcher.NotifyFilter = NotifyFilters.LastWrite |
NotifyFilters.CreationTime;
_watcher.Changed += HandleChanged;
_watcher.Created += HandleCreated;
_watcher.Error += HandleError;
_watcher.Filter = "*.db";
_watcher.IncludeSubdirectories = true;
_watcher.EnableRaisingEvents = true;
}
private void HandleChanged(object sender, FileSystemEventArgs e)
{
if (e.ChangeType != WatcherChangeTypes.Changed) return;
var dbName = Path.GetFileName(e.FullPath);
if (!WatchFiles.Keys.Contains(dbName)) return;
Console.WriteLine("New Change in {0}", dbName);
var data = File.ReadAllText(e.FullPath);
foreach (var line in data.Split('\n'))
{
var type = WatchFiles[dbName];
var item = JsonSerializer.Deserialize(line, type);
if (item == null) continue;
OnDatabaseChange?.Invoke(dbName, item);
}
}
private void HandleCreated(object sender, FileSystemEventArgs e)
{
if (e.ChangeType != WatcherChangeTypes.Created) return;
var dbName = Path.GetFileName(e.FullPath);
if (!WatchFiles.Keys.Contains(dbName)) return;
var data = File.ReadAllText(e.FullPath);
foreach (var line in data.Split('\n'))
{
var type = WatchFiles[dbName];
var item = JsonSerializer.Deserialize(line, type);
if (item == null) continue;
OnDatabaseChange?.Invoke(dbName, item);
}
}
private void HandleError(object sender, ErrorEventArgs e)
{
Console.WriteLine("Error: {0}\n{1}", e.GetException().Message, e.GetException().StackTrace);
}
}

View file

@ -0,0 +1,9 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="FreeTubeSyncer.MainWindow"
Title="FreeTubeSyncer">
Welcome to Avalonia!
</Window>

View file

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace FreeTubeSyncer;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}

View file

@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class History
{
public string _id { get; set; } = string.Empty;
public string videoId { get; set; } = string.Empty;
public string title { get; set; } = string.Empty;
public string author { get; set; } = string.Empty;
public string authorId { get; set; } = string.Empty;
public long published { get; set; }
public string description { get; set; } = string.Empty;
public long viewCount { get; set; }
public long lengthSeconds { get; set; }
public float watchProgress { get; set; }
public long timeWatched { get; set; }
public bool isLive { get; set; }
public string type { get; set; } = string.Empty;
public string lastViewedPlaylistType { get; set; } = string.Empty;
public string? lastViewedPlaylistItemId { get; set; }
}

View file

@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class Playlist
{
public string _id { get; set; } = string.Empty;
public string playlistName { get; set; } = string.Empty;
public bool @protected { get; set; }
public List<Video> videos { get; set; } = [];
public long createdAt { get; set; }
public long lastUpdatedAt { get; set; }
}

View file

@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class Profile
{
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; } = [];
}

View file

@ -0,0 +1,10 @@
using System.Diagnostics.CodeAnalysis;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class SearchHistory
{
public string _id { get; set; } = string.Empty;
public long lastUpdatedAt { get; set; }
}

View file

@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class Setting
{
#pragma warning disable CS8618
public string _id { get; set; } = string.Empty;
public string? ValueJson { get; set; }
#pragma warning restore CS8618
public object Value
{
#pragma warning disable CS8603
get => string.IsNullOrEmpty(ValueJson) ? null : JsonSerializer.Deserialize<object>(ValueJson);
#pragma warning restore CS8603
set => ValueJson = JsonSerializer.Serialize(value);
}
}

View file

@ -0,0 +1,11 @@
using System.Diagnostics.CodeAnalysis;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class Subscription
{
public required string id { get; set; }
public required string name { get; set; }
public string? thumbnail { get; set; }
}

View file

@ -0,0 +1,17 @@
using System.Diagnostics.CodeAnalysis;
namespace FreeTubeSyncer.Models.DatabaseModels;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class Video
{
public string videoId { get; set; } = string.Empty;
public string title { get; set; } = string.Empty;
public string author { get; set; } = string.Empty;
public string authorId { get; set; } = string.Empty;
public string lengthSeconds { get; set; } = string.Empty;
public long pubished { get; set; }
public long timeAdded { get; set; }
public string playlistItemId { get; set; } = string.Empty;
public string type { get; set; } = string.Empty;
}

21
FreeTubeSyncer/Program.cs Normal file
View file

@ -0,0 +1,21 @@
using Avalonia;
using System;
namespace FreeTubeSyncer;
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// 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);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}

View file

@ -0,0 +1,102 @@
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,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="FreeTubeSyncer.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>