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