mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2024-11-25 14:42:44 +01:00
Compare commits
3 Commits
3d9d6a454a
...
d2934f4d6a
Author | SHA1 | Date | |
---|---|---|---|
d2934f4d6a | |||
567253d147 | |||
aa6555990c |
@ -45,17 +45,17 @@
|
|||||||
<Rectangle />
|
<Rectangle />
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<TextBlock Classes="label">Servers</TextBlock>
|
<TextBlock Classes="label">Servers</TextBlock>
|
||||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalServers, Converter={StaticResource NumberValueConverter}}" />
|
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalServers, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Rectangle />
|
<Rectangle />
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<TextBlock Classes="label">Channels</TextBlock>
|
<TextBlock Classes="label">Channels</TextBlock>
|
||||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalChannels, Converter={StaticResource NumberValueConverter}}" />
|
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalChannels, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Rectangle />
|
<Rectangle />
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<TextBlock Classes="label">Messages</TextBlock>
|
<TextBlock Classes="label">Messages</TextBlock>
|
||||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalMessages, Converter={StaticResource NumberValueConverter}}" />
|
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalMessages, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
@ -20,9 +20,9 @@ public sealed partial class MainWindow : Window {
|
|||||||
DataContext = new MainWindowModel(this, args);
|
DataContext = new MainWindowModel(this, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnClosed(object? sender, EventArgs e) {
|
public async void OnClosed(object? sender, EventArgs e) {
|
||||||
if (DataContext is IDisposable disposable) {
|
if (DataContext is MainWindowModel model) {
|
||||||
disposable.Dispose();
|
await model.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var temporaryFile in ViewerPageModel.TemporaryFiles) {
|
foreach (var temporaryFile in ViewerPageModel.TemporaryFiles) {
|
||||||
|
@ -12,7 +12,7 @@ using DHT.Utils.Models;
|
|||||||
|
|
||||||
namespace DHT.Desktop.Main;
|
namespace DHT.Desktop.Main;
|
||||||
|
|
||||||
sealed class MainWindowModel : BaseModel, IDisposable {
|
sealed class MainWindowModel : BaseModel, IAsyncDisposable {
|
||||||
private const string DefaultTitle = "Discord History Tracker";
|
private const string DefaultTitle = "Discord History Tracker";
|
||||||
|
|
||||||
public string Title { get; private set; } = DefaultTitle;
|
public string Title { get; private set; } = DefaultTitle;
|
||||||
@ -75,7 +75,7 @@ sealed class MainWindowModel : BaseModel, IDisposable {
|
|||||||
if (e.PropertyName == nameof(welcomeScreenModel.Db)) {
|
if (e.PropertyName == nameof(welcomeScreenModel.Db)) {
|
||||||
if (mainContentScreenModel != null) {
|
if (mainContentScreenModel != null) {
|
||||||
mainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed;
|
mainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed;
|
||||||
mainContentScreenModel.Dispose();
|
await mainContentScreenModel.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
db?.Dispose();
|
db?.Dispose();
|
||||||
@ -107,9 +107,13 @@ sealed class MainWindowModel : BaseModel, IDisposable {
|
|||||||
welcomeScreenModel.CloseDatabase();
|
welcomeScreenModel.CloseDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public async ValueTask DisposeAsync() {
|
||||||
welcomeScreenModel.Dispose();
|
welcomeScreenModel.Dispose();
|
||||||
mainContentScreenModel?.Dispose();
|
|
||||||
|
if (mainContentScreenModel != null) {
|
||||||
|
await mainContentScreenModel.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
db?.Dispose();
|
db?.Dispose();
|
||||||
db = null;
|
db = null;
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
<StackPanel Orientation="Vertical" Spacing="20">
|
<StackPanel Orientation="Vertical" Spacing="20">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<Button Command="{Binding OnClickToggleDownload}" Content="{Binding ToggleDownloadButtonText}" IsEnabled="{Binding IsToggleDownloadButtonEnabled}" DockPanel.Dock="Left" />
|
<Button Command="{Binding OnClickToggleDownload}" Content="{Binding ToggleDownloadButtonText}" IsEnabled="{Binding IsToggleDownloadButtonEnabled}" DockPanel.Dock="Left" />
|
||||||
<TextBlock Text="{Binding DownloadMessage}" Margin="10 0 0 0" VerticalAlignment="Center" DockPanel.Dock="Left" />
|
<TextBlock Text="{Binding DownloadMessage}" MinWidth="100" Margin="10 0 0 0" VerticalAlignment="Center" TextAlignment="Right" DockPanel.Dock="Left" />
|
||||||
<ProgressBar Value="{Binding DownloadProgress}" IsVisible="{Binding IsDownloading}" Margin="15 0" VerticalAlignment="Center" DockPanel.Dock="Right" />
|
<ProgressBar Value="{Binding DownloadProgress}" IsVisible="{Binding IsDownloading}" Margin="15 0" VerticalAlignment="Center" DockPanel.Dock="Right" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
<controls:AttachmentFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !IsDownloading, RelativeSource={RelativeSource AncestorType=pages:AttachmentsPageModel}}" />
|
<controls:AttachmentFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !IsDownloading, RelativeSource={RelativeSource AncestorType=pages:AttachmentsPageModel}}" />
|
||||||
@ -42,8 +42,8 @@
|
|||||||
<DataGrid ItemsSource="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True">
|
<DataGrid ItemsSource="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="State" Binding="{Binding State}" Width="*" />
|
<DataGridTextColumn Header="State" Binding="{Binding State}" Width="*" />
|
||||||
<DataGridTextColumn Header="Attachments" Binding="{Binding Items, Converter={StaticResource NumberValueConverter}}" Width="*" CellStyleClasses="right" />
|
<DataGridTextColumn Header="Attachments" Binding="{Binding Items, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" Width="*" CellStyleClasses="right" />
|
||||||
<DataGridTextColumn Header="Size" Binding="{Binding Size, Converter={StaticResource BytesValueConverter}}" Width="*" CellStyleClasses="right" />
|
<DataGridTextColumn Header="Size" Binding="{Binding Size, Mode=OneWay, Converter={StaticResource BytesValueConverter}}" Width="*" CellStyleClasses="right" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</Expander>
|
</Expander>
|
||||||
|
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using DHT.Desktop.Common;
|
using DHT.Desktop.Common;
|
||||||
using DHT.Desktop.Main.Controls;
|
using DHT.Desktop.Main.Controls;
|
||||||
@ -15,16 +16,17 @@ using DHT.Utils.Tasks;
|
|||||||
|
|
||||||
namespace DHT.Desktop.Main.Pages;
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
sealed class AttachmentsPageModel : BaseModel, IAsyncDisposable {
|
||||||
private static readonly DownloadItemFilter EnqueuedItemFilter = new() {
|
private static readonly DownloadItemFilter EnqueuedItemFilter = new() {
|
||||||
IncludeStatuses = new HashSet<DownloadStatus> {
|
IncludeStatuses = new HashSet<DownloadStatus> {
|
||||||
DownloadStatus.Enqueued
|
DownloadStatus.Enqueued,
|
||||||
|
DownloadStatus.Downloading
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private bool isThreadDownloadButtonEnabled = true;
|
private bool isThreadDownloadButtonEnabled = true;
|
||||||
|
|
||||||
public string ToggleDownloadButtonText => downloadThread == null ? "Start Downloading" : "Stop Downloading";
|
public string ToggleDownloadButtonText => downloader == null ? "Start Downloading" : "Stop Downloading";
|
||||||
|
|
||||||
public bool IsToggleDownloadButtonEnabled {
|
public bool IsToggleDownloadButtonEnabled {
|
||||||
get => isThreadDownloadButtonEnabled;
|
get => isThreadDownloadButtonEnabled;
|
||||||
@ -32,7 +34,7 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string DownloadMessage { get; set; } = "";
|
public string DownloadMessage { get; set; } = "";
|
||||||
public double DownloadProgress => allItemsCount is null or 0 ? 0.0 : 100.0 * doneItemsCount / allItemsCount.Value;
|
public double DownloadProgress => totalItemsToDownloadCount is null or 0 ? 0.0 : 100.0 * doneItemsCount / totalItemsToDownloadCount.Value;
|
||||||
|
|
||||||
public AttachmentFilterPanelModel FilterModel { get; }
|
public AttachmentFilterPanelModel FilterModel { get; }
|
||||||
|
|
||||||
@ -52,15 +54,16 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsDownloading => downloadThread != null;
|
public bool IsDownloading => downloader != null;
|
||||||
public bool HasFailedDownloads => statisticsFailed.Items > 0;
|
public bool HasFailedDownloads => statisticsFailed.Items > 0;
|
||||||
|
|
||||||
private readonly IDatabaseFile db;
|
private readonly IDatabaseFile db;
|
||||||
private readonly AsyncValueComputer<DownloadStatusStatistics>.Single downloadStatisticsComputer;
|
private readonly AsyncValueComputer<DownloadStatusStatistics>.Single downloadStatisticsComputer;
|
||||||
private BackgroundDownloadThread? downloadThread;
|
private BackgroundDownloader? downloader;
|
||||||
|
|
||||||
private int doneItemsCount;
|
private int doneItemsCount;
|
||||||
private int? allItemsCount;
|
private int initialFinishedCount;
|
||||||
|
private int? totalItemsToDownloadCount;
|
||||||
|
|
||||||
public AttachmentsPageModel() : this(DummyDatabaseFile.Instance) {}
|
public AttachmentsPageModel() : this(DummyDatabaseFile.Instance) {}
|
||||||
|
|
||||||
@ -74,11 +77,11 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public async ValueTask DisposeAsync() {
|
||||||
db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
||||||
|
|
||||||
FilterModel.Dispose();
|
FilterModel.Dispose();
|
||||||
DisposeDownloadThread();
|
await DisposeDownloader();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDbStatisticsChanged(object? sender, PropertyChangedEventArgs e) {
|
private void OnDbStatisticsChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
@ -124,44 +127,42 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
OnPropertyChanged(nameof(HasFailedDownloads));
|
OnPropertyChanged(nameof(HasFailedDownloads));
|
||||||
}
|
}
|
||||||
|
|
||||||
allItemsCount = doneItemsCount + statisticsEnqueued.Items;
|
totalItemsToDownloadCount = statisticsEnqueued.Items + statisticsDownloaded.Items + statisticsFailed.Items - initialFinishedCount;
|
||||||
UpdateDownloadMessage();
|
UpdateDownloadMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateDownloadMessage() {
|
private void UpdateDownloadMessage() {
|
||||||
DownloadMessage = IsDownloading ? doneItemsCount.Format() + " / " + (allItemsCount?.Format() ?? "?") : "";
|
DownloadMessage = IsDownloading ? doneItemsCount.Format() + " / " + (totalItemsToDownloadCount?.Format() ?? "?") : "";
|
||||||
|
|
||||||
OnPropertyChanged(nameof(DownloadMessage));
|
OnPropertyChanged(nameof(DownloadMessage));
|
||||||
OnPropertyChanged(nameof(DownloadProgress));
|
OnPropertyChanged(nameof(DownloadProgress));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DownloadThreadOnOnItemFinished(object? sender, DownloadItem e) {
|
private void DownloaderOnOnItemFinished(object? sender, DownloadItem e) {
|
||||||
Interlocked.Increment(ref doneItemsCount);
|
Interlocked.Increment(ref doneItemsCount);
|
||||||
|
|
||||||
Dispatcher.UIThread.Invoke(UpdateDownloadMessage);
|
Dispatcher.UIThread.Invoke(UpdateDownloadMessage);
|
||||||
downloadStatisticsComputer.Recompute();
|
downloadStatisticsComputer.Recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DownloadThreadOnOnServerStopped(object? sender, EventArgs e) {
|
public async Task OnClickToggleDownload() {
|
||||||
downloadStatisticsComputer.Recompute();
|
if (downloader == null) {
|
||||||
IsToggleDownloadButtonEnabled = true;
|
initialFinishedCount = statisticsDownloaded.Items + statisticsFailed.Items;
|
||||||
}
|
|
||||||
|
|
||||||
public void OnClickToggleDownload() {
|
|
||||||
if (downloadThread == null) {
|
|
||||||
EnqueueDownloadItems();
|
EnqueueDownloadItems();
|
||||||
downloadThread = new BackgroundDownloadThread(db);
|
downloader = new BackgroundDownloader(db);
|
||||||
downloadThread.OnItemFinished += DownloadThreadOnOnItemFinished;
|
downloader.OnItemFinished += DownloaderOnOnItemFinished;
|
||||||
downloadThread.OnServerStopped += DownloadThreadOnOnServerStopped;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
IsToggleDownloadButtonEnabled = false;
|
IsToggleDownloadButtonEnabled = false;
|
||||||
DisposeDownloadThread();
|
await DisposeDownloader();
|
||||||
|
downloadStatisticsComputer.Recompute();
|
||||||
|
IsToggleDownloadButtonEnabled = true;
|
||||||
|
|
||||||
db.RemoveDownloadItems(EnqueuedItemFilter, FilterRemovalMode.RemoveMatching);
|
db.RemoveDownloadItems(EnqueuedItemFilter, FilterRemovalMode.RemoveMatching);
|
||||||
|
|
||||||
doneItemsCount = 0;
|
doneItemsCount = 0;
|
||||||
allItemsCount = null;
|
initialFinishedCount = 0;
|
||||||
|
totalItemsToDownloadCount = null;
|
||||||
UpdateDownloadMessage();
|
UpdateDownloadMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +174,7 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
var allExceptFailedFilter = new DownloadItemFilter {
|
var allExceptFailedFilter = new DownloadItemFilter {
|
||||||
IncludeStatuses = new HashSet<DownloadStatus> {
|
IncludeStatuses = new HashSet<DownloadStatus> {
|
||||||
DownloadStatus.Enqueued,
|
DownloadStatus.Enqueued,
|
||||||
|
DownloadStatus.Downloading,
|
||||||
DownloadStatus.Success
|
DownloadStatus.Success
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -184,13 +186,13 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisposeDownloadThread() {
|
private async Task DisposeDownloader() {
|
||||||
if (downloadThread != null) {
|
if (downloader != null) {
|
||||||
downloadThread.OnItemFinished -= DownloadThreadOnOnItemFinished;
|
downloader.OnItemFinished -= DownloaderOnOnItemFinished;
|
||||||
downloadThread.StopThread();
|
await downloader.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadThread = null;
|
downloader = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class StatisticsRow {
|
public sealed class StatisticsRow {
|
||||||
|
@ -11,7 +11,7 @@ using DHT.Utils.Logging;
|
|||||||
|
|
||||||
namespace DHT.Desktop.Main.Screens;
|
namespace DHT.Desktop.Main.Screens;
|
||||||
|
|
||||||
sealed class MainContentScreenModel : IDisposable {
|
sealed class MainContentScreenModel : IAsyncDisposable {
|
||||||
private static readonly Log Log = Log.ForType<MainContentScreenModel>();
|
private static readonly Log Log = Log.ForType<MainContentScreenModel>();
|
||||||
|
|
||||||
public DatabasePage DatabasePage { get; }
|
public DatabasePage DatabasePage { get; }
|
||||||
@ -35,7 +35,7 @@ sealed class MainContentScreenModel : IDisposable {
|
|||||||
public bool HasDebugPage => true;
|
public bool HasDebugPage => true;
|
||||||
private DebugPageModel DebugPageModel { get; }
|
private DebugPageModel DebugPageModel { get; }
|
||||||
#else
|
#else
|
||||||
public bool HasDebugPage => false;
|
public bool HasDebugPage => false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public StatusBarModel StatusBarModel { get; }
|
public StatusBarModel StatusBarModel { get; }
|
||||||
@ -97,9 +97,9 @@ sealed class MainContentScreenModel : IDisposable {
|
|||||||
serverManager.Launch();
|
serverManager.Launch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public async ValueTask DisposeAsync() {
|
||||||
ServerLauncher.ServerManagementExceptionCaught -= ServerLauncherOnServerManagementExceptionCaught;
|
ServerLauncher.ServerManagementExceptionCaught -= ServerLauncherOnServerManagementExceptionCaught;
|
||||||
AttachmentsPageModel.Dispose();
|
await AttachmentsPageModel.DisposeAsync();
|
||||||
ViewerPageModel.Dispose();
|
ViewerPageModel.Dispose();
|
||||||
serverManager.Dispose();
|
serverManager.Dispose();
|
||||||
}
|
}
|
||||||
|
@ -8,5 +8,6 @@ namespace DHT.Server.Data;
|
|||||||
public enum DownloadStatus {
|
public enum DownloadStatus {
|
||||||
Enqueued = 0,
|
Enqueued = 0,
|
||||||
GenericError = 1,
|
GenericError = 1,
|
||||||
|
Downloading = 2,
|
||||||
Success = HttpStatusCode.OK
|
Success = HttpStatusCode.OK
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ public sealed class DummyDatabaseFile : IDatabaseFile {
|
|||||||
|
|
||||||
public void EnqueueDownloadItems(AttachmentFilter? filter = null) {}
|
public void EnqueueDownloadItems(AttachmentFilter? filter = null) {}
|
||||||
|
|
||||||
public List<DownloadItem> GetEnqueuedDownloadItems(int count) {
|
public List<DownloadItem> PullEnqueuedDownloadItems(int count) {
|
||||||
return new();
|
return new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ public interface IDatabaseFile : IDisposable {
|
|||||||
DownloadedAttachment? GetDownloadedAttachment(string url);
|
DownloadedAttachment? GetDownloadedAttachment(string url);
|
||||||
|
|
||||||
void EnqueueDownloadItems(AttachmentFilter? filter = null);
|
void EnqueueDownloadItems(AttachmentFilter? filter = null);
|
||||||
List<DownloadItem> GetEnqueuedDownloadItems(int count);
|
List<DownloadItem> PullEnqueuedDownloadItems(int count);
|
||||||
void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode);
|
void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode);
|
||||||
DownloadStatusStatistics GetDownloadStatusStatistics();
|
DownloadStatusStatistics GetDownloadStatusStatistics();
|
||||||
|
|
||||||
|
@ -174,8 +174,8 @@ sealed class Schema {
|
|||||||
int processedUrls = -1;
|
int processedUrls = -1;
|
||||||
|
|
||||||
await using (var updateCmd = conn.Command("UPDATE attachments SET download_url = url, url = :normalized_url WHERE attachment_id = :attachment_id")) {
|
await using (var updateCmd = conn.Command("UPDATE attachments SET download_url = url, url = :normalized_url WHERE attachment_id = :attachment_id")) {
|
||||||
updateCmd.Parameters.Add(":attachment_id", SqliteType.Integer);
|
updateCmd.Add(":attachment_id", SqliteType.Integer);
|
||||||
updateCmd.Parameters.Add(":normalized_url", SqliteType.Text);
|
updateCmd.Add(":normalized_url", SqliteType.Text);
|
||||||
|
|
||||||
foreach (var (attachmentId, normalizedUrl) in normalizedUrls) {
|
foreach (var (attachmentId, normalizedUrl) in normalizedUrls) {
|
||||||
if (++processedUrls % 1000 == 0) {
|
if (++processedUrls % 1000 == 0) {
|
||||||
@ -235,8 +235,8 @@ sealed class Schema {
|
|||||||
tx = conn.BeginTransaction();
|
tx = conn.BeginTransaction();
|
||||||
|
|
||||||
await using (var updateCmd = conn.Command("UPDATE downloads SET download_url = :download_url, url = :normalized_url WHERE url = :download_url")) {
|
await using (var updateCmd = conn.Command("UPDATE downloads SET download_url = :download_url, url = :normalized_url WHERE url = :download_url")) {
|
||||||
updateCmd.Parameters.Add(":normalized_url", SqliteType.Text);
|
updateCmd.Add(":normalized_url", SqliteType.Text);
|
||||||
updateCmd.Parameters.Add(":download_url", SqliteType.Text);
|
updateCmd.Add(":download_url", SqliteType.Text);
|
||||||
|
|
||||||
foreach (var (normalizedUrl, downloadUrl) in normalizedUrlsToOriginalUrls) {
|
foreach (var (normalizedUrl, downloadUrl) in normalizedUrlsToOriginalUrls) {
|
||||||
if (++processedUrls % 100 == 0) {
|
if (++processedUrls % 100 == 0) {
|
||||||
|
@ -522,25 +522,42 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
|||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<DownloadItem> GetEnqueuedDownloadItems(int count) {
|
public List<DownloadItem> PullEnqueuedDownloadItems(int count) {
|
||||||
var list = new List<DownloadItem>();
|
var found = new List<DownloadItem>();
|
||||||
|
var pulled = new List<DownloadItem>();
|
||||||
|
|
||||||
using var conn = pool.Take();
|
using var conn = pool.Take();
|
||||||
using var cmd = conn.Command("SELECT normalized_url, download_url, size FROM downloads WHERE status = :enqueued LIMIT :limit");
|
using (var cmd = conn.Command("SELECT normalized_url, download_url, size FROM downloads WHERE status = :enqueued LIMIT :limit")) {
|
||||||
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||||
cmd.AddAndSet(":limit", SqliteType.Integer, Math.Max(0, count));
|
cmd.AddAndSet(":limit", SqliteType.Integer, Math.Max(0, count));
|
||||||
|
|
||||||
using var reader = cmd.ExecuteReader();
|
using var reader = cmd.ExecuteReader();
|
||||||
|
|
||||||
while (reader.Read()) {
|
while (reader.Read()) {
|
||||||
list.Add(new DownloadItem {
|
found.Add(new DownloadItem {
|
||||||
NormalizedUrl = reader.GetString(0),
|
NormalizedUrl = reader.GetString(0),
|
||||||
DownloadUrl = reader.GetString(1),
|
DownloadUrl = reader.GetString(1),
|
||||||
Size = reader.GetUint64(2),
|
Size = reader.GetUint64(2),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
if (found.Count != 0) {
|
||||||
|
using var cmd = conn.Command("UPDATE downloads SET status = :downloading WHERE normalized_url = :normalized_url AND status = :enqueued");
|
||||||
|
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||||
|
cmd.AddAndSet(":downloading", SqliteType.Integer, (int) DownloadStatus.Downloading);
|
||||||
|
cmd.Add(":normalized_url", SqliteType.Text);
|
||||||
|
|
||||||
|
foreach (var item in found) {
|
||||||
|
cmd.Set(":normalized_url", item.NormalizedUrl);
|
||||||
|
|
||||||
|
if (cmd.ExecuteNonQuery() == 1) {
|
||||||
|
pulled.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pulled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode) {
|
public void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode) {
|
||||||
@ -562,15 +579,16 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
|||||||
static void LoadSuccessStatistics(ISqliteConnection conn, DownloadStatusStatistics result) {
|
static void LoadSuccessStatistics(ISqliteConnection conn, DownloadStatusStatistics result) {
|
||||||
using var cmd = conn.Command("""
|
using var cmd = conn.Command("""
|
||||||
SELECT
|
SELECT
|
||||||
IFNULL(SUM(CASE WHEN status = :enqueued THEN 1 ELSE 0 END), 0),
|
IFNULL(SUM(CASE WHEN status IN (:enqueued, :downloading) THEN 1 ELSE 0 END), 0),
|
||||||
IFNULL(SUM(CASE WHEN status = :enqueued THEN size ELSE 0 END), 0),
|
IFNULL(SUM(CASE WHEN status IN (:enqueued, :downloading) THEN size ELSE 0 END), 0),
|
||||||
IFNULL(SUM(CASE WHEN status = :success THEN 1 ELSE 0 END), 0),
|
IFNULL(SUM(CASE WHEN status = :success THEN 1 ELSE 0 END), 0),
|
||||||
IFNULL(SUM(CASE WHEN status = :success THEN size ELSE 0 END), 0),
|
IFNULL(SUM(CASE WHEN status = :success THEN size ELSE 0 END), 0),
|
||||||
IFNULL(SUM(CASE WHEN status != :enqueued AND status != :success THEN 1 ELSE 0 END), 0),
|
IFNULL(SUM(CASE WHEN status NOT IN (:enqueued, :downloading) AND status != :success THEN 1 ELSE 0 END), 0),
|
||||||
IFNULL(SUM(CASE WHEN status != :enqueued AND status != :success THEN size ELSE 0 END), 0)
|
IFNULL(SUM(CASE WHEN status NOT IN (:enqueued, :downloading) AND status != :success THEN size ELSE 0 END), 0)
|
||||||
FROM downloads
|
FROM downloads
|
||||||
""");
|
""");
|
||||||
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||||
|
cmd.AddAndSet(":downloading", SqliteType.Integer, (int) DownloadStatus.Downloading);
|
||||||
cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
|
cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
|
||||||
|
|
||||||
using var reader = cmd.ExecuteReader();
|
using var reader = cmd.ExecuteReader();
|
||||||
|
@ -62,6 +62,10 @@ static class SqliteExtensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void Add(this SqliteCommand cmd, string key, SqliteType type) {
|
||||||
|
cmd.Parameters.Add(key, type);
|
||||||
|
}
|
||||||
|
|
||||||
public static void AddAndSet(this SqliteCommand cmd, string key, SqliteType type, object? value) {
|
public static void AddAndSet(this SqliteCommand cmd, string key, SqliteType type, object? value) {
|
||||||
cmd.Parameters.Add(key, type).Value = value ?? DBNull.Value;
|
cmd.Parameters.Add(key, type).Value = value ?? DBNull.Value;
|
||||||
}
|
}
|
||||||
|
@ -1,130 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Utils.Logging;
|
|
||||||
using DHT.Utils.Models;
|
|
||||||
|
|
||||||
namespace DHT.Server.Download;
|
|
||||||
|
|
||||||
public sealed class BackgroundDownloadThread : BaseModel {
|
|
||||||
private static readonly Log Log = Log.ForType<BackgroundDownloadThread>();
|
|
||||||
|
|
||||||
public event EventHandler<DownloadItem>? OnItemFinished {
|
|
||||||
add => parameters.OnItemFinished += value;
|
|
||||||
remove => parameters.OnItemFinished -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler? OnServerStopped {
|
|
||||||
add => parameters.OnServerStopped += value;
|
|
||||||
remove => parameters.OnServerStopped -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource cancellationTokenSource;
|
|
||||||
private readonly ThreadInstance.Parameters parameters;
|
|
||||||
|
|
||||||
public BackgroundDownloadThread(IDatabaseFile db) {
|
|
||||||
this.cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
this.parameters = new ThreadInstance.Parameters(db, cancellationTokenSource);
|
|
||||||
|
|
||||||
var thread = new Thread(new ThreadInstance().Work) {
|
|
||||||
Name = "DHT download thread"
|
|
||||||
};
|
|
||||||
|
|
||||||
thread.Start(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopThread() {
|
|
||||||
try {
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
} catch (ObjectDisposedException) {
|
|
||||||
Log.Warn("Attempted to stop background download thread after the cancellation token has been disposed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ThreadInstance {
|
|
||||||
private const int QueueSize = 32;
|
|
||||||
|
|
||||||
public sealed class Parameters {
|
|
||||||
public event EventHandler<DownloadItem>? OnItemFinished;
|
|
||||||
public event EventHandler? OnServerStopped;
|
|
||||||
|
|
||||||
public IDatabaseFile Db { get; }
|
|
||||||
public CancellationTokenSource CancellationTokenSource { get; }
|
|
||||||
|
|
||||||
public Parameters(IDatabaseFile db, CancellationTokenSource cancellationTokenSource) {
|
|
||||||
Db = db;
|
|
||||||
CancellationTokenSource = cancellationTokenSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FireOnItemFinished(DownloadItem item) {
|
|
||||||
OnItemFinished?.Invoke(null, item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FireOnServerStopped() {
|
|
||||||
OnServerStopped?.Invoke(null, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly HttpClient client = new ();
|
|
||||||
|
|
||||||
public ThreadInstance() {
|
|
||||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void Work(object? obj) {
|
|
||||||
var parameters = (Parameters) obj!;
|
|
||||||
|
|
||||||
var cancellationTokenSource = parameters.CancellationTokenSource;
|
|
||||||
var cancellationToken = cancellationTokenSource.Token;
|
|
||||||
|
|
||||||
var db = parameters.Db;
|
|
||||||
var queue = new ConcurrentQueue<DownloadItem>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (!cancellationToken.IsCancellationRequested) {
|
|
||||||
FillQueue(db, queue, cancellationToken);
|
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested && queue.TryDequeue(out var item)) {
|
|
||||||
var downloadUrl = item.DownloadUrl;
|
|
||||||
Log.Debug("Downloading " + downloadUrl + "...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
db.AddDownload(Data.Download.NewSuccess(item, await client.GetByteArrayAsync(downloadUrl, cancellationToken)));
|
|
||||||
} catch (HttpRequestException e) {
|
|
||||||
db.AddDownload(Data.Download.NewFailure(item, e.StatusCode, item.Size));
|
|
||||||
Log.Error(e);
|
|
||||||
} catch (Exception e) {
|
|
||||||
db.AddDownload(Data.Download.NewFailure(item, null, item.Size));
|
|
||||||
Log.Error(e);
|
|
||||||
} finally {
|
|
||||||
parameters.FireOnItemFinished(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
//
|
|
||||||
} catch (ObjectDisposedException) {
|
|
||||||
//
|
|
||||||
} finally {
|
|
||||||
cancellationTokenSource.Dispose();
|
|
||||||
parameters.FireOnServerStopped();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void FillQueue(IDatabaseFile db, ConcurrentQueue<DownloadItem> queue, CancellationToken cancellationToken) {
|
|
||||||
while (!cancellationToken.IsCancellationRequested && queue.IsEmpty) {
|
|
||||||
var newItems = db.GetEnqueuedDownloadItems(QueueSize);
|
|
||||||
if (newItems.Count == 0) {
|
|
||||||
Thread.Sleep(TimeSpan.FromMilliseconds(50));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
foreach (var item in newItems) {
|
|
||||||
queue.Enqueue(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
107
app/Server/Download/BackgroundDownloader.cs
Normal file
107
app/Server/Download/BackgroundDownloader.cs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using DHT.Utils.Models;
|
||||||
|
using DHT.Utils.Tasks;
|
||||||
|
|
||||||
|
namespace DHT.Server.Download;
|
||||||
|
|
||||||
|
public sealed class BackgroundDownloader : BaseModel {
|
||||||
|
private static readonly Log Log = Log.ForType<BackgroundDownloader>();
|
||||||
|
|
||||||
|
private const int DownloadTasks = 4;
|
||||||
|
private const int QueueSize = 25;
|
||||||
|
private const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
|
public event EventHandler<DownloadItem>? OnItemFinished;
|
||||||
|
|
||||||
|
private readonly Channel<DownloadItem> downloadQueue = Channel.CreateBounded<DownloadItem>(new BoundedChannelOptions(QueueSize) {
|
||||||
|
SingleReader = false,
|
||||||
|
SingleWriter = true,
|
||||||
|
AllowSynchronousContinuations = false,
|
||||||
|
FullMode = BoundedChannelFullMode.Wait
|
||||||
|
});
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
||||||
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
|
private readonly IDatabaseFile db;
|
||||||
|
private readonly Task queueWriterTask;
|
||||||
|
private readonly Task[] downloadTasks;
|
||||||
|
|
||||||
|
public BackgroundDownloader(IDatabaseFile db) {
|
||||||
|
this.cancellationToken = cancellationTokenSource.Token;
|
||||||
|
this.db = db;
|
||||||
|
this.queueWriterTask = Task.Run(RunQueueWriterTask);
|
||||||
|
this.downloadTasks = Enumerable.Range(1, DownloadTasks).Select(taskIndex => Task.Run(() => RunDownloadTask(taskIndex))).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunQueueWriterTask() {
|
||||||
|
while (await downloadQueue.Writer.WaitToWriteAsync(cancellationToken)) {
|
||||||
|
var newItems = db.PullEnqueuedDownloadItems(QueueSize);
|
||||||
|
if (newItems.Count == 0) {
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var newItem in newItems) {
|
||||||
|
await downloadQueue.Writer.WriteAsync(newItem, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunDownloadTask(int taskIndex) {
|
||||||
|
var log = Log.ForType<BackgroundDownloader>("Task " + taskIndex);
|
||||||
|
|
||||||
|
var client = new HttpClient();
|
||||||
|
client.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgent);
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
while (!cancellationToken.IsCancellationRequested) {
|
||||||
|
var item = await downloadQueue.Reader.ReadAsync(cancellationToken);
|
||||||
|
log.Debug("Downloading " + item.DownloadUrl + "...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
var downloadedBytes = await client.GetByteArrayAsync(item.DownloadUrl, cancellationToken);
|
||||||
|
db.AddDownload(Data.Download.NewSuccess(item, downloadedBytes));
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
// Ignore.
|
||||||
|
} catch (HttpRequestException e) {
|
||||||
|
db.AddDownload(Data.Download.NewFailure(item, e.StatusCode, item.Size));
|
||||||
|
log.Error(e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
db.AddDownload(Data.Download.NewFailure(item, null, item.Size));
|
||||||
|
log.Error(e);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
OnItemFinished?.Invoke(this, item);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.Error("Caught exception in event handler: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Stop() {
|
||||||
|
try {
|
||||||
|
await cancellationTokenSource.CancelAsync();
|
||||||
|
} catch (Exception) {
|
||||||
|
Log.Warn("Attempted to stop background download twice.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadQueue.Writer.Complete();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queueWriterTask.WaitIgnoringCancellation();
|
||||||
|
await Task.WhenAll(downloadTasks).WaitIgnoringCancellation();
|
||||||
|
} finally {
|
||||||
|
cancellationTokenSource.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
app/Utils/Tasks/TaskExtensions.cs
Normal file
12
app/Utils/Tasks/TaskExtensions.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DHT.Utils.Tasks;
|
||||||
|
|
||||||
|
public static class TaskExtensions {
|
||||||
|
public static async Task WaitIgnoringCancellation(this Task task) {
|
||||||
|
try {
|
||||||
|
await task;
|
||||||
|
} catch (OperationCanceledException) {}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user