Changed how the service gets installed, fixed few bugs

This commit is contained in:
Roger Far 2020-04-17 07:30:29 -06:00
parent f4ae266d19
commit dc4992a360
14 changed files with 218 additions and 153 deletions

View file

@ -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

View file

@ -12,8 +12,6 @@ namespace RdtClient.Service
services.AddScoped<IQBittorrent, QBittorrent>();
services.AddScoped<ISettings, Settings>();
services.AddScoped<ITorrents, Torrents>();
services.AddHostedService<TaskRunner>();
}
}
}

View file

@ -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,

View file

@ -19,6 +19,7 @@ namespace RdtClient.Service.Services
Task<TorrentProperties> 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<IDictionary<String, TorrentCategory>> 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);

View file

@ -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<Guid, DownloadManager> ActiveDownloads = new ConcurrentDictionary<Guid, DownloadManager>();
private readonly ILogger<TaskRunner> _logger;
private readonly IServiceProvider _services;
private Timer _timer;
private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
public TaskRunner(ILogger<TaskRunner> 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<IDownloads>();
var settings = scope.ServiceProvider.GetRequiredService<ISettings>();
var torrents = scope.ServiceProvider.GetRequiredService<ITorrents>();
var rdKey = await settings.GetString("RealDebridApiKey");
if (String.IsNullOrWhiteSpace(rdKey))
{
var downloads = scope.ServiceProvider.GetRequiredService<IDownloads>();
var settings = scope.ServiceProvider.GetRequiredService<ISettings>();
var torrents = scope.ServiceProvider.GetRequiredService<ITorrents>();
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);
}
});
}

View file

@ -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);
}
}
}
}
}

View file

@ -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<ActionResult> 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; }
}
}

View file

@ -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<Startup>()
.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<TaskRunner>();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
logging.AddFile($"{AppContext.BaseDirectory}app.log");
})
.UseUrls(hostUrl)
.UseKestrel()
.UseStartup<Startup>();
})
.Build()
.RunAsync();
}
}
/*public class Program
{
public static void Main(String[] args)
{
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.CaptureStartupErrors(false)
.UseStartup<Startup>()
.UseKestrel();
});
}
}*/
}

View file

@ -2,22 +2,19 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<None Remove="nssm.exe" />
<None Remove="serviceinstall.bat" />
<None Remove="startup.bat" />
<None Remove="serviceremove.bat" />
</ItemGroup>
<ItemGroup>
<Content Include="nssm.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="serviceinstall.bat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="startup.bat">
<Content Include="serviceremove.bat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
@ -32,6 +29,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="3.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NReco.Logging.File" Version="1.0.5" />

Binary file not shown.

View file

@ -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

View file

@ -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

View file

@ -1 +0,0 @@
dotnet RdtClient.Web.dll

View file

@ -9,5 +9,5 @@
<link rel="stylesheet" href="styles.8d1160aad8efbde1424d.css"></head>
<body>
<app-root></app-root>
<script src="runtime-es2015.1eba213af0b233498d9d.js" type="module"></script><script src="runtime-es5.1eba213af0b233498d9d.js" nomodule defer></script><script src="polyfills-es5.9e286f6d9247438cbb02.js" nomodule defer></script><script src="polyfills-es2015.690002c25ea8557bb4b0.js" type="module"></script><script src="main-es2015.ae1b4548c83ee352697f.js" type="module"></script><script src="main-es5.ae1b4548c83ee352697f.js" nomodule defer></script></body>
<script src="runtime-es2015.1eba213af0b233498d9d.js" type="module"></script><script src="runtime-es5.1eba213af0b233498d9d.js" nomodule defer></script><script src="polyfills-es5.9e286f6d9247438cbb02.js" nomodule defer></script><script src="polyfills-es2015.690002c25ea8557bb4b0.js" type="module"></script><script src="main-es2015.62ca07a5f3e2338db447.js" type="module"></script><script src="main-es5.62ca07a5f3e2338db447.js" nomodule defer></script></body>
</html>