diff --git a/CHANGELOG.md b/CHANGELOG.md index 60ed563..d228d37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.9.1] - 2021-10-24 +### Added +- Added automatic torrent retrying when RealDebrid reports an error on the torrent. + +### Changed +- Changed how Aria2 is polled, this will result in less RPC calls for a large amount of torrents. +- Add more and better logging for the torrent runner in Debug. +- Add Aria2 checks to see if links get added properly. +- Update Aria2.NET and RD.NET, added automatic retrying in case of server failures. + ## [1.9.0] - 2021-10-24 ### Added - Add priorty for torrents. You can set the priority when adding a new torrent. When added from Sonarr/Radarr it will assume no priority. diff --git a/client/src/app/navbar/navbar.component.html b/client/src/app/navbar/navbar.component.html index 4937688..983ccf6 100644 --- a/client/src/app/navbar/navbar.component.html +++ b/client/src/app/navbar/navbar.component.html @@ -55,7 +55,7 @@ Profile Logout - + diff --git a/client/src/app/torrent-status.pipe.ts b/client/src/app/torrent-status.pipe.ts index 2b9b7d6..3d3cfa3 100644 --- a/client/src/app/torrent-status.pipe.ts +++ b/client/src/app/torrent-status.pipe.ts @@ -39,7 +39,7 @@ export class TorrentStatusPipe implements PipeTransform { if (allSpeeds > 0) { speed = this.pipe.transform(allSpeeds, 'filesize'); - return `Downloading (${progress.toFixed(2)}% - ${speed}/s)`; + return `Downloading file ${downloading.length}/${torrent.downloads.length} (${progress.toFixed(2)}% - ${speed}/s)`; } } @@ -57,7 +57,7 @@ export class TorrentStatusPipe implements PipeTransform { let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length; if (allSpeeds > 0) { - return `Extracting (${progress.toFixed(2)}%)`; + return `Extracting file ${unpacking.length}/${torrent.downloads.length} (${progress.toFixed(2)}%)`; } } diff --git a/package.json b/package.json index ebf6cb5..6a4ded9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rdt-client", - "version": "1.9.0", + "version": "1.9.1", "description": "This is a web interface to manage your torrents on Real-Debrid.", "main": "index.js", "dependencies": { diff --git a/server/RdtClient.Data/Data/DownloadData.cs b/server/RdtClient.Data/Data/DownloadData.cs index 7521f43..cafc98a 100644 --- a/server/RdtClient.Data/Data/DownloadData.cs +++ b/server/RdtClient.Data/Data/DownloadData.cs @@ -35,6 +35,7 @@ namespace RdtClient.Data.Data public async Task Get(Guid torrentId, String path) { return await _dataContext.Downloads + .Include(m => m.Torrent) .AsNoTracking() .FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path); } @@ -241,6 +242,40 @@ namespace RdtClient.Data.Data await TorrentData.VoidCache(); } + public async Task Download(Guid downloadId) + { + var dbDownload = await _dataContext.Downloads + .FirstOrDefaultAsync(m => m.DownloadId == downloadId); + + dbDownload.DownloadStarted = null; + dbDownload.DownloadFinished = null; + dbDownload.UnpackingQueued = null; + dbDownload.UnpackingStarted = null; + dbDownload.UnpackingFinished = null; + dbDownload.Completed = null; + dbDownload.Error = null; + + await _dataContext.SaveChangesAsync(); + + await TorrentData.VoidCache(); + } + + public async Task Unpack(Guid downloadId) + { + var dbDownload = await _dataContext.Downloads + .FirstOrDefaultAsync(m => m.DownloadId == downloadId); + + dbDownload.UnpackingQueued = DateTimeOffset.UtcNow; + dbDownload.UnpackingStarted = null; + dbDownload.UnpackingFinished = null; + dbDownload.Completed = null; + dbDownload.Error = null; + + await _dataContext.SaveChangesAsync(); + + await TorrentData.VoidCache(); + } + public async Task Reset(Guid downloadId) { var dbDownload = await _dataContext.Downloads diff --git a/server/RdtClient.Data/RdtClient.Data.csproj b/server/RdtClient.Data/RdtClient.Data.csproj index 7c5eef3..0b49acd 100644 --- a/server/RdtClient.Data/RdtClient.Data.csproj +++ b/server/RdtClient.Data/RdtClient.Data.csproj @@ -5,11 +5,11 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index 751ea3d..0309b5c 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using RdtClient.Service.Services; +using RdtClient.Service.Services.TorrentClients; namespace RdtClient.Service { @@ -11,6 +12,7 @@ namespace RdtClient.Service services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/server/RdtClient.Service/Helpers/FileHelper.cs b/server/RdtClient.Service/Helpers/FileHelper.cs new file mode 100644 index 0000000..830e73c --- /dev/null +++ b/server/RdtClient.Service/Helpers/FileHelper.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace RdtClient.Service.Helpers +{ + public static class FileHelper + { + public static async Task Delete(String path) + { + if (String.IsNullOrWhiteSpace(path)) + { + return; + } + + if (!File.Exists(path)) + { + return; + } + + var retry = 0; + + while (true) + { + try + { + File.Delete(path); + + break; + } + catch + { + if (retry >= 3) + { + throw; + } + + retry++; + + await Task.Delay(1000 * retry); + } + } + } + } +} diff --git a/server/RdtClient.Service/Helpers/Logger.cs b/server/RdtClient.Service/Helpers/Logger.cs new file mode 100644 index 0000000..5d963d6 --- /dev/null +++ b/server/RdtClient.Service/Helpers/Logger.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Web; +using RdtClient.Data.Models.Data; + +namespace RdtClient.Service.Helpers +{ + public static class Logger + { + public static String ToLog(this Download download) + { + var fileName = download.Path; + + if (!String.IsNullOrWhiteSpace(download.Link)) + { + var uri = new Uri(download.Link); + fileName = uri.Segments.Last(); + fileName = HttpUtility.UrlDecode(fileName); + } + + var done = (Int32)((Double)download.BytesDone / download.BytesTotal) * 100; + + return $"for download {fileName} {done}% ({download.DownloadId})"; + } + + public static String ToLog(this Torrent torrent) + { + return $"for torrent {torrent.RdName} ({torrent.RdId} - {torrent.RdStatusRaw} {torrent.RdProgress}%) ({torrent.TorrentId})"; + } + } +} diff --git a/server/RdtClient.Service/Models/TorrentClient/TorrentClientAvailableFile.cs b/server/RdtClient.Service/Models/TorrentClient/TorrentClientAvailableFile.cs new file mode 100644 index 0000000..7e543b4 --- /dev/null +++ b/server/RdtClient.Service/Models/TorrentClient/TorrentClientAvailableFile.cs @@ -0,0 +1,11 @@ +using System; + +namespace RdtClient.Service.Models.TorrentClient +{ + public class TorrentClientAvailableFile + { + public String Filename { get; set; } + + public Int64 Filesize { get; set; } + } +} diff --git a/server/RdtClient.Service/Models/TorrentClient/TorrentClientTorrent.cs b/server/RdtClient.Service/Models/TorrentClient/TorrentClientTorrent.cs new file mode 100644 index 0000000..ce3c651 --- /dev/null +++ b/server/RdtClient.Service/Models/TorrentClient/TorrentClientTorrent.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace RdtClient.Service.Models.TorrentClient +{ + public class TorrentClientTorrent + { + public String Id { get; set; } + public String Filename { get; set; } + public String OriginalFilename { get; set; } + public String Hash { get; set; } + public Int64 Bytes { get; set; } + public Int64 OriginalBytes { get; set; } + public String Host { get; set; } + public Int64 Split { get; set; } + public Int64 Progress { get; set; } + public String Status { get; set; } + public DateTimeOffset Added { get; set; } + public List Files { get; set; } + public List Links { get; set; } + public DateTimeOffset? Ended { get; set; } + public Int64? Speed { get; set; } + public Int64? Seeders { get; set; } + } + + public class TorrentClientTorrentFile + { + public Int64 Id { get; set; } + public String Path { get; set; } + public Int64 Bytes { get; set; } + public Boolean Selected { get; set; } + } +} diff --git a/server/RdtClient.Service/Models/TorrentClient/TorrentClientUser.cs b/server/RdtClient.Service/Models/TorrentClient/TorrentClientUser.cs new file mode 100644 index 0000000..5697035 --- /dev/null +++ b/server/RdtClient.Service/Models/TorrentClient/TorrentClientUser.cs @@ -0,0 +1,10 @@ +using System; + +namespace RdtClient.Service.Models.TorrentClient +{ + public class TorrentClientUser + { + public String Username { get; set; } + public DateTimeOffset Expiration { get; set; } + } +} diff --git a/server/RdtClient.Service/RdtClient.Service.csproj b/server/RdtClient.Service/RdtClient.Service.csproj index 9b441ed..0fb6877 100644 --- a/server/RdtClient.Service/RdtClient.Service.csproj +++ b/server/RdtClient.Service/RdtClient.Service.csproj @@ -6,11 +6,11 @@ - + - + diff --git a/server/RdtClient.Service/Services/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs index 6278375..128cb21 100644 --- a/server/RdtClient.Service/Services/DownloadClient.cs +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Threading.Tasks; using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Internal; @@ -15,7 +14,7 @@ namespace RdtClient.Service.Services private readonly Download _download; private readonly Torrent _torrent; - private IDownloader _downloader; + public IDownloader Downloader; public DownloadClient(Download download, Torrent torrent, String destinationPath) { @@ -24,6 +23,8 @@ namespace RdtClient.Service.Services _destinationPath = destinationPath; } + public String Type { get; set; } + public Boolean Finished { get; private set; } public String Error { get; private set; } @@ -47,12 +48,11 @@ namespace RdtClient.Service.Services throw new Exception("Invalid download path"); } - if (File.Exists(filePath)) - { - File.Delete(filePath); - } + await FileHelper.Delete(filePath); - _downloader = settings.DownloadClient switch + Type = settings.DownloadClient; + + Downloader = settings.DownloadClient switch { "Simple" => new SimpleDownloader(_download.Link, filePath), "MultiPart" => new MultiDownloader(_download.Link, filePath, settings), @@ -60,20 +60,24 @@ namespace RdtClient.Service.Services _ => throw new Exception($"Unknown download client {settings.DownloadClient}") }; - _downloader.DownloadComplete += (_, args) => + Downloader.DownloadComplete += (_, args) => { Finished = true; Error = args.Error; }; - _downloader.DownloadProgress += (_, args) => + Downloader.DownloadProgress += (_, args) => { Speed = args.Speed; BytesDone = args.BytesDone; BytesTotal = args.BytesTotal; }; - return await _downloader.Download(); + var result = await Downloader.Download(); + + await Task.Delay(1000); + + return result; } catch (Exception ex) { @@ -86,29 +90,29 @@ namespace RdtClient.Service.Services public async Task Cancel() { - if (_downloader == null) + if (Downloader == null) { return; } - await _downloader.Cancel(); + await Downloader.Cancel(); } public async Task Pause() { - if (_downloader == null) + if (Downloader == null) { return; } - await _downloader.Pause(); + await Downloader.Pause(); } public async Task Resume() { - if (_downloader == null) + if (Downloader == null) { return; } - await _downloader.Resume(); + await Downloader.Resume(); } } } diff --git a/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs b/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs index 8002e55..3b6ad40 100644 --- a/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; -using System.Timers; using Aria2NET; using RdtClient.Data.Models.Internal; @@ -20,8 +20,7 @@ namespace RdtClient.Service.Services.Downloaders private readonly String _filePath; private readonly Aria2NetClient _aria2NetClient; - private readonly Timer _timer; - + private String _gid; public Aria2cDownloader(String gid, String uri, String filePath, DbSettings settings) @@ -35,14 +34,7 @@ namespace RdtClient.Service.Services.Downloaders Timeout = TimeSpan.FromSeconds(10) }; - _aria2NetClient = new Aria2NetClient(settings.Aria2cUrl, settings.Aria2cSecret, httpClient); - - _timer = new Timer(); - - _timer.Elapsed += OnTimedEvent; - - _timer.Interval = 1000; - _timer.Enabled = false; + _aria2NetClient = new Aria2NetClient(settings.Aria2cUrl, settings.Aria2cSecret, httpClient, 10); } public async Task Download() @@ -50,16 +42,11 @@ namespace RdtClient.Service.Services.Downloaders var path = Path.GetDirectoryName(_filePath); var fileName = Path.GetFileName(_filePath); - if (_gid != null) + var isAlreadyAdded = await CheckIfAdded(); + + if (isAlreadyAdded) { - try - { - await _aria2NetClient.TellStatus(_gid); - } - catch - { - _gid = null; - } + throw new Exception($"The download link {_uri} has already been added to Aria2"); } var retryCount = 0; @@ -67,6 +54,20 @@ namespace RdtClient.Service.Services.Downloaders { try { + if (_gid != null) + { + try + { + await _aria2NetClient.TellStatus(_gid); + + return _gid; + } + catch + { + _gid = null; + } + } + _gid ??= await _aria2NetClient.AddUri(new List { _uri @@ -81,7 +82,9 @@ namespace RdtClient.Service.Services.Downloaders } }); - break; + await _aria2NetClient.TellStatus(_gid); + + return _gid; } catch { @@ -95,19 +98,10 @@ namespace RdtClient.Service.Services.Downloaders retryCount++; } } - - // Add a delay to prevent sending too many Add requests to Aria2 at the same time. - await Task.Delay(1000); - - _timer.Start(); - - return _gid; } public async Task Cancel() { - _timer.Stop(); - if (String.IsNullOrWhiteSpace(_gid)) { return; @@ -166,46 +160,82 @@ namespace RdtClient.Service.Services.Downloaders } } - private async void OnTimedEvent(Object source, ElapsedEventArgs e) + public async Task Update(IEnumerable allDownloads) { if (_gid == null) { return; } - try + var download = allDownloads.FirstOrDefault(m => m.Gid == _gid); + + if (download == null) { - var status = await _aria2NetClient.TellStatus(_gid); - - if (!String.IsNullOrWhiteSpace(status.ErrorMessage) || status.Status == "error") - { - await Cancel(); - DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs - { - Error = $"{status.ErrorCode}: {status.ErrorMessage}" - }); - return; - } - - if (status.Status == "complete" || status.Status == "removed") - { - await Cancel(); - DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs()); - return; - } - - DownloadProgress?.Invoke(this, new DownloadProgressEventArgs - { - BytesDone = status.CompletedLength, - BytesTotal = status.TotalLength, - Speed = status.DownloadSpeed - }); + return; } - catch + + if (!String.IsNullOrWhiteSpace(download.ErrorMessage) || download.Status == "error") { await Cancel(); - DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs()); + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs + { + Error = $"{download.ErrorCode}: {download.ErrorMessage}" + }); + return; } + + if (download.Status == "complete" || download.Status == "removed") + { + await Cancel(); + + var retryCount = 0; + while (true) + { + if (retryCount >= 10) + { + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs + { + Error = $"File not found at {_filePath}" + }); + break; + } + + if (File.Exists(_filePath)) + { + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs()); + break; + } + + await Task.Delay(1000 * retryCount); + retryCount++; + } + return; + } + + DownloadProgress?.Invoke(this, new DownloadProgressEventArgs + { + BytesDone = download.CompletedLength, + BytesTotal = download.TotalLength, + Speed = download.DownloadSpeed + }); + } + + private async Task CheckIfAdded() + { + var allDownloads = await _aria2NetClient.TellAll(); + + foreach (var download in allDownloads.Where(m => m.Files != null)) + { + foreach (var file in download.Files.Where(m => m.Uris != null)) + { + if (file.Uris.Any(uri => uri.Uri == _uri)) + { + return true; + } + } + } + + return false; } } } diff --git a/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs b/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs index f0101e0..4268e00 100644 --- a/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs @@ -28,9 +28,9 @@ namespace RdtClient.Service.Services.Downloaders _filePath = filePath; } - public async Task Download() + public Task Download() { - Task.Run(async () => + _ = Task.Run(async () => { await StartDownloadTask(); }); diff --git a/server/RdtClient.Service/Services/Downloads.cs b/server/RdtClient.Service/Services/Downloads.cs index 61007ac..6ccae14 100644 --- a/server/RdtClient.Service/Services/Downloads.cs +++ b/server/RdtClient.Service/Services/Downloads.cs @@ -90,6 +90,16 @@ namespace RdtClient.Service.Services await _downloadData.DeleteForTorrent(torrentId); } + public async Task UpdateDownload(Guid downloadId) + { + await _downloadData.Download(downloadId); + } + + public async Task UpdateUnpack(Guid downloadId) + { + await _downloadData.Unpack(downloadId); + } + public async Task Reset(Guid downloadId) { await _downloadData.Reset(downloadId); diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index fbb6be8..db267e5 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -293,25 +293,19 @@ namespace RdtClient.Service.Services Upspeed = speed }; - if (torrent.Completed.HasValue && torrent.Downloads.All(m => m.Completed.HasValue)) + if (torrent.Completed.HasValue) { - // Indicates completed torrent and not seeding anymore. - result.State = "pausedUP"; + var allDownloadsComplete = torrent.Downloads.All(m => m.Completed.HasValue); + var hasDownloadsWithErrors = torrent.Downloads.Any(m => m.Error != null); - if (torrent.Downloads.Count > 0) + if (torrent.Downloads.Count == 0 || hasDownloadsWithErrors || torrent.RdStatus == RealDebridStatus.Error) { - var error = torrent.Downloads.FirstOrDefault(m => m.Error != null); - - if (error != null) - { - result.State = "error"; - } + result.State = "error"; + } + else if (allDownloadsComplete) + { + result.State = "pausedUP"; } - } - - if (torrent.RdStatus == RealDebridStatus.Error) - { - result.State = "error"; } results.Add(result); @@ -538,7 +532,7 @@ namespace RdtClient.Service.Services { if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) { - downloadClient.Pause(); + await downloadClient.Pause(); } } } @@ -558,7 +552,7 @@ namespace RdtClient.Service.Services { if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) { - downloadClient.Resume(); + await downloadClient.Resume(); } } } diff --git a/server/RdtClient.Service/Services/Settings.cs b/server/RdtClient.Service/Services/Settings.cs index 1d503ad..2c80e0b 100644 --- a/server/RdtClient.Service/Services/Settings.cs +++ b/server/RdtClient.Service/Services/Settings.cs @@ -8,6 +8,7 @@ using Aria2NET; using RdtClient.Data.Data; using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Internal; +using RdtClient.Service.Helpers; namespace RdtClient.Service.Services { @@ -54,7 +55,8 @@ namespace RdtClient.Service.Services var testFile = $"{path}/test.txt"; await File.WriteAllTextAsync(testFile, "RealDebridClient Test File, you can remove this file."); - File.Delete(testFile); + + await FileHelper.Delete(testFile); } public async Task TestDownloadSpeed(CancellationToken cancellationToken) @@ -63,10 +65,7 @@ namespace RdtClient.Service.Services var testFilePath = Path.Combine(downloadPath, "testDefault.rar"); - if (File.Exists(testFilePath)) - { - File.Delete(testFilePath); - } + await FileHelper.Delete(testFilePath); var download = new Download { @@ -90,23 +89,20 @@ namespace RdtClient.Service.Services if (cancellationToken.IsCancellationRequested) { - downloadClient.Cancel(); + await downloadClient.Cancel(); } if (downloadClient.BytesDone > 1024 * 1024 * 50) { - downloadClient.Cancel(); + await downloadClient.Cancel(); break; } } - if (File.Exists(testFilePath)) - { - File.Delete(testFilePath); - } + await FileHelper.Delete(testFilePath); - Clean(); + await Clean(); return downloadClient.Speed; } @@ -117,10 +113,7 @@ namespace RdtClient.Service.Services var testFilePath = Path.Combine(downloadPath, "test.tmp"); - if (File.Exists(testFilePath)) - { - File.Delete(testFilePath); - } + await FileHelper.Delete(testFilePath); const Int32 testFileSize = 64 * 1024 * 1024; @@ -147,10 +140,7 @@ namespace RdtClient.Service.Services fileStream.Close(); - if (File.Exists(testFilePath)) - { - File.Delete(testFilePath); - } + await FileHelper.Delete(testFilePath); return writeSpeed; } @@ -162,7 +152,7 @@ namespace RdtClient.Service.Services return await client.GetVersion(); } - public void Clean() + public async Task Clean() { try { @@ -174,7 +164,7 @@ namespace RdtClient.Service.Services foreach (var file in files) { - File.Delete(file); + await FileHelper.Delete(file); } } } diff --git a/server/RdtClient.Service/Services/Startup.cs b/server/RdtClient.Service/Services/Startup.cs index 3d5fafa..8acd183 100644 --- a/server/RdtClient.Service/Services/Startup.cs +++ b/server/RdtClient.Service/Services/Startup.cs @@ -23,7 +23,7 @@ namespace RdtClient.Service.Services await settings.ResetCache(); - settings.Clean(); + await settings.Clean(); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/server/RdtClient.Service/Services/TaskRunner.cs b/server/RdtClient.Service/Services/TaskRunner.cs index 9919c3e..faa385e 100644 --- a/server/RdtClient.Service/Services/TaskRunner.cs +++ b/server/RdtClient.Service/Services/TaskRunner.cs @@ -33,18 +33,12 @@ namespace RdtClient.Service.Services { try { - await Torrents.TorrentResetLock.WaitAsync(stoppingToken); - await torrentRunner.Tick(); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error occurred in TorrentDownloadManager.Tick"); } - finally - { - Torrents.TorrentResetLock.Release(); - } await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); } diff --git a/server/RdtClient.Service/Services/TorrentClients/ITorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/ITorrentClient.cs new file mode 100644 index 0000000..260910d --- /dev/null +++ b/server/RdtClient.Service/Services/TorrentClients/ITorrentClient.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using RdtClient.Service.Models.TorrentClient; + +namespace RdtClient.Service.Services.TorrentClients +{ + public interface ITorrentClient + { + Task> GetTorrents(); + Task GetUser(); + Task AddMagnet(String magnetLink); + Task AddFile(Byte[] bytes); + Task> GetAvailableFiles(String hash); + Task SelectFiles(String torrentId, IList fileIds); + Task GetInfo(String torrentId); + Task Delete(String torrentId); + Task Unrestrict(String link); + } +} diff --git a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs new file mode 100644 index 0000000..9fd3154 --- /dev/null +++ b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using RDNET; +using RdtClient.Service.Models.TorrentClient; + +namespace RdtClient.Service.Services.TorrentClients +{ + public class RealDebridTorrentClient : ITorrentClient + { + private readonly IHttpClientFactory _httpClientFactory; + + public RealDebridTorrentClient(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + private RdNetClient GetRdNetClient() + { + var apiKey = Settings.Get.RealDebridApiKey; + + if (String.IsNullOrWhiteSpace(apiKey)) + { + throw new Exception("Real-Debrid API Key not set in the settings"); + } + + var httpClient = _httpClientFactory.CreateClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + + var rdtNetClient = new RdNetClient(null, httpClient, 5); + rdtNetClient.UseApiAuthentication(apiKey); + + return rdtNetClient; + } + + private static TorrentClientTorrent Map(Torrent torrent) + { + return new TorrentClientTorrent + { + Id = torrent.Id, + Filename = torrent.Filename, + OriginalFilename = torrent.OriginalFilename, + Hash = torrent.Hash, + Bytes = torrent.Bytes, + OriginalBytes = torrent.OriginalBytes, + Host = torrent.Host, + Split = torrent.Split, + Progress = torrent.Progress, + Status = torrent.Status, + Added = torrent.Added, + Files = (torrent.Files ?? new List()).Select(m => new TorrentClientTorrentFile + { + Path = m.Path, + Bytes = m.Bytes, + Id = m.Id, + Selected = m.Selected + }).ToList(), + Links = torrent.Links, + Ended = torrent.Ended, + Speed = torrent.Speed, + Seeders = torrent.Seeders, + }; + } + + public async Task> GetTorrents() + { + var page = 0; + var results = new List(); + + while (true) + { + var pagedResults = await GetRdNetClient().Torrents.GetAsync(page, 100); + + results.AddRange(pagedResults); + + if (pagedResults.Count == 0) + { + break; + } + + page += 100; + } + + return results.Select(Map).ToList(); + } + + public async Task GetUser() + { + var user = await GetRdNetClient().User.GetAsync(); + + return new TorrentClientUser + { + Username = user.Username, + Expiration = user.Expiration + }; + } + + public async Task AddMagnet(String magnetLink) + { + var result = await GetRdNetClient().Torrents.AddMagnetAsync(magnetLink); + + return result.Id; + } + + public async Task AddFile(Byte[] bytes) + { + var result = await GetRdNetClient().Torrents.AddFileAsync(bytes); + + return result.Id; + } + + public async Task> GetAvailableFiles(String hash) + { + var result = await GetRdNetClient().Torrents.GetAvailableFiles(hash); + + var files = result.SelectMany(m => m.Value).SelectMany(m => m.Value).SelectMany(m => m.Values); + + var groups = files.GroupBy(m => $"{m.Filename}-{m.Filesize}"); + + var torrentClientAvailableFiles = groups.Select(m => new TorrentClientAvailableFile + { + Filename = m.First().Filename, + Filesize = m.First().Filesize + } ).ToList(); + + return torrentClientAvailableFiles; + } + + public async Task SelectFiles(String torrentId, IList fileIds) + { + await GetRdNetClient().Torrents.SelectFilesAsync(torrentId, fileIds.ToArray()); + } + + public async Task GetInfo(String torrentId) + { + var result = await GetRdNetClient().Torrents.GetInfoAsync(torrentId); + + return Map(result); + } + + public async Task Delete(String torrentId) + { + await GetRdNetClient().Torrents.DeleteAsync(torrentId); + } + + public async Task Unrestrict(String link) + { + var result = await GetRdNetClient().Unrestrict.LinkAsync(link); + + return result.Download; + } + } +} diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs index d35217a..ae7a836 100644 --- a/server/RdtClient.Service/Services/TorrentRunner.cs +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -3,10 +3,17 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using System.Web; +using Aria2NET; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using RdtClient.Data.Enums; -using Serilog; +using RdtClient.Data.Models.Data; +using RdtClient.Data.Models.Internal; +using RdtClient.Service.Helpers; +using RdtClient.Service.Services.Downloaders; namespace RdtClient.Service.Services { @@ -19,54 +26,75 @@ namespace RdtClient.Service.Services public static readonly ConcurrentDictionary ActiveDownloadClients = new(); public static readonly ConcurrentDictionary ActiveUnpackClients = new(); + + private readonly ILogger _logger; + private readonly Torrents _torrents; private readonly Downloads _downloads; private readonly RemoteService _remoteService; - - private readonly Torrents _torrents; + private readonly HttpClient _httpClient; - public TorrentRunner(Torrents torrents, Downloads downloads, RemoteService remoteService) + public TorrentRunner(ILogger logger, Torrents torrents, Downloads downloads, RemoteService remoteService) { + _logger = logger; _torrents = torrents; _downloads = downloads; _remoteService = remoteService; + + _httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(10) + }; } public async Task Initialize() { + Log("Initializing TorrentRunner"); + + var settingsCopy = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(Settings.Get)); + + if (settingsCopy != null) + { + settingsCopy.RealDebridApiKey = "*****"; + settingsCopy.Aria2cSecret = "*****"; + + Log(JsonConvert.SerializeObject(settingsCopy)); + } + // When starting up reset any pending downloads or unpackings so that they are restarted. var torrents = await _torrents.Get(); torrents = torrents.Where(m => m.Completed == null).ToList(); - - var downloads = torrents.SelectMany(m => m.Downloads) - .Where(m => m.DownloadQueued != null && m.DownloadStarted != null && m.DownloadFinished == null && m.Error == null) - .OrderBy(m => m.DownloadQueued); - foreach (var download in downloads) + Log($"Found {torrents.Count} not completed torrents"); + + foreach (var torrent in torrents) { - await _downloads.UpdateDownloadStarted(download.DownloadId, null); + foreach (var download in torrent.Downloads) + { + if (download.DownloadQueued != null && download.DownloadStarted != null && download.DownloadFinished == null && download.Error == null) + { + Log("Resetting download status", download, torrent); + + await _downloads.UpdateDownloadStarted(download.DownloadId, null); + } + + if (download.UnpackingQueued != null && download.UnpackingStarted != null && download.UnpackingFinished == null && download.Error == null) + { + Log("Resetting unpack status", download, torrent); + + await _downloads.UpdateUnpackingStarted(download.DownloadId, null); + } + } } - var unpacks = torrents.SelectMany(m => m.Downloads) - .Where(m => m.UnpackingQueued != null && m.UnpackingStarted != null && m.UnpackingFinished == null && m.Error == null) - .OrderBy(m => m.DownloadQueued); - - foreach (var download in unpacks) - { - await _downloads.UpdateUnpackingStarted(download.DownloadId, null); - } + Log("TorrentRunner Initialized"); } public async Task Tick() { - var sw = new Stopwatch(); - sw.Start(); - - Log.Debug("TorrentRunner Tick Start"); - if (String.IsNullOrWhiteSpace(Settings.Get.RealDebridApiKey)) { - Log.Debug($"No RealDebridApiKey set!"); + Log($"No RealDebridApiKey set in settings"); return; } @@ -85,304 +113,369 @@ namespace RdtClient.Service.Services var settingDownloadPath = Settings.Get.DownloadPath; if (String.IsNullOrWhiteSpace(settingDownloadPath)) { + _logger.LogError("No DownloadPath set in settings"); return; } - Log.Debug($"Currently {ActiveDownloadClients.Count} active downloads"); - Log.Debug($"Currently {ActiveUnpackClients.Count} active unpacks"); + var sw = new Stopwatch(); + sw.Start(); + + if (ActiveDownloadClients.Count > 0 || ActiveUnpackClients.Count > 0) + { + Log($"TorrentRunner Tick Start, {ActiveDownloadClients.Count} active downloads, {ActiveUnpackClients.Count} active unpacks"); + } + + if (ActiveDownloadClients.Any(m => m.Value.Type == "Aria2c")) + { + Log("Updating Aria2 status"); + + var aria2NetClient = new Aria2NetClient(Settings.Get.Aria2cUrl, Settings.Get.Aria2cSecret, _httpClient, 1); + + var allDownloads = await aria2NetClient.TellAll(); + + Log($"Found {allDownloads.Count} Aria2 downloads"); + + foreach (var activeDownload in ActiveDownloadClients) + { + if (activeDownload.Value.Downloader is Aria2cDownloader aria2Downloader) + { + await aria2Downloader.Update(allDownloads); + } + } + + Log("Finished updating Aria2 status"); + } // Check if any torrents are finished downloading to the host, remove them from the active download list. var completedActiveDownloads = ActiveDownloadClients.Where(m => m.Value.Finished).ToList(); - Log.Debug($"Processing {completedActiveDownloads.Count} completed downloads"); - - foreach (var (downloadId, downloadClient) in completedActiveDownloads) + if (completedActiveDownloads.Count > 0) { - if (downloadClient.Error != null) + Log($"Processing {completedActiveDownloads.Count} completed downloads"); + + foreach (var (downloadId, downloadClient) in completedActiveDownloads) { - // Retry the download - Log.Debug($"Processing active download {downloadId}: error {downloadClient.Error}"); var download = await _downloads.GetById(downloadId); if (download == null) { ActiveDownloadClients.TryRemove(downloadId, out _); - Log.Debug($"Download with ID {downloadId} not found! Removed from queue"); + Log($"Download with ID {downloadId} not found! Removed from download queue"); + continue; } - if (download.RetryCount < DownloadRetryCount) - { - Log.Debug($"Processing active download {downloadId}: error {downloadClient.Error}, download retry count {download.RetryCount}/{DownloadRetryCount}, torrent retry count {download.Torrent.RetryCount}/{TorrentRetryCount}, retrying download"); + Log("Processing download", download, download.Torrent); - await _downloads.UpdateRetryCount(downloadId, download.RetryCount + 1); - await _torrents.Download(downloadId); - } - else if (download.Torrent.RetryCount < TorrentRetryCount) + if (downloadClient.Error != null) { - Log.Debug($"Processing active download {downloadId}: error {downloadClient.Error}, download retry count {download.RetryCount}/{DownloadRetryCount}, torrent retry count {download.Torrent.RetryCount}/{TorrentRetryCount}, retrying torrent"); - - Task.Run(async () => + // Retry the download if an error is encountered. + Log($"Download reported an error: {downloadClient.Error}", download, download.Torrent); + Log($"Download retry count {download.RetryCount}/{DownloadRetryCount}, torrent retry count {download.Torrent.RetryCount}/{TorrentRetryCount}", download, download.Torrent); + + if (download.RetryCount < DownloadRetryCount) { - await _torrents.RetryTorrent(download.TorrentId); - }); + Log($"Retrying download", download, download.Torrent); + + await _downloads.UpdateRetryCount(downloadId, download.RetryCount + 1); + await _downloads.UpdateDownload(downloadId); + } + else if (download.Torrent.RetryCount < TorrentRetryCount) + { + Log($"Retrying torrent", download, download.Torrent); + + await _torrents.RetryTorrent(download.TorrentId, download.Torrent.RetryCount + 1); + + continue; + } + else + { + Log($"Not retrying", download, download.Torrent); + + await _downloads.UpdateError(downloadId, downloadClient.Error); + await _downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow); + } } else { - Log.Debug($"Processing active download {downloadId}: error {downloadClient.Error}, not retrying"); + Log($"Download finished successfully", download, download.Torrent); - await _downloads.UpdateError(downloadId, downloadClient.Error); - await _downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow); + await _downloads.UpdateDownloadFinished(downloadId, DateTimeOffset.UtcNow); + await _downloads.UpdateUnpackingQueued(downloadId, DateTimeOffset.UtcNow); } + + ActiveDownloadClients.TryRemove(downloadId, out _); + + Log($"Removed from ActiveDownloadClients", download, download.Torrent); } - else - { - Log.Debug($"Processing active download {downloadId}: finished succesfully"); - - await _downloads.UpdateDownloadFinished(downloadId, DateTimeOffset.UtcNow); - await _downloads.UpdateUnpackingQueued(downloadId, DateTimeOffset.UtcNow); - } - - ActiveDownloadClients.TryRemove(downloadId, out _); - - Log.Debug($"Removed active download {downloadId} from queue"); } - + // Check if any torrents are finished unpacking, remove them from the active unpack list. var completedUnpacks = ActiveUnpackClients.Where(m => m.Value.Finished).ToList(); - - Log.Debug($"Processing {completedUnpacks.Count} completed extractions"); - foreach (var (downloadId, unpackClient) in completedUnpacks) + if (completedUnpacks.Count > 0) { - if (unpackClient.Error != null) + Log($"Processing {completedUnpacks.Count} completed unpacks"); + + foreach (var (downloadId, unpackClient) in completedUnpacks) { - Log.Debug($"Processing active unpack {downloadId}: error {unpackClient.Error}"); + var download = await _downloads.GetById(downloadId); - await _downloads.UpdateError(downloadId, unpackClient.Error); - await _downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow); + if (download == null) + { + ActiveUnpackClients.TryRemove(downloadId, out _); + + Log($"Download with ID {downloadId} not found! Removed from unpack queue"); + + continue; + } + + if (unpackClient.Error != null) + { + Log($"Unpack reported an error: {unpackClient.Error}", download, download.Torrent); + + await _downloads.UpdateError(downloadId, unpackClient.Error); + await _downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow); + } + else + { + Log($"Unpack finished successfully", download, download.Torrent); + + await _downloads.UpdateUnpackingFinished(downloadId, DateTimeOffset.UtcNow); + await _downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow); + } + + ActiveUnpackClients.TryRemove(downloadId, out _); + + Log($"Removed from ActiveUnpackClients", download, download.Torrent); } - else - { - Log.Debug($"Processing active unpack {downloadId}: finished succesfully"); - - await _downloads.UpdateUnpackingFinished(downloadId, DateTimeOffset.UtcNow); - await _downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow); - } - - ActiveUnpackClients.TryRemove(downloadId, out _); - - Log.Debug($"Removed active unpack {downloadId} from queue"); } - + var torrents = await _torrents.Get(); - Log.Debug($"Found {torrents.Count} torrents"); + torrents = torrents.Where(m => m.Completed == null).ToList(); // Only poll Real-Debrid every second when a hub is connected, otherwise every 30 seconds if (_nextUpdate < DateTime.UtcNow && torrents.Count > 0) { - Log.Debug($"Updating torrent info from Real-Debrid"); + Log($"Updating torrent info from Real-Debrid"); +#if DEBUG + var updateTime = 0; + + _nextUpdate = DateTime.UtcNow; +#else var updateTime = 30; if (RdtHub.HasConnections) { - updateTime = 1; + updateTime = 5; } + updateTime = 0; + _nextUpdate = DateTime.UtcNow.AddSeconds(updateTime); +#endif await _torrents.UpdateRdData(); // Re-get torrents to account for updated info torrents = await _torrents.Get(); + torrents = torrents.Where(m => m.Completed == null).ToList(); - Log.Debug($"Finished updating torrent info from Real-Debrid, next update in {updateTime} seconds"); + Log($"Finished updating torrent info from Real-Debrid, next update in {updateTime} seconds"); } - torrents = torrents.Where(m => m.Completed == null).ToList(); - - // Check if there are any downloads that are queued and can be started. - var queuedDownloads = torrents.SelectMany(m => m.Downloads) - .Where(m => m.Completed == null && m.DownloadQueued != null && m.DownloadStarted == null && m.Error == null) - .OrderBy(m => m.DownloadQueued) - .ToList(); - - Log.Debug($"Found {queuedDownloads.Count} torrents queued for download"); - - foreach (var download in queuedDownloads) + if (torrents.Count > 0) { - Log.Debug($"Starting download {download.DownloadId}"); - - var torrentDownload = torrents.First(m => m.TorrentId == download.TorrentId); - - if (TorrentRunner.ActiveDownloadClients.Count >= settingDownloadLimit) - { - Log.Debug($"Not starting download {download.DownloadId} because there are already the max number of downloads active"); - - continue; - } - - if (TorrentRunner.ActiveDownloadClients.ContainsKey(download.DownloadId)) - { - Log.Debug($"Not starting download {download.DownloadId} because this download is already active"); - - continue; - } - - try - { - var downloadLink = await _torrents.UnrestrictLink(download.DownloadId); - download.Link = downloadLink; - } - catch (Exception ex) - { - await _downloads.UpdateError(download.DownloadId, ex.Message); - await _downloads.UpdateCompleted(download.DownloadId, DateTimeOffset.UtcNow); - download.Error = ex.Message; - download.Completed = DateTimeOffset.UtcNow; - - Log.Information($"Cannot unrestrict: {ex.Message}"); - continue; - } - - download.DownloadStarted = DateTime.UtcNow; - await _downloads.UpdateDownloadStarted(download.DownloadId, download.DownloadStarted); - - var downloadPath = settingDownloadPath; - - if (!String.IsNullOrWhiteSpace(torrentDownload.Category)) - { - downloadPath = Path.Combine(downloadPath, torrentDownload.Category); - } - - Log.Debug($"Setting download path for {download.DownloadId} to {downloadPath}"); - - // Start the download process - var downloadClient = new DownloadClient(download, torrentDownload, downloadPath); - - if (TorrentRunner.ActiveDownloadClients.TryAdd(download.DownloadId, downloadClient)) - { - Log.Debug($"Added download {download.DownloadId} to active downloads"); - - var remoteId = await downloadClient.Start(Settings.Get); - - if (!String.IsNullOrWhiteSpace(remoteId) && download.RemoteId != remoteId) - { - Log.Debug($"Received ID {remoteId} for download {download.DownloadId}"); - - await _downloads.UpdateRemoteId(download.DownloadId, remoteId); - } - - Log.Debug($"Download {download.DownloadId} started"); - } - } - - // Check if there are any unpacks that are queued and can be started. - var queuedUnpacks = torrents.SelectMany(m => m.Downloads) - .Where(m => m.Completed == null && m.UnpackingQueued != null && m.UnpackingStarted == null && m.Error == null) - .OrderBy(m => m.DownloadQueued) - .ToList(); - - Log.Debug($"Found {queuedUnpacks.Count} torrents queued for unpacking"); - - foreach (var download in queuedUnpacks) - { - Log.Debug($"Starting unpack {download.DownloadId}"); - - var torrentDownload = torrents.First(m => m.TorrentId == download.TorrentId); - - if (download.Link == null) - { - await _downloads.UpdateError(download.DownloadId, "Download Link cannot be null"); - await _downloads.UpdateCompleted(download.DownloadId, DateTimeOffset.UtcNow); - - continue; - } - - // Check if the unpacking process is even needed - var uri = new Uri(download.Link); - var fileName = uri.Segments.Last(); - - fileName = HttpUtility.UrlDecode(fileName); - - Log.Debug($"Found file name {fileName} for {download.DownloadId}"); - - var extension = Path.GetExtension(fileName); - - if (extension != ".rar") - { - Log.Debug($"No need to unpack {download.DownloadId}, setting it as unpacked"); - - download.UnpackingStarted = DateTimeOffset.UtcNow; - download.UnpackingFinished = DateTimeOffset.UtcNow; - download.Completed = DateTimeOffset.UtcNow; - - await _downloads.UpdateUnpackingStarted(download.DownloadId, download.UnpackingStarted); - await _downloads.UpdateUnpackingFinished(download.DownloadId, download.UnpackingFinished); - await _downloads.UpdateCompleted(download.DownloadId, download.Completed); - - continue; - } - - // Check if we have reached the download limit, if so queue the download, but don't start it. - if (TorrentRunner.ActiveUnpackClients.Count >= settingUnpackLimit) - { - Log.Debug($"Not starting unpack {download.DownloadId} because there are already the max number of unpacks active"); - - continue; - } - - if (TorrentRunner.ActiveUnpackClients.ContainsKey(download.DownloadId)) - { - Log.Debug($"Not starting unpack {download.DownloadId} because this download is already active"); - - continue; - } - - download.UnpackingStarted = DateTimeOffset.UtcNow; - await _downloads.UpdateUnpackingStarted(download.DownloadId, download.UnpackingStarted); - - var downloadPath = settingDownloadPath; - - if (!String.IsNullOrWhiteSpace(torrentDownload.Category)) - { - downloadPath = Path.Combine(downloadPath, torrentDownload.Category); - } - - Log.Debug($"Setting unpack path for {download.DownloadId} to {downloadPath}"); - - // Start the unpacking process - var unpackClient = new UnpackClient(download, downloadPath); - - if (TorrentRunner.ActiveUnpackClients.TryAdd(download.DownloadId, unpackClient)) - { - Log.Debug($"Added unpack {download.DownloadId} to active unpacks"); - - unpackClient.Start(); - - Log.Debug($"Unpack {download.DownloadId} started"); - } + Log($"Processing {torrents.Count} torrents"); } foreach (var torrent in torrents) { - // If torrent is erroring out on the Real-Debrid side, skip processing this torrent. - if (torrent.RdStatus == RealDebridStatus.Error) - { - Log.Debug($"Torrent {torrent.RdId} has an error: {torrent.RdStatusRaw}, not processing further"); + // Check if there are any downloads that are queued and can be started. + var queuedDownloads = torrents.SelectMany(m => m.Downloads) + .Where(m => m.Completed == null && m.DownloadQueued != null && m.DownloadStarted == null && m.Error == null) + .OrderBy(m => m.DownloadQueued) + .ToList(); - continue; + foreach (var download in queuedDownloads) + { + Log($"Processing to download", download, torrent); + + if (ActiveDownloadClients.Count >= settingDownloadLimit) + { + Log($"Not starting download because there are already the max number of downloads active", download, torrent); + + continue; + } + + if (ActiveDownloadClients.ContainsKey(download.DownloadId)) + { + Log($"Not starting download because this download is already active", download, torrent); + + continue; + } + + try + { + Log($"Unrestricting links", download, torrent); + + var downloadLink = await _torrents.UnrestrictLink(download.DownloadId); + download.Link = downloadLink; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Cannot unrestrict link: {ex.Message}"); + + await _downloads.UpdateError(download.DownloadId, ex.Message); + await _downloads.UpdateCompleted(download.DownloadId, DateTimeOffset.UtcNow); + download.Error = ex.Message; + download.Completed = DateTimeOffset.UtcNow; + + continue; + } + + Log($"Marking download as started", download, torrent); + + download.DownloadStarted = DateTime.UtcNow; + await _downloads.UpdateDownloadStarted(download.DownloadId, download.DownloadStarted); + + var downloadPath = settingDownloadPath; + + if (!String.IsNullOrWhiteSpace(torrent.Category)) + { + downloadPath = Path.Combine(downloadPath, torrent.Category); + } + + Log($"Setting download path to {downloadPath}", download, torrent); + + // Start the download process + var downloadClient = new DownloadClient(download, torrent, downloadPath); + + if (ActiveDownloadClients.TryAdd(download.DownloadId, downloadClient)) + { + Log($"Starting download", download, torrent); + + var remoteId = await downloadClient.Start(Settings.Get); + + if (!String.IsNullOrWhiteSpace(remoteId) && download.RemoteId != remoteId) + { + Log($"Received ID {remoteId}", download, torrent); + + await _downloads.UpdateRemoteId(download.DownloadId, remoteId); + } + else + { + Log($"No ID received", download, torrent); + } + } } - // The files are selected but there are no downloads yet, check if Real-Debrid has generated links yet. - if (torrent.Downloads.Count == 0 && torrent.FilesSelected != null) - { - Log.Debug($"Torrent {torrent.RdId} checking for links"); + // Check if there are any unpacks that are queued and can be started. + var queuedUnpacks = torrents.SelectMany(m => m.Downloads) + .Where(m => m.Completed == null && m.UnpackingQueued != null && m.UnpackingStarted == null && m.Error == null) + .OrderBy(m => m.DownloadQueued) + .ToList(); - await _torrents.CheckForLinks(torrent.TorrentId); + foreach (var download in queuedUnpacks) + { + Log($"Starting unpack", download, torrent); + + if (download.Link == null) + { + Log($"No download link found", download, torrent); + + await _downloads.UpdateError(download.DownloadId, "Download Link cannot be null"); + await _downloads.UpdateCompleted(download.DownloadId, DateTimeOffset.UtcNow); + + continue; + } + + // Check if the unpacking process is even needed + var uri = new Uri(download.Link); + var fileName = uri.Segments.Last(); + + fileName = HttpUtility.UrlDecode(fileName); + + Log($"Found file name {fileName}", download, torrent); + + var extension = Path.GetExtension(fileName); + + if (extension != ".rar") + { + Log($"No need to unpack, setting it as unpacked", download, torrent); + + download.UnpackingStarted = DateTimeOffset.UtcNow; + download.UnpackingFinished = DateTimeOffset.UtcNow; + download.Completed = DateTimeOffset.UtcNow; + + await _downloads.UpdateUnpackingStarted(download.DownloadId, download.UnpackingStarted); + await _downloads.UpdateUnpackingFinished(download.DownloadId, download.UnpackingFinished); + await _downloads.UpdateCompleted(download.DownloadId, download.Completed); + + continue; + } + + // Check if we have reached the download limit, if so queue the download, but don't start it. + if (TorrentRunner.ActiveUnpackClients.Count >= settingUnpackLimit) + { + Log($"Not starting unpack because there are already the max number of unpacks active", download, torrent); + + continue; + } + + if (TorrentRunner.ActiveUnpackClients.ContainsKey(download.DownloadId)) + { + Log($"Not starting unpack because this download is already active", download, torrent); + + continue; + } + + download.UnpackingStarted = DateTimeOffset.UtcNow; + await _downloads.UpdateUnpackingStarted(download.DownloadId, download.UnpackingStarted); + + var downloadPath = settingDownloadPath; + + if (!String.IsNullOrWhiteSpace(torrent.Category)) + { + downloadPath = Path.Combine(downloadPath, torrent.Category); + } + + Log($"Setting unpack path to {downloadPath}", download, torrent); + + // Start the unpacking process + var unpackClient = new UnpackClient(download, downloadPath); + + if (TorrentRunner.ActiveUnpackClients.TryAdd(download.DownloadId, unpackClient)) + { + Log($"Starting unpack", download, torrent); + + unpackClient.Start(); + } + } + + Log("Processing", torrent); + + // If torrent is erroring out on the Real-Debrid side. + if (torrent.RdStatus == RealDebridStatus.Error) + { + Log($"Torrent reported an error: {torrent.RdStatusRaw}", torrent); + Log($"Torrent retry count {torrent.RetryCount}/{TorrentRetryCount}", torrent); + + if (torrent.RetryCount < TorrentRetryCount) + { + Log($"Retrying torrent", torrent); + + await _torrents.RetryTorrent(torrent.TorrentId, torrent.RetryCount + 1); + continue; + } + + Log($"Received RealDebrid error: {torrent.RdStatusRaw}, not processing further", torrent); + await _torrents.UpdateComplete(torrent.TorrentId, DateTimeOffset.Now); + + continue; } // Real-Debrid is waiting for file selection, select which files to download. @@ -391,55 +484,55 @@ namespace RdtClient.Service.Services torrent.Downloads.Count == 0 && torrent.FilesSelected == null) { - Log.Debug($"Torrent {torrent.RdId} selecting files"); + Log($"Selecting files", torrent); var files = torrent.Files; if (torrent.DownloadAction == TorrentDownloadAction.DownloadAvailableFiles) { - Log.Debug($"Torrent {torrent.RdId} determining which files are already available"); + Log($"Determining which files are already available on RealDebrid", torrent); var availableFiles = await _torrents.GetAvailableFiles(torrent.Hash); - Log.Debug($"Found {files.Count}/{torrent.Files.Count} available files for torrent {torrent.RdId}"); + Log($"Found {files.Count}/{torrent.Files.Count} available files on RealDebrid", torrent); files = torrent.Files.Where(m => availableFiles.Any(f => m.Path.EndsWith(f.Filename))).ToList(); } else if (torrent.DownloadAction == TorrentDownloadAction.DownloadAll) { - Log.Debug("Selecting all files"); + Log("Selecting all files", torrent); files = torrent.Files.ToList(); } else if (torrent.DownloadAction == TorrentDownloadAction.DownloadManual) { - Log.Debug("Selecting manual selected files"); + Log("Selecting manual selected files", torrent); files = torrent.Files.Where(m => torrent.ManualFiles.Any(f => m.Path.EndsWith(f))).ToList(); } - Log.Debug($"Selecting {files.Count}/{torrent.Files.Count} files for torrent {torrent.RdId}"); + Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent); if (torrent.DownloadAction != TorrentDownloadAction.DownloadManual && torrent.DownloadMinSize > 0) { var minFileSize = torrent.DownloadMinSize * 1024 * 1024; - Log.Debug($"Torrent {torrent.RdId} determining which files are over {minFileSize} bytes"); + Log($"Determining which files are over {minFileSize} bytes", torrent); files = files.Where(m => m.Bytes > minFileSize) .ToList(); - Log.Debug($"Found {files.Count} files that match the minimum file size criterea for torrent {torrent.RdId}"); + Log($"Found {files.Count} files that match the minimum file size criterea", torrent); } if (files.Count == 0) { - Log.Debug($"Filtered all files out for {torrent.RdId}! Downloading ALL files!"); + Log($"Filtered all files out! Downloading ALL files instead!", torrent); files = torrent.Files; } var fileIds = files.Select(m => m.Id.ToString()).ToArray(); - Log.Debug($"Selecting files for torrent {torrent.RdId}: {String.Join(", ", fileIds)}"); + Log($"Selecting files:{Environment.NewLine}{String.Join(Environment.NewLine, files.Select(m => m.Path))}", torrent); await _torrents.SelectFiles(torrent.TorrentId, fileIds); @@ -449,6 +542,14 @@ namespace RdtClient.Service.Services // Real-Debrid finished downloading the torrent, process the file to host. if (torrent.RdStatus == RealDebridStatus.Finished) { + // The files are selected but there are no downloads yet, check if Real-Debrid has generated links yet. + if (torrent.Downloads.Count == 0 && torrent.FilesSelected != null) + { + Log($"Checking for links", torrent); + + await _torrents.CheckForLinks(torrent.TorrentId); + } + // If the torrent has any files that need starting to be downloaded, download them. var downloadsPending = torrent.Downloads .Where(m => m.Completed == null && @@ -458,18 +559,16 @@ namespace RdtClient.Service.Services .OrderBy(m => m.Added) .ToList(); - Log.Debug($"Torrent {torrent.RdId} found {downloadsPending.Count} downloads pending"); - if (downloadsPending.Count > 0) { + Log($"Found {downloadsPending.Count} downloads pending", torrent); + foreach (var download in downloadsPending) { - Log.Debug($"Torrent {torrent.RdId} starting download {download.DownloadId}"); + Log($"Marking to download", download, torrent); - await _torrents.Download(download.DownloadId); + await _downloads.UpdateDownload(download.DownloadId); } - - continue; } // If all files are finished downloading, move to the unpacking step. @@ -480,47 +579,52 @@ namespace RdtClient.Service.Services m.UnpackingFinished == null) .ToList(); - Log.Debug($"Torrent {torrent.RdId} found {unpackingPending.Count} unpacks pending"); - if (unpackingPending.Count > 0) { + Log($"Found {unpackingPending.Count} unpacks pending", torrent); + foreach (var download in unpackingPending) { - Log.Debug($"Torrent {torrent.RdId} starting unpack {download.DownloadId}"); + Log($"Marking to unpack", download, torrent); - await _torrents.Unpack(download.DownloadId); + await _downloads.UpdateUnpack(download.DownloadId); } - - continue; } } // Check if torrent is complete if (torrent.Downloads.Count > 0) { - var allComplete = torrent.Downloads.All(m => m.Completed != null); + var allComplete = torrent.Downloads.Count(m => m.Completed != null); - if (allComplete) + if (allComplete > 0) { - Log.Debug($"Torrent {torrent.RdId} all downloads complete"); + Log($"All downloads complete, marking torrent as complete", torrent); await _torrents.UpdateComplete(torrent.TorrentId, DateTimeOffset.UtcNow); switch (torrent.FinishedAction) { case TorrentFinishedAction.RemoveAllTorrents: - Log.Debug($"Torrent {torrent.RdId} removing torrents from Real-Debrid and Real-Debrid Client, no files"); + Log($"Removing torrents from Real-Debrid and Real-Debrid Client, no files", torrent); await _torrents.Delete(torrent.TorrentId, true, true, false); break; case TorrentFinishedAction.RemoveRealDebrid: - Log.Debug($"Torrent {torrent.RdId} removing torrents from Real-Debrid, no files"); + Log($"Removing torrents from Real-Debrid, no files", torrent); await _torrents.Delete(torrent.TorrentId, false, true, false); break; case TorrentFinishedAction.None: - Log.Debug($"Torrent {torrent.RdId} not removing torrents or files"); + Log($"Not removing torrents or files", torrent); + break; + default: + Log($"Invalid torrent FinishedAction {torrent.FinishedAction}", torrent); break; } } + else + { + Log($"Waiting for downloads to complete. {allComplete}/{torrent.Downloads.Count} complete", torrent); + } } } @@ -528,7 +632,35 @@ namespace RdtClient.Service.Services sw.Stop(); - Log.Debug($"TorrentRunner Tick End (took {sw.ElapsedMilliseconds}ms)"); + if (sw.ElapsedMilliseconds > 1000) + { + Log($"TorrentRunner Tick End (took {sw.ElapsedMilliseconds}ms)"); + } + } + + private void Log(String message, Download download, Torrent torrent) + { + if (download != null) + { + message = $"{message} {download.ToLog()}"; + } + + if (torrent != null) + { + message = $"{message} {torrent.ToLog()}"; + } + + _logger.LogDebug(message); + } + + private void Log(String message, Torrent torrent = null) + { + if (torrent != null) + { + message = $"{message} {torrent.ToLog()}"; + } + + _logger.LogDebug(message); } } } diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index 8fcd8d8..35d0896 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -2,53 +2,41 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using MonoTorrent; using Newtonsoft.Json; -using RDNET; using RdtClient.Data.Data; using RdtClient.Data.Enums; using RdtClient.Service.Helpers; using RdtClient.Service.Models; +using RdtClient.Service.Models.TorrentClient; +using RdtClient.Service.Services.TorrentClients; using Torrent = RdtClient.Data.Models.Data.Torrent; namespace RdtClient.Service.Services { public class Torrents { - public static readonly SemaphoreSlim TorrentResetLock = new(1, 1); - private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1); - private readonly Downloads _downloads; - private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; private readonly TorrentData _torrentData; + private readonly Downloads _downloads; + + private readonly ITorrentClient _torrentClient; - public Torrents(IHttpClientFactory httpClientFactory, TorrentData torrentData, Downloads downloads) + public Torrents(ILogger logger, + TorrentData torrentData, + Downloads downloads, + RealDebridTorrentClient realDebridTorrentClient) { - _httpClientFactory = httpClientFactory; + _logger = logger; _torrentData = torrentData; _downloads = downloads; - } - - private RdNetClient GetRdNetClient() - { - var apiKey = Settings.Get.RealDebridApiKey; - - if (String.IsNullOrWhiteSpace(apiKey)) - { - throw new Exception("Real-Debrid API Key not set in the settings"); - } - - var httpClient = _httpClientFactory.CreateClient(); - httpClient.Timeout = TimeSpan.FromSeconds(10); - - var rdtNetClient = new RdNetClient(null, httpClient); - rdtNetClient.UseApiAuthentication(apiKey); - - return rdtNetClient; + + _torrentClient = realDebridTorrentClient; } public async Task> Get() @@ -98,6 +86,8 @@ namespace RdtClient.Service.Services return; } + Log($"Update category to {category}", torrent); + await _torrentData.UpdateCategory(torrent.TorrentId, category); } @@ -117,12 +107,19 @@ namespace RdtClient.Service.Services } catch (Exception ex) { + _logger.LogError(ex, $"{ex.Message}, trying to parse {magnetLink}"); throw new Exception($"{ex.Message}, trying to parse {magnetLink}"); } - var rdTorrent = await GetRdNetClient().Torrents.AddMagnetAsync(magnetLink); + var id = await _torrentClient.AddMagnet(magnetLink); - return await Add(rdTorrent.Id, magnet.InfoHash.ToHex(), category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, magnetLink, false, priority); + var hash = magnet.InfoHash.ToHex(); + + var newTorrent = await Add(id, hash, category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, magnetLink, false, priority); + + Log($"Adding {hash} magnet link {magnetLink}", newTorrent); + + return newTorrent; } public async Task UploadFile(Byte[] bytes, @@ -133,33 +130,35 @@ namespace RdtClient.Service.Services String downloadManualFiles, Int32? priority) { - MonoTorrent.Torrent torrent; + MonoTorrent.Torrent monoTorrent; var fileAsBase64 = Convert.ToBase64String(bytes); try { - torrent = await MonoTorrent.Torrent.LoadAsync(bytes); + monoTorrent = await MonoTorrent.Torrent.LoadAsync(bytes); } catch (Exception ex) { throw new Exception($"{ex.Message}, trying to parse {fileAsBase64}"); } - var rdTorrent = await GetRdNetClient().Torrents.AddFileAsync(bytes); + var id = await _torrentClient.AddFile(bytes); - return await Add(rdTorrent.Id, torrent.InfoHash.ToHex(), category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, fileAsBase64, true, priority); + var hash = monoTorrent.InfoHash.ToHex(); + + var newTorrent = await Add(id, hash, category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, fileAsBase64, true, priority); + + Log($"Adding {hash} torrent file {fileAsBase64}", newTorrent); + + return newTorrent; } - public async Task> GetAvailableFiles(String hash) + public async Task> GetAvailableFiles(String hash) { - var result = await GetRdNetClient().Torrents.GetAvailableFiles(hash); + var result = await _torrentClient.GetAvailableFiles(hash); - var files = result.SelectMany(m => m.Value).SelectMany(m => m.Value).SelectMany(m => m.Values); - - var groups = files.GroupBy(m => $"{m.Filename}-{m.Filesize}"); - - return groups.Select(m => m.First()).ToList(); + return result; } public async Task SelectFiles(Guid torrentId, IList fileIds) @@ -171,7 +170,7 @@ namespace RdtClient.Service.Services return; } - await GetRdNetClient().Torrents.SelectFilesAsync(torrent.RdId, fileIds.ToArray()); + await _torrentClient.SelectFiles(torrent.RdId, fileIds); } public async Task CheckForLinks(Guid torrentId) @@ -183,10 +182,12 @@ namespace RdtClient.Service.Services return; } - var rdTorrent = await GetRdNetClient().Torrents.GetInfoAsync(torrent.RdId); + var rdTorrent = await _torrentClient.GetInfo(torrent.RdId); var torrentLinks = rdTorrent.Links.Where(m => !String.IsNullOrWhiteSpace(m)).ToList(); + Log($"Found {torrentLinks} links", torrent); + // Sometimes RD will give you 1 rar with all files, sometimes it will give you 1 link per file. if (torrent.Files.Count(m => m.Selected) != torrentLinks.Count && torrent.ManualFiles.Count != torrentLinks.Count && @@ -216,21 +217,33 @@ namespace RdtClient.Service.Services return; } + Log($"Deleting", torrent); + foreach (var download in torrent.Downloads) { - if (TorrentRunner.ActiveUnpackClients.TryRemove(download.DownloadId, out var activeUnpack)) + while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) { - activeUnpack.Cancel(); + Log($"Cancelling download", download, torrent); + + await downloadClient.Cancel(); + + await Task.Delay(100); } - if (TorrentRunner.ActiveDownloadClients.TryRemove(download.DownloadId, out var activeDownload)) + while (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient)) { - activeDownload.Cancel(); + Log($"Cancelling unpack", download, torrent); + + unpackClient.Cancel(); + + await Task.Delay(100); } } if (deleteData) { + Log($"Deleting RdtClient data", torrent); + await _torrentData.UpdateComplete(torrent.TorrentId, DateTimeOffset.UtcNow); await _downloads.DeleteForTorrent(torrent.TorrentId); await _torrentData.Delete(torrentId); @@ -238,7 +251,16 @@ namespace RdtClient.Service.Services if (deleteRdTorrent) { - await GetRdNetClient().Torrents.DeleteAsync(torrent.RdId); + Log($"Deleting RealDebrid Torrent", torrent); + + try + { + await _torrentClient.Delete(torrent.RdId); + } + catch + { + // ignored + } } if (deleteLocalFiles) @@ -246,6 +268,8 @@ namespace RdtClient.Service.Services var downloadPath = DownloadPath(torrent); downloadPath = Path.Combine(downloadPath, torrent.RdName); + Log($"Deleting local files in {downloadPath}", torrent); + if (Directory.Exists(downloadPath)) { var retry = 0; @@ -274,58 +298,6 @@ namespace RdtClient.Service.Services } } - private async Task DeleteDownload(Guid torrentId, Guid downloadId) - { - var torrent = await GetById(torrentId); - - if (torrent == null) - { - return; - } - - var download = await _downloads.GetById(downloadId); - - if (download == null) - { - return; - } - - var downloadPath = DownloadPath(torrent); - - var filePath = DownloadHelper.GetDownloadPath(downloadPath, torrent, download); - - if (filePath == null) - { - return; - } - - if (File.Exists(filePath)) - { - var retry = 0; - - while (true) - { - try - { - File.Delete(filePath); - - break; - } - catch - { - retry++; - - if (retry >= 3) - { - throw; - } - - await Task.Delay(1000); - } - } - } - } - public async Task UnrestrictLink(Guid downloadId) { var download = await _downloads.GetById(downloadId); @@ -335,36 +307,18 @@ namespace RdtClient.Service.Services throw new Exception($"Download with ID {downloadId} not found"); } - var unrestrictedLink = await GetRdNetClient().Unrestrict.LinkAsync(download.Path); + Log($"Unrestricting link", download, download.Torrent); - await _downloads.UpdateUnrestrictedLink(downloadId, unrestrictedLink.Download); + var unrestrictedLink = await _torrentClient.Unrestrict(download.Path); - return unrestrictedLink.Download; - } + await _downloads.UpdateUnrestrictedLink(downloadId, unrestrictedLink); - public async Task Download(Guid downloadId) - { - await _downloads.UpdateDownloadStarted(downloadId, null); - await _downloads.UpdateDownloadFinished(downloadId, null); - await _downloads.UpdateUnpackingQueued(downloadId, null); - await _downloads.UpdateUnpackingStarted(downloadId, null); - await _downloads.UpdateUnpackingFinished(downloadId, null); - await _downloads.UpdateCompleted(downloadId, null); - await _downloads.UpdateError(downloadId, null); - } - - public async Task Unpack(Guid downloadId) - { - await _downloads.UpdateUnpackingQueued(downloadId, DateTimeOffset.UtcNow); - await _downloads.UpdateUnpackingStarted(downloadId, null); - await _downloads.UpdateUnpackingFinished(downloadId, null); - await _downloads.UpdateCompleted(downloadId, null); - await _downloads.UpdateError(downloadId, null); + return unrestrictedLink; } public async Task GetProfile() { - var user = await GetRdNetClient().User.GetAsync(); + var user = await _torrentClient.GetUser(); var profile = new Profile { @@ -383,7 +337,7 @@ namespace RdtClient.Service.Services try { - var rdTorrents = await GetRdNetClient().Torrents.GetAsync(0, 100); + var rdTorrents = await _torrentClient.GetTorrents(); foreach (var rdTorrent in rdTorrents) { @@ -396,17 +350,6 @@ namespace RdtClient.Service.Services await UpdateRdData(torrent); } - - foreach (var torrent in torrents) - { - var rdTorrent = rdTorrents.FirstOrDefault(m => m.Id == torrent.RdId); - - if (rdTorrent == null) - { - await _downloads.DeleteForTorrent(torrent.TorrentId); - await _torrentData.Delete(torrent.TorrentId); - } - } } finally { @@ -414,7 +357,7 @@ namespace RdtClient.Service.Services } } - public async Task RetryTorrent(Guid torrentId) + public async Task RetryTorrent(Guid torrentId, Int32 retryCount) { var torrent = await _torrentData.GetById(torrentId); @@ -423,13 +366,13 @@ namespace RdtClient.Service.Services return; } - var newRetryCount = torrent.RetryCount + 1; + Log($"Retrying Torrent", torrent); foreach (var download in torrent.Downloads) { while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) { - downloadClient.Cancel(); + await downloadClient.Cancel(); await Task.Delay(100); } @@ -449,35 +392,26 @@ namespace RdtClient.Service.Services throw new Exception($"Cannot re-add this torrent, original magnet or file not found"); } - await TorrentResetLock.WaitAsync(); + Torrent newTorrent; - try + if (torrent.IsFile) { - Torrent newTorrent; + var bytes = Convert.FromBase64String(torrent.FileOrMagnet); - if (torrent.IsFile) - { - var bytes = Convert.FromBase64String(torrent.FileOrMagnet); - - newTorrent = await UploadFile(bytes, torrent.Category, torrent.DownloadAction, torrent.FinishedAction, torrent.DownloadMinSize, torrent.DownloadManualFiles, torrent.Priority); - } - else - { - newTorrent = await UploadMagnet(torrent.FileOrMagnet, - torrent.Category, - torrent.DownloadAction, - torrent.FinishedAction, - torrent.DownloadMinSize, - torrent.DownloadManualFiles, - torrent.Priority); - } - - await _torrentData.UpdateRetryCount(newTorrent.TorrentId, newRetryCount); + newTorrent = await UploadFile(bytes, torrent.Category, torrent.DownloadAction, torrent.FinishedAction, torrent.DownloadMinSize, torrent.DownloadManualFiles, torrent.Priority); } - finally + else { - TorrentResetLock.Release(); + newTorrent = await UploadMagnet(torrent.FileOrMagnet, + torrent.Category, + torrent.DownloadAction, + torrent.FinishedAction, + torrent.DownloadMinSize, + torrent.DownloadManualFiles, + torrent.Priority); } + + await _torrentData.UpdateRetryCount(newTorrent.TorrentId, retryCount); } public async Task RetryDownload(Guid downloadId) @@ -489,9 +423,11 @@ namespace RdtClient.Service.Services return; } + Log($"Retrying Download", download, download.Torrent); + while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) { - downloadClient.Cancel(); + await downloadClient.Cancel(); await Task.Delay(100); } @@ -503,21 +439,19 @@ namespace RdtClient.Service.Services await Task.Delay(100); } - await TorrentResetLock.WaitAsync(); + var downloadPath = DownloadPath(download.Torrent); + + var filePath = DownloadHelper.GetDownloadPath(downloadPath, download.Torrent, download); - try - { + Log($"Deleting {filePath}", download, download.Torrent); + + await FileHelper.Delete(filePath); + + await _torrentData.UpdateComplete(download.TorrentId, null); - await DeleteDownload(download.TorrentId, download.DownloadId); + Log($"Resetting", download, download.Torrent); - await _torrentData.UpdateComplete(download.TorrentId, null); - - await _downloads.Reset(downloadId); - } - finally - { - TorrentResetLock.Release(); - } + await _downloads.Reset(downloadId); } public async Task UpdateComplete(Guid torrentId, DateTimeOffset datetime) @@ -640,57 +574,71 @@ namespace RdtClient.Service.Services ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); - var rdTorrent = await GetRdNetClient().Torrents.GetInfoAsync(torrent.RdId); - - if (!String.IsNullOrWhiteSpace(rdTorrent.Filename)) + try { - torrent.RdName = rdTorrent.Filename; + var rdTorrent = await _torrentClient.GetInfo(torrent.RdId); + + if (!String.IsNullOrWhiteSpace(rdTorrent.Filename)) + { + torrent.RdName = rdTorrent.Filename; + } + + if (!String.IsNullOrWhiteSpace(rdTorrent.OriginalFilename)) + { + torrent.RdName = rdTorrent.OriginalFilename; + } + + if (rdTorrent.Bytes > 0) + { + torrent.RdSize = rdTorrent.Bytes; + } + else if (rdTorrent.OriginalBytes > 0) + { + torrent.RdSize = rdTorrent.OriginalBytes; + } + + if (rdTorrent.Files != null) + { + torrent.RdFiles = JsonConvert.SerializeObject(rdTorrent.Files); + } + + torrent.RdHost = rdTorrent.Host; + torrent.RdSplit = rdTorrent.Split; + torrent.RdProgress = rdTorrent.Progress; + torrent.RdAdded = rdTorrent.Added; + torrent.RdEnded = rdTorrent.Ended; + torrent.RdSpeed = rdTorrent.Speed; + torrent.RdSeeders = rdTorrent.Seeders; + torrent.RdStatusRaw = rdTorrent.Status; + + torrent.RdStatus = rdTorrent.Status switch + { + "magnet_error" => RealDebridStatus.Error, + "magnet_conversion" => RealDebridStatus.Processing, + "waiting_files_selection" => RealDebridStatus.WaitingForFileSelection, + "queued" => RealDebridStatus.Downloading, + "downloading" => RealDebridStatus.Downloading, + "downloaded" => RealDebridStatus.Finished, + "error" => RealDebridStatus.Error, + "virus" => RealDebridStatus.Error, + "compressing" => RealDebridStatus.Downloading, + "uploading" => RealDebridStatus.Downloading, + "dead" => RealDebridStatus.Error, + _ => RealDebridStatus.Error + }; } - - if (!String.IsNullOrWhiteSpace(rdTorrent.OriginalFilename)) + catch (Exception ex) { - torrent.RdName = rdTorrent.OriginalFilename; + if (ex.Message == "Resource not found") + { + torrent.RdStatusRaw = "deleted"; + } + else + { + throw; + } } - - if (rdTorrent.Bytes > 0) - { - torrent.RdSize = rdTorrent.Bytes; - } - else if (rdTorrent.OriginalBytes > 0) - { - torrent.RdSize = rdTorrent.OriginalBytes; - } - - if (rdTorrent.Files != null) - { - torrent.RdFiles = JsonConvert.SerializeObject(rdTorrent.Files); - } - - torrent.RdHost = rdTorrent.Host; - torrent.RdSplit = rdTorrent.Split; - torrent.RdProgress = rdTorrent.Progress; - torrent.RdAdded = rdTorrent.Added; - torrent.RdEnded = rdTorrent.Ended; - torrent.RdSpeed = rdTorrent.Speed; - torrent.RdSeeders = rdTorrent.Seeders; - torrent.RdStatusRaw = rdTorrent.Status; - - torrent.RdStatus = rdTorrent.Status switch - { - "magnet_error" => RealDebridStatus.Error, - "magnet_conversion" => RealDebridStatus.Processing, - "waiting_files_selection" => RealDebridStatus.WaitingForFileSelection, - "queued" => RealDebridStatus.Downloading, - "downloading" => RealDebridStatus.Downloading, - "downloaded" => RealDebridStatus.Finished, - "error" => RealDebridStatus.Error, - "virus" => RealDebridStatus.Error, - "compressing" => RealDebridStatus.Downloading, - "uploading" => RealDebridStatus.Finished, - "dead" => RealDebridStatus.Error, - _ => RealDebridStatus.Error - }; - + var newTorrent = JsonConvert.SerializeObject(torrent, new JsonSerializerSettings { @@ -702,5 +650,30 @@ namespace RdtClient.Service.Services await _torrentData.UpdateRdData(torrent); } } + + private void Log(String message, Data.Models.Data.Download download, Torrent torrent) + { + if (download != null) + { + message = $"{message} {download.ToLog()}"; + } + + if (torrent != null) + { + message = $"{message} {torrent.ToLog()}"; + } + + _logger.LogDebug(message); + } + + private void Log(String message, Torrent torrent = null) + { + if (torrent != null) + { + message = $"{message} {torrent.ToLog()}"; + } + + _logger.LogDebug(message); + } } } diff --git a/server/RdtClient.Service/Services/UnpackClient.cs b/server/RdtClient.Service/Services/UnpackClient.cs index 9dda73d..082deb9 100644 --- a/server/RdtClient.Service/Services/UnpackClient.cs +++ b/server/RdtClient.Service/Services/UnpackClient.cs @@ -122,20 +122,7 @@ namespace RdtClient.Service.Services } } - var retryCount = 0; - while (File.Exists(filePath) && retryCount < 10) - { - retryCount++; - - try - { - File.Delete(filePath); - } - catch - { - await Task.Delay(1000); - } - } + await FileHelper.Delete(filePath); } catch (Exception ex) { diff --git a/server/RdtClient.Web/Controllers/TorrentsController.cs b/server/RdtClient.Web/Controllers/TorrentsController.cs index 62b734c..a8f4b54 100644 --- a/server/RdtClient.Web/Controllers/TorrentsController.cs +++ b/server/RdtClient.Web/Controllers/TorrentsController.cs @@ -159,7 +159,7 @@ namespace RdtClient.Web.Controllers [Route("Retry/{torrentId:guid}")] public async Task Retry(Guid torrentId) { - await _torrents.RetryTorrent(torrentId); + await _torrents.RetryTorrent(torrentId, 0); return Ok(); } diff --git a/server/RdtClient.Web/Program.cs b/server/RdtClient.Web/Program.cs index 40781ea..64d6a22 100644 --- a/server/RdtClient.Web/Program.cs +++ b/server/RdtClient.Web/Program.cs @@ -88,9 +88,15 @@ namespace RdtClient.Web Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() .Enrich.WithExceptionDetails() - .WriteTo.File(appSettings.Logging.File.Path, rollOnFileSizeLimit: true, fileSizeLimitBytes: appSettings.Logging.File.FileSizeLimitBytes, retainedFileCountLimit: appSettings.Logging.File.MaxRollingFiles) + .WriteTo.File(appSettings.Logging.File.Path, + rollOnFileSizeLimit: true, + fileSizeLimitBytes: appSettings.Logging.File.FileSizeLimitBytes, + retainedFileCountLimit: appSettings.Logging.File.MaxRollingFiles, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}") .WriteTo.Console() .MinimumLevel.ControlledBy(LoggingLevelSwitch) + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Override("System.Net.Http", LogEventLevel.Warning) .CreateLogger(); Serilog.Debugging.SelfLog.Enable(msg => diff --git a/server/RdtClient.Web/RdtClient.Web.csproj b/server/RdtClient.Web/RdtClient.Web.csproj index 68f1edc..01919dc 100644 --- a/server/RdtClient.Web/RdtClient.Web.csproj +++ b/server/RdtClient.Web/RdtClient.Web.csproj @@ -4,7 +4,7 @@ net5.0 Exe 94c24cba-f03f-4453-a671-3640b517c573 - 1.9.0 + 1.9.1 @@ -26,16 +26,16 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - +