diff --git a/README.md b/README.md index d388b0a..eb766e6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This is a web interface to manage your torrents on Real-Debrid. It supports the - Download all files from Real Debrid to your local machine automatically - Unpack all files when finished downloading - Implements a fake qBittorrent API so you can hook up other applications like Sonarr or Couchpotato. -- Build with Angular 9 and .NET Core 3.1 +- Built with Angular 9 and .NET Core 3.1 **You will need a Premium service at Real-Debrid!** @@ -14,18 +14,23 @@ This is a web interface to manage your torrents on Real-Debrid. It supports the 1. Unpack the latest release from the releases folder and run `startup.bat`. This will start the application on port 6500. 2. Browse to http://127.0.0.1:6500 -3. The very first credentials you type in will be remembered for future logins. +3. The very first credentials you enter in will be remembered for future logins. 4. Click on Settings on the top and enter your Real-Debrid API key. 5. Change your download path if needed. 6. To install as service on Windows, exit the console and run `serviceinstall.bat` as administrator. +7. To change the default port edit the `appsettings.json` file. + +## Removing + +1. Run `serviceremove.bat` to remove the service and firewall rules. ## Troubleshooting - If you forgot your logins simply delete the `database.db` and restart the service. -## Connecting Sonarr +## Connecting Sonarr/Radarr -1. Login to Sonarr and click Settings +1. Login to Sonarr or Radarr and click Settings 2. Go to the Download Client tab and click the plus to add 3. Click "qBittorrent" in the list 4. Enter the IP or hostname of the RealDebridClient in the Host field @@ -35,7 +40,7 @@ This is a web interface to manage your torrents on Real-Debrid. It supports the 8. Hit Test and then Save if all is well. 9. Sonarr will now think you have a regular Torrent client hooked up. -Notice: the progress and ETA reported in Sonarr's Activity tab will not be very accurate, but it will report the torrent as completed so it can be processed by Sonarr when done downloading. +Notice: the progress and ETA reported in Sonarr's Activity tab will not be accurate, but it will report the torrent as completed so it can be processed after it is done downloading. ## Build instructions diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index 7192a09..9080c45 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -12,8 +12,6 @@ namespace RdtClient.Service services.AddScoped(); services.AddScoped(); services.AddScoped(); - - services.AddHostedService(); } } } \ No newline at end of file diff --git a/server/RdtClient.Service/Services/DownloadManager.cs b/server/RdtClient.Service/Services/DownloadManager.cs index 86f1474..4d4b4b3 100644 --- a/server/RdtClient.Service/Services/DownloadManager.cs +++ b/server/RdtClient.Service/Services/DownloadManager.cs @@ -36,7 +36,7 @@ namespace RdtClient.Service.Services private DownloadManager ActiveDownload => TaskRunner.ActiveDownloads[Download.DownloadId]; - public async Task Start(String destinationFolderPath) + public async Task Start(String destinationFolderPath, String rootfolderPath) { ActiveDownload.NewStatus = DownloadStatus.Downloading; ActiveDownload.BytesDownloaded = 0; @@ -128,7 +128,7 @@ namespace RdtClient.Service.Services { _rarCurrentEntry = entry; - entry.WriteToDirectory(destinationFolderPath, + entry.WriteToDirectory(rootfolderPath, new ExtractionOptions { ExtractFullPath = true, diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index 17ebae9..31fc465 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -19,6 +19,7 @@ namespace RdtClient.Service.Services Task TorrentProperties(String hash); Task TorrentsDelete(String hash, Boolean deleteFiles); Task TorrentsAdd(String magnetLink, Boolean autoDownload, Boolean autoDelete); + Task TorrentsAddFile(Byte[] fileBytes, Boolean autoDownload, Boolean autoDelete); Task TorrentsSetCategory(String hash, String category); Task> TorrentsCategories(); } @@ -201,7 +202,10 @@ namespace RdtClient.Service.Services var user = await _authentication.GetUser(); - preferences.WebUiUsername = user.UserName; + if (user != null) + { + preferences.WebUiUsername = user.UserName; + } return preferences; } @@ -368,6 +372,11 @@ namespace RdtClient.Service.Services await _torrents.UploadMagnet(magnetLink, autoDownload, autoDelete); } + public async Task TorrentsAddFile(Byte[] fileBytes, Boolean autoDownload, Boolean autoDelete) + { + await _torrents.UploadFile(fileBytes, autoDownload, autoDelete); + } + public async Task TorrentsSetCategory(String hash, String category) { await _torrents.UpdateCategory(hash, category); diff --git a/server/RdtClient.Service/Services/TaskRunner.cs b/server/RdtClient.Service/Services/TaskRunner.cs index 6ce5119..3542627 100644 --- a/server/RdtClient.Service/Services/TaskRunner.cs +++ b/server/RdtClient.Service/Services/TaskRunner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -10,81 +11,60 @@ using RdtClient.Data.Enums; namespace RdtClient.Service.Services { - public class TaskRunner : IHostedService, IDisposable + public class TaskRunner : BackgroundService { public static readonly ConcurrentDictionary ActiveDownloads = new ConcurrentDictionary(); private readonly ILogger _logger; private readonly IServiceProvider _services; - private Timer _timer; - - private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1); - public TaskRunner(ILogger logger, IServiceProvider services) { _logger = logger; _services = services; } - public Task StartAsync(CancellationToken cancellationToken) + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _logger.LogInformation("Timed Hosted Service running."); + await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken); - _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5)); + _logger.LogInformation("TaskRunner started."); - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Timed Hosted Service is stopping."); - - _timer?.Change(Timeout.Infinite, 0); - - return Task.CompletedTask; - } - - public void Dispose() - { - _timer?.Dispose(); - } - - private async void DoWork(Object state) - { - // Make sure only 1 process enters the lock - var obtainLock = await _semaphoreSlim.WaitAsync(100); - - if (!obtainLock) + while (!stoppingToken.IsCancellationRequested) { - return; + await DoWork(); + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } + _logger.LogInformation("TaskRunner stopped."); + } + + private async Task DoWork() + { try { - using (var scope = _services.CreateScope()) + using var scope = _services.CreateScope(); + + var downloads = scope.ServiceProvider.GetRequiredService(); + var settings = scope.ServiceProvider.GetRequiredService(); + var torrents = scope.ServiceProvider.GetRequiredService(); + + var rdKey = await settings.GetString("RealDebridApiKey"); + + if (String.IsNullOrWhiteSpace(rdKey)) { - var downloads = scope.ServiceProvider.GetRequiredService(); - var settings = scope.ServiceProvider.GetRequiredService(); - var torrents = scope.ServiceProvider.GetRequiredService(); - - var rdKey = await settings.GetString("RealDebridApiKey"); - - if (String.IsNullOrWhiteSpace(rdKey)) - { - return; - } - - await torrents.Update(); - - await ProcessAutoDownloads(downloads, settings, torrents); - await ProcessDownloads(downloads, settings, torrents); - await ProcessStatus(downloads, settings, torrents); + return; } + + await torrents.Update(); + + await ProcessAutoDownloads(downloads, settings, torrents); + await ProcessDownloads(downloads, settings, torrents); + await ProcessStatus(downloads, settings, torrents); } - finally + catch(Exception ex) { - _semaphoreSlim.Release(1); + _logger.LogError(ex, ex.Message); } } @@ -136,8 +116,10 @@ namespace RdtClient.Service.Services if (ActiveDownloads.TryAdd(download.DownloadId, downloadManager)) { + var folderPath = Path.Combine(destinationFolderPath, download.Torrent.RdName); + downloadManager.Download = download; - await downloadManager.Start(destinationFolderPath); + await downloadManager.Start(folderPath, destinationFolderPath); } }); } diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index cf4b9fc..b44118c 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -132,7 +132,7 @@ namespace RdtClient.Service.Services { var torrents = await _torrentData.Get(); - var w = await SemaphoreSlim.WaitAsync(10); + var w = await SemaphoreSlim.WaitAsync(1); if (!w) { return torrents; @@ -267,22 +267,31 @@ namespace RdtClient.Service.Services private async Task Add(String rdTorrentId, String infoHash, Boolean autoDownload, Boolean autoDelete) { - var newTorrent = await _torrentData.Add(rdTorrentId, infoHash, autoDownload, autoDelete); - - var rdTorrent = await RdNetClient.GetTorrentInfoAsync(rdTorrentId); - - if (rdTorrent.Files != null && rdTorrent.Files.Count > 0) + var w = await SemaphoreSlim.WaitAsync(1); + if (!w) { - if (!rdTorrent.Files.Any(m => m.Selected)) - { - var fileIds = rdTorrent.Files.Select(m => m.Id.ToString()) - .ToArray(); - - await RdNetClient.SelectTorrentFilesAsync(rdTorrentId, fileIds); - } + return; } - await Update(newTorrent, rdTorrent); + try + { + var torrent = await _torrentData.GetByHash(infoHash); + + if (torrent != null) + { + return; + } + + var newTorrent = await _torrentData.Add(rdTorrentId, infoHash, autoDownload, autoDelete); + + var rdTorrent = await RdNetClient.GetTorrentInfoAsync(rdTorrentId); + + await Update(newTorrent, rdTorrent); + } + finally + { + SemaphoreSlim.Release(); + } } private async Task Update(Torrent torrent, RDNET.Torrent rdTorrent) @@ -342,6 +351,19 @@ namespace RdtClient.Service.Services await _torrentData.UpdateStatus(torrent.TorrentId, torrent.Status); } } + + if (rdTorrent.Files != null && rdTorrent.Files.Count > 0) + { + if (!rdTorrent.Files.Any(m => m.Selected)) + { + var fileIds = rdTorrent.Files + .Where(m => m.Bytes > 1024 * 10) + .Select(m => m.Id.ToString()) + .ToArray(); + + await RdNetClient.SelectTorrentFilesAsync(rdTorrent.Id, fileIds); + } + } } } } \ No newline at end of file diff --git a/server/RdtClient.Web/Controllers/QBittorrentController.cs b/server/RdtClient.Web/Controllers/QBittorrentController.cs index de47954..df78976 100644 --- a/server/RdtClient.Web/Controllers/QBittorrentController.cs +++ b/server/RdtClient.Web/Controllers/QBittorrentController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -114,7 +115,7 @@ namespace RdtClient.Web.Controllers return Ok(); } - [Authorize] + [AllowAnonymous] [Route("app/preferences")] [HttpGet] [HttpPost] @@ -288,7 +289,25 @@ namespace RdtClient.Web.Controllers [HttpPost] public async Task TorrentsAddPost([FromForm] QBTorrentsAddRequest request) { - return await TorrentsAdd(request); + foreach (var file in Request.Form.Files) + { + if (file.Length > 0) + { + await using var target = new MemoryStream(); + + await file.CopyToAsync(target); + var fileBytes = target.ToArray(); + + await _qBittorrent.TorrentsAddFile(fileBytes, true, true); + } + } + + if (request.Urls != null) + { + return await TorrentsAdd(request); + } + + return Ok(); } [Authorize] @@ -402,15 +421,4 @@ namespace RdtClient.Web.Controllers public String Hashes { get; set; } public String Category { get; set; } } - - public class QbTorrentsCreateCategoryRequest - { - public String Category { get; set; } - public String SavePath { get; set; } - } - - public class QbTorrentsRemoveCategoryRequest - { - public String Categories { get; set; } - } } \ No newline at end of file diff --git a/server/RdtClient.Web/Program.cs b/server/RdtClient.Web/Program.cs index ca6aa2a..5277e8f 100644 --- a/server/RdtClient.Web/Program.cs +++ b/server/RdtClient.Web/Program.cs @@ -1,18 +1,19 @@ using System; -using System.IO; -using Microsoft.AspNetCore; +using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using RdtClient.Service.Services; namespace RdtClient.Web { public class Program { - public static void Main(String[] args) + public static async Task Main(String[] args) { var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", true, false) .Build(); @@ -23,20 +24,47 @@ namespace RdtClient.Web hostUrl = "http://0.0.0.0:6500"; } - WebHost.CreateDefaultBuilder(args) - .UseStartup() - .ConfigureLogging(logging => - { - logging.ClearProviders(); - logging.AddConsole(); - logging.AddFile($"{AppContext.BaseDirectory}app.log"); - }) - .CaptureStartupErrors(false) - .UseUrls(hostUrl) - .UseKestrel() - .UseIISIntegration() - .Build() - .Run(); + await Host.CreateDefaultBuilder(args) + .UseWindowsService() + .ConfigureServices((hostContext, services) => + { + services.AddHostedService(); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddConsole(); + logging.AddFile($"{AppContext.BaseDirectory}app.log"); + }) + .UseUrls(hostUrl) + .UseKestrel() + .UseStartup(); + }) + .Build() + .RunAsync(); } } + + /*public class Program + { + public static void Main(String[] args) + { + + + + + Host.CreateDefaultBuilder(args) + .UseWindowsService() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .CaptureStartupErrors(false) + + .UseStartup() + .UseKestrel(); + }); + } + }*/ } \ No newline at end of file diff --git a/server/RdtClient.Web/RdtClient.Web.csproj b/server/RdtClient.Web/RdtClient.Web.csproj index 68e4626..f0f7e2e 100644 --- a/server/RdtClient.Web/RdtClient.Web.csproj +++ b/server/RdtClient.Web/RdtClient.Web.csproj @@ -2,22 +2,19 @@ netcoreapp3.1 + Exe - - + - - PreserveNewest - PreserveNewest - + PreserveNewest @@ -32,6 +29,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/server/RdtClient.Web/nssm.exe b/server/RdtClient.Web/nssm.exe deleted file mode 100644 index 6ccfe3c..0000000 Binary files a/server/RdtClient.Web/nssm.exe and /dev/null differ diff --git a/server/RdtClient.Web/serviceinstall.bat b/server/RdtClient.Web/serviceinstall.bat index 0c86a20..284f6e7 100644 --- a/server/RdtClient.Web/serviceinstall.bat +++ b/server/RdtClient.Web/serviceinstall.bat @@ -1,44 +1,30 @@ -echo OFF +@echo off set installpath=%~dp0 NET SESSION >nul 2>&1 IF %ERRORLEVEL% EQU 0 ( - echo adding firewall rules... - netsh.exe advfirewall firewall add rule name="RealDebridClient" dir=in action=allow program="%installpath%RdtClient.Web.exe" enable=yes > nul - - @echo off - sc query | findstr /C:"SERVICE_NAME: RealDebridClient" - IF ERRORLEVEL 1 ( - echo installing service... - nssm install RealDebridClient "%installpath%startup.bat" - ) - IF ERRORLEVEL 0 ( - echo service already installed - ) - - echo.>"%installpath%RdtClient.Web.exe":Zone.Identifier - - echo starting service... - net start "RealDebridClient" - echo Starting web app, remember, to set your credentials login with any credentials for the first time. - ping 127.0.0.1 -n 10 > nul - start "" http://127.0.0.1:6500/ + echo adding firewall rules... + netsh.exe advfirewall firewall add rule name="RealDebridClient" dir=in action=allow program="%installpath%RdtClient.Web.exe" enable=yes > nul + echo installing service... + sc create RealDebridClient binPath="%installpath%RdtClient.Web.exe" start=auto + timeout /t 5 /nobreak > NUL + net start RealDebridClient ) ELSE ( - echo ######## ######## ######## ####### ######## - echo ## ## ## ## ## ## ## ## ## - echo ## ## ## ## ## ## ## ## ## - echo ###### ######## ######## ## ## ######## - echo ## ## ## ## ## ## ## ## ## - echo ## ## ## ## ## ## ## ## ## - echo ######## ## ## ## ## ####### ## ## - echo. - echo. - echo ####### ERROR: ADMINISTRATOR PRIVILEGES REQUIRED ######### - echo This script must be run as administrator to work properly! - echo If you're seeing this after clicking on a start menu icon, - echo then right click on the shortcut and select "Run As Administrator". - echo ########################################################## - echo. - PAUSE - EXIT /B 1 + echo ######## ######## ######## ####### ######## + echo ## ## ## ## ## ## ## ## ## + echo ## ## ## ## ## ## ## ## ## + echo ###### ######## ######## ## ## ######## + echo ## ## ## ## ## ## ## ## ## + echo ## ## ## ## ## ## ## ## ## + echo ######## ## ## ## ## ####### ## ## + echo. + echo. + echo ####### ERROR: ADMINISTRATOR PRIVILEGES REQUIRED ######### + echo This script must be run as administrator to work properly! + echo If you're seeing this after clicking on a start menu icon, + echo then right click on the shortcut and select "Run As Administrator". + echo ########################################################## + echo. + PAUSE + EXIT /B 1 ) @echo ON \ No newline at end of file diff --git a/server/RdtClient.Web/serviceremove.bat b/server/RdtClient.Web/serviceremove.bat new file mode 100644 index 0000000..b6fa4f1 --- /dev/null +++ b/server/RdtClient.Web/serviceremove.bat @@ -0,0 +1,30 @@ +@echo off +set installpath=%~dp0 +NET SESSION >nul 2>&1 +IF %ERRORLEVEL% EQU 0 ( + echo removing firewall rules... + netsh.exe advfirewall firewall remove rule name="RealDebridClient" > nul + echo removing service... + net stop RealDebridClient + timeout /t 5 /nobreak > NUL + sc delete RealDebridClient +) ELSE ( + echo ######## ######## ######## ####### ######## + echo ## ## ## ## ## ## ## ## ## + echo ## ## ## ## ## ## ## ## ## + echo ###### ######## ######## ## ## ######## + echo ## ## ## ## ## ## ## ## ## + echo ## ## ## ## ## ## ## ## ## + echo ######## ## ## ## ## ####### ## ## + echo. + echo. + echo ####### ERROR: ADMINISTRATOR PRIVILEGES REQUIRED ######### + echo This script must be run as administrator to work properly! + echo If you're seeing this after clicking on a start menu icon, + echo then right click on the shortcut and select "Run As Administrator". + echo ########################################################## + echo. + PAUSE + EXIT /B 1 +) +@echo ON \ No newline at end of file diff --git a/server/RdtClient.Web/startup.bat b/server/RdtClient.Web/startup.bat deleted file mode 100644 index 4420b52..0000000 --- a/server/RdtClient.Web/startup.bat +++ /dev/null @@ -1 +0,0 @@ -dotnet RdtClient.Web.dll \ No newline at end of file diff --git a/server/RdtClient.Web/wwwroot/index.html b/server/RdtClient.Web/wwwroot/index.html index 65a724a..a27c525 100644 --- a/server/RdtClient.Web/wwwroot/index.html +++ b/server/RdtClient.Web/wwwroot/index.html @@ -9,5 +9,5 @@ - +