rdt-client/server/RdtClient.Service/Services/Torrents.cs

649 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MonoTorrent;
using Newtonsoft.Json;
using RDNET;
using RdtClient.Data.Data;
using RdtClient.Data.Enums;
using RdtClient.Service.Helpers;
using RdtClient.Service.Models;
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 TorrentData _torrentData;
public Torrents(IHttpClientFactory httpClientFactory, TorrentData torrentData, Downloads downloads)
{
_httpClientFactory = httpClientFactory;
_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;
}
public async Task<IList<Torrent>> Get()
{
var torrents = await _torrentData.Get();
foreach (var torrent in torrents)
{
foreach (var download in torrent.Downloads)
{
if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
{
download.Speed = downloadClient.Speed;
download.BytesTotal = downloadClient.BytesTotal;
download.BytesDone = downloadClient.BytesDone;
}
if (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
{
download.BytesTotal = unpackClient.BytesTotal;
download.BytesDone = unpackClient.BytesDone;
}
}
}
return torrents;
}
public async Task<Torrent> GetByHash(String hash)
{
var torrent = await _torrentData.GetByHash(hash);
if (torrent != null)
{
await Update(torrent);
}
return torrent;
}
public async Task UpdateCategory(String hash, String category)
{
var torrent = await _torrentData.GetByHash(hash);
if (torrent == null)
{
return;
}
await _torrentData.UpdateCategory(torrent.TorrentId, category);
}
public async Task<Torrent> UploadMagnet(String magnetLink,
String category,
TorrentDownloadAction downloadAction,
TorrentFinishedAction finishedAction,
Int32 downloadMinSize,
String downloadManualFiles)
{
MagnetLink magnet;
try
{
magnet = MagnetLink.Parse(magnetLink);
}
catch (Exception ex)
{
throw new Exception($"{ex.Message}, trying to parse {magnetLink}");
}
var rdTorrent = await GetRdNetClient().Torrents.AddMagnetAsync(magnetLink);
return await Add(rdTorrent.Id, magnet.InfoHash.ToHex(), category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, magnetLink, false);
}
public async Task<Torrent> UploadFile(Byte[] bytes,
String category,
TorrentDownloadAction downloadAction,
TorrentFinishedAction finishedAction,
Int32 downloadMinSize,
String downloadManualFiles)
{
MonoTorrent.Torrent torrent;
var fileAsBase64 = Convert.ToBase64String(bytes);
try
{
torrent = await MonoTorrent.Torrent.LoadAsync(bytes);
}
catch (Exception ex)
{
throw new Exception($"{ex.Message}, trying to parse {fileAsBase64}");
}
var rdTorrent = await GetRdNetClient().Torrents.AddFileAsync(bytes);
return await Add(rdTorrent.Id, torrent.InfoHash.ToHex(), category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, fileAsBase64, true);
}
public async Task<List<TorrentInstantAvailabilityFile>> 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}");
return groups.Select(m => m.First()).ToList();
}
public async Task SelectFiles(Guid torrentId, IList<String> fileIds)
{
var torrent = await GetById(torrentId);
await GetRdNetClient().Torrents.SelectFilesAsync(torrent.RdId, fileIds.ToArray());
}
public async Task CheckForLinks(Guid torrentId)
{
var torrent = await GetById(torrentId);
var rdTorrent = await GetRdNetClient().Torrents.GetInfoAsync(torrent.RdId);
var torrentLinks = rdTorrent.Links.Where(m => !String.IsNullOrWhiteSpace(m)).ToList();
// 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 &&
torrentLinks.Count != 1)
{
return;
}
foreach (var file in torrentLinks)
{
// Make sure downloads don't get added multiple times
var downloadExists = await _downloads.Get(torrent.TorrentId, file);
if (downloadExists == null && !String.IsNullOrWhiteSpace(file))
{
await _downloads.Add(torrent.TorrentId, file);
}
}
}
public async Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles)
{
var torrent = await GetById(torrentId);
if (torrent != null)
{
foreach (var download in torrent.Downloads)
{
if (TorrentRunner.ActiveUnpackClients.TryRemove(download.DownloadId, out var activeUnpack))
{
activeUnpack.Cancel();
}
if (TorrentRunner.ActiveDownloadClients.TryRemove(download.DownloadId, out var activeDownload))
{
activeDownload.Cancel();
}
}
if (deleteData)
{
await _torrentData.UpdateComplete(torrent.TorrentId, DateTimeOffset.UtcNow);
await _downloads.DeleteForTorrent(torrent.TorrentId);
await _torrentData.Delete(torrentId);
}
if (deleteRdTorrent)
{
await GetRdNetClient().Torrents.DeleteAsync(torrent.RdId);
}
if (deleteLocalFiles)
{
var downloadPath = DownloadPath(torrent);
downloadPath = Path.Combine(downloadPath, torrent.RdName);
if (Directory.Exists(downloadPath))
{
var retry = 0;
while (true)
{
try
{
Directory.Delete(downloadPath, true);
break;
}
catch
{
retry++;
if (retry >= 3)
{
throw;
}
await Task.Delay(1000);
}
}
}
}
}
}
private async Task DeleteDownload(Guid torrentId, Guid downloadId)
{
var torrent = await GetById(torrentId);
var download = await _downloads.GetById(downloadId);
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<String> UnrestrictLink(Guid downloadId)
{
var download = await _downloads.GetById(downloadId);
var unrestrictedLink = await GetRdNetClient().Unrestrict.LinkAsync(download.Path);
await _downloads.UpdateUnrestrictedLink(downloadId, unrestrictedLink.Download);
return unrestrictedLink.Download;
}
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);
}
public async Task<Profile> GetProfile()
{
var user = await GetRdNetClient().User.GetAsync();
var profile = new Profile
{
UserName = user.Username,
Expiration = user.Expiration
};
return profile;
}
public async Task Update()
{
await RealDebridUpdateLock.WaitAsync();
var torrents = await Get();
try
{
var rdTorrents = await GetRdNetClient().Torrents.GetAsync(0, 100);
foreach (var rdTorrent in rdTorrents)
{
var torrent = torrents.FirstOrDefault(m => m.RdId == rdTorrent.Id);
if (torrent == null)
{
continue;
}
await Update(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
{
RealDebridUpdateLock.Release();
}
}
public async Task RetryTorrent(Guid torrentId)
{
var torrent = await _torrentData.GetById(torrentId);
var newRetryCount = torrent.RetryCount + 1;
foreach (var download in torrent.Downloads)
{
while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
{
downloadClient.Cancel();
await Task.Delay(100);
}
while (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
{
unpackClient.Cancel();
await Task.Delay(100);
}
}
await Delete(torrentId, true, true, true);
if (String.IsNullOrWhiteSpace(torrent.FileOrMagnet))
{
throw new Exception($"Cannot re-add this torrent, original magnet or file not found");
}
await TorrentResetLock.WaitAsync();
try
{
Torrent newTorrent;
if (torrent.IsFile)
{
var bytes = Convert.FromBase64String(torrent.FileOrMagnet);
newTorrent = await UploadFile(bytes, torrent.Category, torrent.DownloadAction, torrent.FinishedAction, torrent.DownloadMinSize, torrent.DownloadManualFiles);
}
else
{
newTorrent = await UploadMagnet(torrent.FileOrMagnet,
torrent.Category,
torrent.DownloadAction,
torrent.FinishedAction,
torrent.DownloadMinSize,
torrent.DownloadManualFiles);
}
await _torrentData.UpdateRetryCount(newTorrent.TorrentId, newRetryCount);
}
finally
{
TorrentResetLock.Release();
}
}
public async Task RetryDownload(Guid downloadId)
{
var download = await _downloads.GetById(downloadId);
if (download == null)
{
return;
}
while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
{
downloadClient.Cancel();
await Task.Delay(100);
}
while (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
{
unpackClient.Cancel();
await Task.Delay(100);
}
await TorrentResetLock.WaitAsync();
try
{
await DeleteDownload(download.TorrentId, download.DownloadId);
await _torrentData.UpdateComplete(download.TorrentId, null);
await _downloads.Reset(downloadId);
}
finally
{
TorrentResetLock.Release();
}
}
public async Task UpdateComplete(Guid torrentId, DateTimeOffset datetime)
{
await _torrentData.UpdateComplete(torrentId, datetime);
}
public async Task UpdateFilesSelected(Guid torrentId, DateTimeOffset datetime)
{
await _torrentData.UpdateFilesSelected(torrentId, datetime);
}
public async Task<Torrent> GetById(Guid torrentId)
{
var torrent = await _torrentData.GetById(torrentId);
if (torrent != null)
{
await Update(torrent);
foreach (var download in torrent.Downloads)
{
if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
{
download.Speed = downloadClient.Speed;
download.BytesTotal = downloadClient.BytesTotal;
download.BytesDone = downloadClient.BytesDone;
}
if (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
{
download.BytesTotal = unpackClient.BytesTotal;
download.BytesDone = unpackClient.BytesDone;
}
}
}
return torrent;
}
private static String DownloadPath(Torrent torrent)
{
var settingDownloadPath = Settings.Get.DownloadPath;
if (!String.IsNullOrWhiteSpace(torrent.Category))
{
settingDownloadPath = Path.Combine(settingDownloadPath, torrent.Category);
}
return settingDownloadPath;
}
private async Task<Torrent> Add(String rdTorrentId,
String infoHash,
String category,
TorrentDownloadAction downloadAction,
TorrentFinishedAction finishedAction,
Int32 downloadMinSize,
String downloadManualFiles,
String fileOrMagnetContents,
Boolean isFile)
{
await RealDebridUpdateLock.WaitAsync();
try
{
var torrent = await _torrentData.GetByHash(infoHash);
if (torrent != null)
{
return torrent;
}
var newTorrent = await _torrentData.Add(rdTorrentId,
infoHash,
category,
downloadAction,
finishedAction,
downloadMinSize,
downloadManualFiles,
fileOrMagnetContents,
isFile);
await Update(newTorrent);
return newTorrent;
}
finally
{
RealDebridUpdateLock.Release();
}
}
private async Task Update(Torrent torrent)
{
var originalTorrent = JsonConvert.SerializeObject(torrent,
new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
var rdTorrent = await GetRdNetClient().Torrents.GetInfoAsync(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.Finished,
"dead" => RealDebridStatus.Error,
_ => RealDebridStatus.Error
};
var newTorrent = JsonConvert.SerializeObject(torrent,
new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
if (originalTorrent != newTorrent)
{
await _torrentData.UpdateRdData(torrent);
}
}
}
}