using System.Text.RegularExpressions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MonoTorrent; using RdtClient.Data.Models.DebridClient; using RdtClient.Data.Models.Internal; using RdtClient.Service.BackgroundServices; using RdtClient.Service.Helpers; using RdtClient.Service.Services; using Torrent = RdtClient.Data.Models.Data.Torrent; namespace RdtClient.Web.Controllers; [Authorize(Policy = "AuthSetting")] [Route("Api/Torrents")] public class TorrentsController(ILogger logger, Torrents torrents, TorrentRunner torrentRunner, IRateLimitCoordinator coordinator) : Controller { [HttpGet] [Route("")] public async Task>> GetAll() { var results = await torrents.Get(); var torrentDtos = results.Select(torrent => TorrentDtoMapper.ToListDto(torrent, torrents.GetDownloadStats)) .ToList(); return Ok(torrentDtos); } [HttpGet] [Route("Get/{torrentId:guid}")] public async Task> GetById(Guid torrentId) { var torrent = await torrents.GetById(torrentId); if (torrent == null) { return NotFound(); } foreach (var file in torrent.Downloads) { file.Torrent = null; } var torrentDto = TorrentDtoMapper.ToDetailDto(torrent, torrents.GetDownloadStats); return Ok(torrentDto); } [HttpGet] [Route("DiskSpaceStatus")] public ActionResult GetDiskSpaceStatus() { var status = DiskSpaceMonitor.GetCurrentStatus(); return Ok(status); } [HttpGet] [Route("RateLimitStatus")] public ActionResult GetRateLimitStatus() { var nextDequeueTime = coordinator.GetMaxNextAllowedAt(); if (nextDequeueTime == null || nextDequeueTime < DateTimeOffset.Now) { return Ok(new RateLimitStatus { NextDequeueTime = null, SecondsRemaining = 0 }); } return Ok(new RateLimitStatus { NextDequeueTime = nextDequeueTime, SecondsRemaining = (nextDequeueTime.Value - DateTimeOffset.Now).TotalSeconds }); } /// /// Used for debugging only. Force a tick. /// /// [HttpGet] [Route("Tick")] public async Task Tick() { await torrentRunner.Tick(); return Ok(); } [HttpPost] [Route("UploadFile")] public async Task UploadFile([FromForm] IFormFile? file, [ModelBinder(BinderType = typeof(JsonModelBinder))] TorrentControllerUploadFileRequest? formData) { if (file == null || file.Length <= 0) { return BadRequest("Invalid torrent file"); } if (formData?.Torrent == null) { return BadRequest("Invalid Torrent"); } logger.LogDebug($"Add file"); var bytes = await ReadFormFileBytes(file); await torrents.AddFileToDebridQueue(bytes, formData.Torrent); return Ok(); } [HttpPost] [Route("UploadMagnet")] public async Task UploadMagnet([FromBody] TorrentControllerUploadMagnetRequest? request) { if (request == null) { return BadRequest(); } if (String.IsNullOrEmpty(request.MagnetLink)) { return BadRequest("Invalid magnet link"); } if (request.Torrent == null) { return BadRequest("Invalid Torrent"); } logger.LogDebug($"Add magnet"); await torrents.AddMagnetToDebridQueue(request.MagnetLink, request.Torrent); return Ok(); } [HttpPost] [Route("UploadNzbFile")] public async Task UploadNzbFile([FromForm] IFormFile? file, [ModelBinder(BinderType = typeof(JsonModelBinder))] TorrentControllerUploadFileRequest? formData) { if (file == null || file.Length <= 0) { return BadRequest("Invalid nzb file"); } if (formData?.Torrent == null) { return BadRequest("Invalid Torrent"); } logger.LogDebug($"Add nzb file"); if (String.IsNullOrWhiteSpace(formData.Torrent.RdName)) { formData.Torrent.RdName = file.FileName; } var bytes = await ReadFormFileBytes(file); await torrents.AddNzbFileToDebridQueue(bytes, file.FileName, formData.Torrent); return Ok(); } [HttpPost] [Route("UploadNzbLink")] public async Task UploadNzbLink([FromBody] TorrentControllerUploadNzbLinkRequest? request) { if (request == null) { return BadRequest(); } if (String.IsNullOrEmpty(request.NzbLink)) { return BadRequest("Invalid nzb link"); } if (request.Torrent == null) { return BadRequest("Invalid Torrent"); } logger.LogDebug($"Add nzb link {request.NzbLink}"); await torrents.AddNzbLinkToDebridQueue(request.NzbLink, request.Torrent); return Ok(); } [HttpPost] [Route("CheckFiles")] public async Task CheckFiles([FromForm] IFormFile? file) { if (file == null || file.Length <= 0) { return BadRequest("Invalid torrent file"); } var bytes = await ReadFormFileBytes(file); var torrent = await MonoTorrent.Torrent.LoadAsync(bytes); var result = await torrents.GetAvailableFiles(torrent.InfoHashes.V1OrV2.ToHex()); return Ok(result); } [HttpPost] [Route("CheckFilesMagnet")] public async Task CheckFilesMagnet([FromBody] TorrentControllerCheckFilesRequest? request) { if (request == null) { return BadRequest(); } if (String.IsNullOrEmpty(request.MagnetLink)) { return BadRequest("MagnetLink cannot be null or empty"); } var magnet = MagnetLink.Parse(request.MagnetLink); var result = await torrents.GetAvailableFiles(magnet.InfoHashes.V1OrV2.ToHex()); return Ok(result); } [HttpPost] [Route("Delete/{torrentId:guid}")] public async Task Delete(Guid torrentId, [FromBody] TorrentControllerDeleteRequest? request) { if (request == null) { return BadRequest(); } logger.LogDebug("Delete {torrentId}", torrentId); await torrents.Delete(torrentId, request.DeleteData, request.DeleteRdTorrent, request.DeleteLocalFiles); return Ok(); } [HttpPost] [Route("Retry/{torrentId:guid}")] public async Task Retry(Guid torrentId) { logger.LogDebug("Retry {torrentId}", torrentId); await torrents.UpdateRetry(torrentId, DateTimeOffset.UtcNow, 0); await torrents.RetryTorrent(torrentId, 0); return Ok(); } [HttpPost] [Route("RetryDownload/{downloadId:guid}")] public async Task RetryDownload(Guid downloadId) { logger.LogDebug("Retry download {downloadId}", downloadId); await torrents.RetryDownload(downloadId); return Ok(); } [HttpPut] [Route("Update")] public async Task Update([FromBody] Torrent? torrent) { if (torrent == null) { return BadRequest(); } await torrents.Update(torrent); return Ok(); } [HttpPost] [Route("VerifyRegex")] public async Task VerifyRegex([FromForm] IFormFile? file, [FromBody] TorrentControllerVerifyRegexRequest? request) { if (request == null) { return Ok(); } var includeError = ""; var excludeError = ""; IList availableFiles; if (!String.IsNullOrWhiteSpace(request.MagnetLink)) { var magnet = MagnetLink.Parse(request.MagnetLink); availableFiles = await torrents.GetAvailableFiles(magnet.InfoHashes.V1OrV2.ToHex()); } else if (file != null) { var bytes = await ReadFormFileBytes(file); var torrent = await MonoTorrent.Torrent.LoadAsync(bytes); availableFiles = await torrents.GetAvailableFiles(torrent.InfoHashes.V1OrV2.ToHex()); } else { return BadRequest(); } var selectedFiles = new List(); if (!String.IsNullOrWhiteSpace(request.IncludeRegex)) { var includeRegex = BuildRegex(request.IncludeRegex, out includeError); if (includeRegex != null) { foreach (var availableFile in availableFiles) { if (includeRegex.IsMatch(availableFile.Filename)) { selectedFiles.Add(availableFile); } } } } else if (!String.IsNullOrWhiteSpace(request.ExcludeRegex)) { var excludeRegex = BuildRegex(request.ExcludeRegex, out excludeError); if (excludeRegex != null) { foreach (var availableFile in availableFiles) { if (!excludeRegex.IsMatch(availableFile.Filename)) { selectedFiles.Add(availableFile); } } } } else { selectedFiles = [.. availableFiles]; } return Ok(new { includeError, excludeError, selectedFiles }); } private static Regex? BuildRegex(String pattern, out String error) { try { error = ""; return new(pattern, RegexOptions.Compiled); } catch (Exception ex) { error = ex.Message; return null; } } private static async Task ReadFormFileBytes(IFormFile file) { await using var fileStream = file.OpenReadStream(); await using var memoryStream = file.Length is > 0 and <= Int32.MaxValue ? new((Int32)file.Length) : new MemoryStream(); await fileStream.CopyToAsync(memoryStream); return memoryStream.ToArray(); } } public class TorrentControllerUploadFileRequest { public Torrent? Torrent { get; set; } } public class TorrentControllerUploadMagnetRequest { public String? MagnetLink { get; set; } public Torrent? Torrent { get; set; } } public class TorrentControllerUploadNzbLinkRequest { public String? NzbLink { get; set; } public Torrent? Torrent { get; set; } } public class TorrentControllerDeleteRequest { public Boolean DeleteData { get; set; } public Boolean DeleteRdTorrent { get; set; } public Boolean DeleteLocalFiles { get; set; } } public class TorrentControllerCheckFilesRequest { public String? MagnetLink { get; set; } } public class TorrentControllerVerifyRegexRequest { public String? IncludeRegex { get; set; } public String? ExcludeRegex { get; set; } public String? MagnetLink { get; set; } }