using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MonoTorrent;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
using RdtClient.Service.Services;
using Torrent = RdtClient.Data.Models.Data.Torrent;
using System.Text.Json.Serialization;
using NSwag.Annotations;
namespace RdtClient.Web.Controllers;
///
/// Controller for managing torrents and their downloads
///
[Authorize(Policy = "AuthSetting")]
[Route("Api/Torrents")]
public class TorrentsController(ILogger logger, Torrents torrents, TorrentRunner torrentRunner) : Controller
{
///
/// Retrieves all torrents and their associated downloads
///
/// List of all torrents with their download status
/// Returns the list of torrents
[HttpGet]
[Route("")]
[ProducesResponseType(typeof(IList), StatusCodes.Status200OK)]
public async Task>> GetAll()
{
var results = await torrents.Get();
// Prevent infinite recursion when serializing
foreach (var file in results.SelectMany(torrent => torrent.Downloads))
{
file.Torrent = null;
}
return Ok(results);
}
///
/// Retrieves a specific torrent by its ID
///
/// The unique identifier of the torrent
/// The requested torrent details
/// Returns the requested torrent
/// Torrent not found
[HttpGet]
[Route("Get/{torrentId:guid}")]
[ProducesResponseType(typeof(Torrent), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task> GetById(Guid torrentId)
{
var torrent = await torrents.GetById(torrentId);
if (torrent?.Downloads != null)
{
foreach (var file in torrent.Downloads)
{
file.Torrent = null;
}
}
return Ok(torrent);
}
///
/// Forces an immediate processing cycle for debugging purposes
///
/// Success status
/// Processing cycle completed successfully
[HttpGet]
[Route("Tick")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task Tick()
{
await torrentRunner.Tick();
return Ok();
}
///
/// Adds a new torrent file with configuration
///
/// The .torrent file to add
/// Configuration for the torrent download
/// Success status
/// Torrent added successfully
/// Invalid file or configuration provided
[HttpPost]
[Route("UploadFile")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(String), StatusCodes.Status400BadRequest)]
public async Task UploadFile([OpenApiFile] IFormFile? file,
[ModelBinder(BinderType = typeof(JsonModelBinder))]
[FromForm] 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 fileStream = file.OpenReadStream();
await using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream);
var bytes = memoryStream.ToArray();
await torrents.UploadFile(bytes, formData.Torrent);
return Ok();
}
///
/// Adds a new torrent using a magnet link
///
/// The magnet link and torrent configuration
/// Success status
/// Magnet link processed successfully
/// Invalid magnet link or configuration
[HttpPost]
[Route("UploadMagnet")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(String), StatusCodes.Status400BadRequest)]
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.UploadMagnet(request.MagnetLink, request.Torrent);
return Ok();
}
///
/// Checks available files in a torrent file
///
/// The .torrent file to analyze
/// List of available files in the torrent
/// Returns the list of available files
/// Invalid torrent file provided
[HttpPost]
[Route("CheckFiles")]
[ProducesResponseType(typeof(IList), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(String), StatusCodes.Status400BadRequest)]
public async Task>> CheckFiles([FromForm] IFormFile? file)
{
if (file == null || file.Length <= 0)
{
return BadRequest("Invalid torrent file");
}
var fileStream = file.OpenReadStream();
await using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream);
var bytes = memoryStream.ToArray();
var torrent = await MonoTorrent.Torrent.LoadAsync(bytes);
var result = await torrents.GetAvailableFiles(torrent.InfoHashes.V1OrV2.ToHex());
return Ok(result);
}
///
/// Checks available files from a magnet link
///
/// The magnet link to analyze
/// List of available files in the torrent
/// Returns the list of available files
/// Invalid magnet link provided
[HttpPost]
[Route("CheckFilesMagnet")]
[ProducesResponseType(typeof(IList), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(String), StatusCodes.Status400BadRequest)]
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);
}
///
/// Deletes a torrent and optionally its associated data
///
/// The unique identifier of the torrent to delete
/// Delete options specifying what should be removed
/// Success status
/// Torrent deleted successfully
/// Invalid request parameters
[HttpPost]
[Route("Delete/{torrentId:guid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
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();
}
///
/// Retries a failed torrent download
///
/// The unique identifier of the torrent to retry
/// Success status
/// Retry initiated successfully
[HttpPost]
[Route("Retry/{torrentId:guid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
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();
}
///
/// Retries a failed download within a torrent
///
/// The unique identifier of the download to retry
/// Success status
/// Retry initiated successfully
[HttpPost]
[Route("RetryDownload/{downloadId:guid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task RetryDownload(Guid downloadId)
{
logger.LogDebug("Retry download {downloadId}", downloadId);
await torrents.RetryDownload(downloadId);
return Ok();
}
///
/// Updates torrent configuration
///
/// The updated torrent configuration
/// Success status
/// Torrent updated successfully
/// Invalid torrent configuration
[HttpPut]
[Route("Update")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task Update([FromBody] Torrent? torrent)
{
if (torrent == null)
{
return BadRequest();
}
await torrents.Update(torrent);
return Ok();
}
///
/// Tests regex patterns against torrent files
///
/// The regex patterns and magnet link to test
/// Matching files and any regex errors
/// Returns the regex test results
/// Invalid request parameters
[HttpPost]
[Route("VerifyRegex")]
[ProducesResponseType(typeof(RegexVerificationResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task> VerifyRegex([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
{
return BadRequest();
}
var selectedFiles = new List();
if (!String.IsNullOrWhiteSpace(request.IncludeRegex))
{
foreach (var availableFile in availableFiles)
{
try
{
if (Regex.IsMatch(availableFile.Filename, request.IncludeRegex))
{
selectedFiles.Add(availableFile);
}
}
catch (Exception ex)
{
includeError = ex.Message;
}
}
}
else if (!String.IsNullOrWhiteSpace(request.ExcludeRegex))
{
foreach (var availableFile in availableFiles)
{
try
{
if (!Regex.IsMatch(availableFile.Filename, request.ExcludeRegex))
{
selectedFiles.Add(availableFile);
}
}
catch (Exception ex)
{
excludeError = ex.Message;
}
}
}
else
{
selectedFiles = [.. availableFiles];
}
return Ok(new RegexVerificationResult
{
IncludeError = includeError,
ExcludeError = excludeError,
SelectedFiles = selectedFiles
});
}
}
///
/// Request model for uploading a torrent file
///
public class TorrentControllerUploadFileRequest
{
///
/// Configuration for the torrent download
///
[Required]
public Torrent? Torrent { get; set; }
}
///
/// Request model for adding a magnet link
///
public class TorrentControllerUploadMagnetRequest
{
///
/// The magnet URI to process
///
[Required]
public String? MagnetLink { get; set; }
///
/// Configuration for the torrent download
///
[Required]
public Torrent? Torrent { get; set; }
}
///
/// Request model for deleting a torrent
///
public class TorrentControllerDeleteRequest
{
///
/// Whether to delete the downloaded data
///
public Boolean DeleteData { get; set; }
///
/// Whether to remove the torrent from the Debrid service
///
public Boolean DeleteRdTorrent { get; set; }
///
/// Whether to delete local torrent files
///
public Boolean DeleteLocalFiles { get; set; }
}
///
/// Request model for checking files in a magnet link
///
public class TorrentControllerCheckFilesRequest
{
///
/// The magnet URI to analyze
///
[Required]
public String? MagnetLink { get; set; }
}
///
/// Request model for verifying regex patterns
///
public class TorrentControllerVerifyRegexRequest
{
///
/// Pattern for including files
///
public String? IncludeRegex { get; set; }
///
/// Pattern for excluding files
///
public String? ExcludeRegex { get; set; }
///
/// Magnet link to test patterns against
///
public String? MagnetLink { get; set; }
}
///
/// Response model for regex verification results
///
public class RegexVerificationResult
{
///
/// Error message for the include regex pattern, if any
///
[JsonPropertyName("includeError")]
public String IncludeError { get; set; } = String.Empty;
///
/// Error message for the exclude regex pattern, if any
///
[JsonPropertyName("excludeError")]
public String ExcludeError { get; set; } = String.Empty;
///
/// Files that match the specified patterns
///
[JsonPropertyName("selectedFiles")]
public IList SelectedFiles { get; set; } = new List();
}