Toplu indirme ve Eklenti Güncellemesi

This commit is contained in:
hOLOlu
2026-05-09 11:33:53 +03:00
parent 729879c693
commit 75bd47c052
349 changed files with 5292 additions and 401 deletions

View File

@@ -15,4 +15,30 @@ public class CategoryRepository : ICategoryRepository
public async Task<DownloadCategory?> GetByIdAsync(int id)
=> await _db.Categories.FindAsync(id);
public async Task AddAsync(DownloadCategory category)
{
_db.Categories.Add(category);
await _db.SaveChangesAsync();
}
public async Task UpdateAsync(DownloadCategory category)
{
var existing = await _db.Categories.FindAsync(category.Id);
if (existing != null)
{
_db.Entry(existing).CurrentValues.SetValues(category);
await _db.SaveChangesAsync();
}
}
public async Task DeleteAsync(int id)
{
var category = await _db.Categories.FindAsync(id);
if (category != null)
{
_db.Categories.Remove(category);
await _db.SaveChangesAsync();
}
}
}

View File

@@ -18,6 +18,9 @@ public interface ICategoryRepository
{
Task<IEnumerable<DownloadCategory>> GetAllAsync();
Task<DownloadCategory?> GetByIdAsync(int id);
Task AddAsync(DownloadCategory category);
Task UpdateAsync(DownloadCategory category);
Task DeleteAsync(int id);
}
public interface ISettingsRepository

View File

@@ -52,6 +52,25 @@ public class DownloadEngine
private async Task ProcessDownloadAsync(DownloadItem item, CancellationToken ct)
{
// Kuyruktan çıktıktan sonra güncel durumu veritabanından kontrol et
using (var scope = _serviceProvider.CreateScope())
{
var repo = scope.ServiceProvider.GetRequiredService<Data.Repositories.IDownloadRepository>();
var latest = await repo.GetByIdAsync(item.Id);
if (latest == null || latest.Status == DownloadStatus.Paused)
{
Serilog.Log.Information("İndirme {Id} duraklatılmış veya artık mevcut değil, işlem iptal edildi.", item.Id);
_queue.Release();
return;
}
// Eğer veritabanındaki durum farklıysa (örneğin başka bir yerden güncellendiyse) nesneyi güncelle
item.Status = latest.Status;
item.SavePath = latest.SavePath;
item.FileName = latest.FileName;
}
var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_activeDownloads[item.Id] = cts;
@@ -73,13 +92,6 @@ public class DownloadEngine
item.DownloadedBytes = 0; // Yeni indirme
}
// DURAKLATILMIŞSA İŞLEME (Kuyruktan yeni çıktıysa ve o sırada durdurulduysa)
if (item.Status == DownloadStatus.Paused)
{
Serilog.Log.Information("İndirme {Id} duraklatıldı, atlanıyor.", item.Id);
return;
}
item.Status = DownloadStatus.Downloading;
// Notify UI that we are starting

View File

@@ -4,7 +4,7 @@ public class EngineOptions
{
public int MaxSegments { get; set; } = 8;
public long MinSegmentBytes { get; set; } = 512 * 1024; // 512 KB
public int MaxRetries { get; set; } = 5;
public int MaxRetries { get; set; } = 30;
public int RetryDelayMs { get; set; } = 2000;
public int ConnectionTimeoutSeconds { get; set; } = 30;
public long SpeedLimitBytesPerSec { get; set; } = 0; // 0 = limitsiz

View File

@@ -128,21 +128,29 @@ public class SegmentedDownloader : IDownloader
private async Task DownloadSegmentAsync(DownloadItem item, DownloadSegment segment, string tempDir, IProgress<DownloadProgressEvent>? progress, CancellationToken ct)
{
int retryCount = 0;
while (true)
Interlocked.Increment(ref _activeSegmentCount);
try
{
try
while (true)
{
await PerformSegmentDownloadAsync(item, segment, tempDir, progress, ct);
break;
}
catch (Exception ex) when (retryCount < _options.MaxRetries && !ct.IsCancellationRequested)
{
retryCount++;
Serilog.Log.Warning("Parça {Index} indirilemedi (Deneme {Count}/{Max}): {Error}",
segment.Index, retryCount, _options.MaxRetries, ex.Message);
await Task.Delay(_options.RetryDelayMs * retryCount, ct);
try
{
await PerformSegmentDownloadAsync(item, segment, tempDir, progress, ct);
break;
}
catch (Exception ex) when (retryCount < _options.MaxRetries && !ct.IsCancellationRequested)
{
retryCount++;
Serilog.Log.Warning("Parça {Index} indirilemedi (Deneme {Count}/{Max}): {Error}",
segment.Index, retryCount, _options.MaxRetries, ex.Message);
await Task.Delay((_options.RetryDelayMs * retryCount) + Random.Shared.Next(1000), ct);
}
}
}
finally
{
Interlocked.Decrement(ref _activeSegmentCount);
}
}
private async Task PerformSegmentDownloadAsync(DownloadItem item, DownloadSegment segment, string tempDir, IProgress<DownloadProgressEvent>? progress, CancellationToken ct)
@@ -168,9 +176,15 @@ public class SegmentedDownloader : IDownloader
var buffer = new byte[8192];
int read;
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length, ct)) > 0)
while (true)
{
await fileStream.WriteAsync(buffer, 0, read, ct);
// Read with timeout to prevent hanging on network loss
read = await stream.ReadAsync(buffer, ct).AsTask()
.WaitAsync(TimeSpan.FromSeconds(_options.ConnectionTimeoutSeconds), ct);
if (read == 0) break;
await fileStream.WriteAsync(buffer.AsMemory(0, read), ct);
segment.Downloaded += read;
lock (_progressLock)
@@ -186,6 +200,8 @@ public class SegmentedDownloader : IDownloader
segment.Status = SegmentStatus.Completed;
}
private int _activeSegmentCount = 0;
private void ReportProgress(DownloadItem item, IProgress<DownloadProgressEvent>? progress)
{
if ((DateTime.UtcNow - _lastReport).TotalMilliseconds < 250) return;
@@ -203,7 +219,8 @@ public class SegmentedDownloader : IDownloader
ProgressPercent = percent,
SpeedBytesPerSec = _speedCalculator.GetBytesPerSecond(),
EstimatedRemaining = _speedCalculator.EstimateRemaining(item.TotalSize - downloaded),
Status = DownloadStatus.Downloading
Status = DownloadStatus.Downloading,
ActiveSegments = _activeSegmentCount
});
}

View File

@@ -43,7 +43,7 @@ public class SingleDownloader : IDownloader
retryCount++;
Serilog.Log.Warning("Tekil indirme başarısız (Deneme {Count}/{Max}): {Error}",
retryCount, _options.MaxRetries, ex.Message);
await Task.Delay(_options.RetryDelayMs * retryCount, ct);
await Task.Delay((_options.RetryDelayMs * retryCount) + Random.Shared.Next(1000), ct);
}
}
}
@@ -87,9 +87,15 @@ public class SingleDownloader : IDownloader
var buffer = new byte[8192];
int read;
while ((read = await contentStream.ReadAsync(buffer, 0, buffer.Length, ct)) > 0)
while (true)
{
await fileStream.WriteAsync(buffer, 0, read, ct);
// Read with timeout to prevent hanging on network loss
read = await contentStream.ReadAsync(buffer, ct).AsTask()
.WaitAsync(TimeSpan.FromSeconds(_options.ConnectionTimeoutSeconds), ct);
if (read <= 0) break;
await fileStream.WriteAsync(buffer.AsMemory(0, read), ct);
item.DownloadedBytes += read;
_speedCalculator.AddSample(read);

View File

@@ -12,4 +12,5 @@ public class DownloadProgressEvent
public long SpeedBytesPerSec { get; set; }
public TimeSpan? EstimatedRemaining { get; set; }
public DownloadStatus Status { get; set; }
public int ActiveSegments { get; set; }
}

View File

@@ -1,6 +1,9 @@
using DownloadManager.Core.Data.Repositories;
using DownloadManager.Core.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace DownloadManager.Core.Services;
@@ -8,6 +11,10 @@ namespace DownloadManager.Core.Services;
public interface ICategoryService
{
Task<IEnumerable<DownloadCategory>> GetCategoriesAsync();
Task AddCategoryAsync(DownloadCategory category);
Task UpdateCategoryAsync(DownloadCategory category);
Task DeleteCategoryAsync(int id);
Task<int> GetCategoryIdByExtensionAsync(string fileName);
}
public class CategoryService : ICategoryService
@@ -17,4 +24,34 @@ public class CategoryService : ICategoryService
public Task<IEnumerable<DownloadCategory>> GetCategoriesAsync()
=> _repository.GetAllAsync();
public Task AddCategoryAsync(DownloadCategory category)
=> _repository.AddAsync(category);
public Task UpdateCategoryAsync(DownloadCategory category)
=> _repository.UpdateAsync(category);
public Task DeleteCategoryAsync(int id)
=> _repository.DeleteAsync(id);
public async Task<int> GetCategoryIdByExtensionAsync(string fileName)
{
var ext = Path.GetExtension(fileName).TrimStart('.').ToLower();
var categories = await _repository.GetAllAsync();
foreach (var cat in categories)
{
if (string.IsNullOrWhiteSpace(cat.Extensions)) continue;
var extensions = cat.Extensions.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim().ToLower());
if (extensions.Contains(ext))
{
return cat.Id;
}
}
var other = categories.FirstOrDefault(c => c.Name == "Other" || c.Name == "Diğer");
return other?.Id ?? 6;
}
}

View File

@@ -45,10 +45,15 @@ public class DownloadService : IDownloadService
await _repository.UpdateAsync(item);
}
public Task PauseDownloadAsync(Guid id)
public async Task PauseDownloadAsync(Guid id)
{
var item = await _repository.GetByIdAsync(id);
if (item != null)
{
item.Status = Enums.DownloadStatus.Paused;
await _repository.UpdateAsync(item);
}
_engine.CancelDownload(id);
return Task.CompletedTask;
}
public async Task ResumeDownloadAsync(Guid id)

View File

@@ -1,4 +1,5 @@
using DownloadManager.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading;
@@ -8,15 +9,13 @@ namespace DownloadManager.Core.Services;
public class SchedulerWatchdog
{
private readonly IScheduleService _scheduleService;
private readonly IDownloadService _downloadService;
private readonly IServiceProvider _serviceProvider;
private Timer? _timer;
private bool _isProcessing;
public SchedulerWatchdog(IScheduleService scheduleService, IDownloadService downloadService)
public SchedulerWatchdog(IServiceProvider serviceProvider)
{
_scheduleService = scheduleService;
_downloadService = downloadService;
_serviceProvider = serviceProvider;
}
public void Start()
@@ -39,7 +38,10 @@ public class SchedulerWatchdog
try
{
var jobs = await _scheduleService.GetJobsAsync();
using var scope = _serviceProvider.CreateScope();
var scheduleService = scope.ServiceProvider.GetRequiredService<IScheduleService>();
var jobs = await scheduleService.GetJobsAsync();
var activeJob = jobs.FirstOrDefault(j => j.IsActive);
if (activeJob == null) return;
@@ -49,9 +51,6 @@ public class SchedulerWatchdog
// Gün kontrolü
int dayIndex = (int)now.DayOfWeek; // 0=Pazar, 1=Pazartesi...
// Model Pt=0, Pz=6 şeklinde varsayılmıştı (1111111)
// .NET DayOfWeek: Sunday=0, Monday=1...
// Dönüştürelim: Pt=0 için
int adjustedIndex = dayIndex == 0 ? 6 : dayIndex - 1;
if (activeJob.DaysOfWeek[adjustedIndex] == '0') return;
@@ -60,10 +59,9 @@ public class SchedulerWatchdog
if (activeJob.StartTime.HasValue)
{
var startTime = activeJob.StartTime.Value.TimeOfDay;
// Başlangıç vaktinden sonraki 1 dakika içindeysek başlat (sürekli tetiklenmemesi için)
if (currentTime >= startTime && currentTime < startTime.Add(TimeSpan.FromMinutes(1)))
{
await StartScheduledDownloads();
await StartScheduledDownloads(scope.ServiceProvider.GetRequiredService<IDownloadService>());
}
}
@@ -73,7 +71,7 @@ public class SchedulerWatchdog
var endTime = activeJob.EndTime.Value.TimeOfDay;
if (currentTime >= endTime && currentTime < endTime.Add(TimeSpan.FromMinutes(1)))
{
await StopAllDownloads();
await StopAllDownloads(scope.ServiceProvider.GetRequiredService<IDownloadService>());
}
}
}
@@ -87,27 +85,27 @@ public class SchedulerWatchdog
}
}
private async Task StartScheduledDownloads()
private async Task StartScheduledDownloads(IDownloadService downloadService)
{
Serilog.Log.Information("Zamanlayıcı: İndirmeler başlatılıyor...");
var downloads = await _downloadService.GetAllDownloadsAsync();
var downloads = await downloadService.GetAllDownloadsAsync();
var pending = downloads.Where(d => d.Status == Enums.DownloadStatus.Paused || d.Status == Enums.DownloadStatus.Pending);
foreach (var item in pending)
{
await _downloadService.ResumeDownloadAsync(item.Id);
await downloadService.ResumeDownloadAsync(item.Id);
}
}
private async Task StopAllDownloads()
private async Task StopAllDownloads(IDownloadService downloadService)
{
Serilog.Log.Information("Zamanlayıcı: İndirmeler durduruluyor...");
var downloads = await _downloadService.GetAllDownloadsAsync();
var downloads = await downloadService.GetAllDownloadsAsync();
var active = downloads.Where(d => d.Status == Enums.DownloadStatus.Downloading || d.Status == Enums.DownloadStatus.Queued);
foreach (var item in active)
{
await _downloadService.PauseDownloadAsync(item.Id);
await downloadService.PauseDownloadAsync(item.Id);
}
}
}

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("DownloadManager.Core")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+81faa715a4eb11a2bf648f52891306b46ea7c256")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+561a953b1f10b7ec16bef81fdcd4030ad9d5f1e4")]
[assembly: System.Reflection.AssemblyProductAttribute("DownloadManager.Core")]
[assembly: System.Reflection.AssemblyTitleAttribute("DownloadManager.Core")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
57eee8222b0d77ae6e394591882c8d91daaa59ea38bf4b29e8d5d32072290d1d
355e174528e4d22ee7c251d97b3ad390b2cefbca24bdd6f2d576299a7fce3925

View File

@@ -1,7 +1,6 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -14,10 +13,10 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("DownloadManager.Core")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+81faa715a4eb11a2bf648f52891306b46ea7c256")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+561a953b1f10b7ec16bef81fdcd4030ad9d5f1e4")]
[assembly: System.Reflection.AssemblyProductAttribute("DownloadManager.Core")]
[assembly: System.Reflection.AssemblyTitleAttribute("DownloadManager.Core")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Generated by the MSBuild WriteCodeFragment class.
// MSBuild WriteCodeFragment sınıfı tarafından oluşturuldu.

View File

@@ -1 +1 @@
86922a9eccfa1e5736aa220690f23d77712e45c8681c90dab58e4b60acfd7921
62c36dfd3abfff411d19bf2d9f22e0bd666e57c1af0629fcae7d4ef188fe9e95