Initial commit of auto-adding torrent system

This commit is contained in:
2026-10-11 12:25:32 -04:00
parent 57b2d8835d
commit 32aca8b197
9 changed files with 159 additions and 65 deletions
+12 -1
View File
@@ -35,7 +35,7 @@
<div class="airing-list"> <div class="airing-list">
@foreach (var show in Model.AiringShows) @foreach (var show in Model.AiringShows)
{ {
<article class="airing-row @(show.IsFuture ? "is-upcoming" : "is-aired")"> <article id="show-@show.ShowID" class="airing-row @(show.IsFuture ? "is-upcoming" : "is-aired")">
@if (!string.IsNullOrWhiteSpace(show.HeroImageUrl)) @if (!string.IsNullOrWhiteSpace(show.HeroImageUrl))
{ {
<div class="airing-backdrop" style="background-image: url('@show.HeroImageUrl');" aria-hidden="true"></div> <div class="airing-backdrop" style="background-image: url('@show.HeroImageUrl');" aria-hidden="true"></div>
@@ -169,6 +169,17 @@
{ {
<a href="@show.AnilistUrl" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-outline-info">AniList</a> <a href="@show.AnilistUrl" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-outline-info">AniList</a>
} }
@if (show.Download)
{
<span class="btn btn-sm btn-success disabled" aria-disabled="true">✓ Marked for download</span>
}
else
{
<form method="post" asp-page-handler="Download" class="d-inline">
<input type="hidden" name="showId" value="@show.ShowID" />
<button type="submit" class="btn btn-sm btn-outline-success">⬇ Download</button>
</form>
}
@if (show.SecondaryImages.Count > 0) @if (show.SecondaryImages.Count > 0)
{ {
+21
View File
@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AnimeAnnouncer.Cache; using AnimeAnnouncer.Cache;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AnimeAnnouncer.Web.Pages; namespace AnimeAnnouncer.Web.Pages;
@@ -95,6 +96,25 @@ public class IndexModel : PageModel
} }
} }
public async Task<IActionResult> OnPostDownloadAsync(int showId)
{
if (_tmdbCache == null)
return StatusCode(StatusCodes.Status503ServiceUnavailable);
try
{
if (!await _tmdbCache.SetDownload(showId, true))
_logger.LogWarning("Download requested for show {ShowID}, which is not in the airing list", showId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to flag show {ShowID} for download", showId);
}
// Post/redirect/get, landing back on the row that was clicked.
return RedirectToPage(null, null, null, $"show-{showId}");
}
public class ShowImage public class ShowImage
{ {
public required string Url { get; set; } public required string Url { get; set; }
@@ -112,6 +132,7 @@ public class IndexModel : PageModel
public string Title => AiringSoon.Title; public string Title => AiringSoon.Title;
public int ShowID => AiringSoon.ShowID; public int ShowID => AiringSoon.ShowID;
public bool Download => AiringSoon.Download;
public DateTime? EffectiveLastAirDate => AiringSoon.LastAirDate ?? CachedDetails?.LastAirDate; public DateTime? EffectiveLastAirDate => AiringSoon.LastAirDate ?? CachedDetails?.LastAirDate;
public int LatestEpisodeNumber => AiringSoon.LatestEpisodeNumber > 0 public int LatestEpisodeNumber => AiringSoon.LatestEpisodeNumber > 0
? AiringSoon.LatestEpisodeNumber ? AiringSoon.LatestEpisodeNumber
+14
View File
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTorrentAdd", "MassTorre
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnimeAnnouncer.Web", "AnimeAnnouncer.Web\AnimeAnnouncer.Web.csproj", "{DD4F134D-B455-4A8C-8DDE-AA0ED735DA42}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnimeAnnouncer.Web", "AnimeAnnouncer.Web\AnimeAnnouncer.Web.csproj", "{DD4F134D-B455-4A8C-8DDE-AA0ED735DA42}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnimeAnnouncer.Torrents", "AnimeAnnouncer.Torrents\AnimeAnnouncer.Torrents.csproj", "{901424DF-F8CD-449B-9517-73CA42D61B54}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -55,6 +57,18 @@ Global
{DD4F134D-B455-4A8C-8DDE-AA0ED735DA42}.Release|x64.Build.0 = Release|Any CPU {DD4F134D-B455-4A8C-8DDE-AA0ED735DA42}.Release|x64.Build.0 = Release|Any CPU
{DD4F134D-B455-4A8C-8DDE-AA0ED735DA42}.Release|x86.ActiveCfg = Release|Any CPU {DD4F134D-B455-4A8C-8DDE-AA0ED735DA42}.Release|x86.ActiveCfg = Release|Any CPU
{DD4F134D-B455-4A8C-8DDE-AA0ED735DA42}.Release|x86.Build.0 = Release|Any CPU {DD4F134D-B455-4A8C-8DDE-AA0ED735DA42}.Release|x86.Build.0 = Release|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Debug|Any CPU.Build.0 = Debug|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Debug|x64.ActiveCfg = Debug|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Debug|x64.Build.0 = Debug|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Debug|x86.ActiveCfg = Debug|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Debug|x86.Build.0 = Debug|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Release|Any CPU.ActiveCfg = Release|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Release|Any CPU.Build.0 = Release|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Release|x64.ActiveCfg = Release|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Release|x64.Build.0 = Release|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Release|x86.ActiveCfg = Release|Any CPU
{901424DF-F8CD-449B-9517-73CA42D61B54}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+3
View File
@@ -27,4 +27,7 @@
<HintPath>..\Libraries\TMDbLib.dll</HintPath> <HintPath>..\Libraries\TMDbLib.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AnimeAnnouncer.Torrents\AnimeAnnouncer.Torrents.csproj" />
</ItemGroup>
</Project> </Project>
+1
View File
@@ -7,5 +7,6 @@ namespace AnimeAnnouncer.Cache
public int ShowID { get; set; } public int ShowID { get; set; }
public DateTime? LastAirDate { get; set; } public DateTime? LastAirDate { get; set; }
public int LatestEpisodeNumber { get; set; } public int LatestEpisodeNumber { get; set; }
public bool Download { get; set; }
} }
} }
+13
View File
@@ -60,6 +60,19 @@ public class TMDBCache
else else
return null; return null;
} }
/// <summary>Sets the Download flag on one airing list entry. Returns false when the show is not in the list.</summary>
public async Task<bool> SetDownload(int showID, bool download)
{
var list = await GetAiringList();
var item = list?.FirstOrDefault(s => s.ShowID == showID);
if (item == null)
return false;
item.Download = download;
await SetAiringSoon(list!);
return true;
}
public async Task<bool> KeyExists(String key) public async Task<bool> KeyExists(String key)
{ {
var db = redis.GetDatabase(); var db = redis.GetDatabase();
+59
View File
@@ -4,6 +4,7 @@ using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using AnimeAnnouncer.Cache; using AnimeAnnouncer.Cache;
using AnimeAnnouncer.RSS; using AnimeAnnouncer.RSS;
using AnimeAnnouncer.Torrents;
using Mastonet; using Mastonet;
using Mastonet.Entities; using Mastonet.Entities;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -24,6 +25,9 @@ namespace AnimeAnnouncer
private static Anilist? anilistClient; private static Anilist? anilistClient;
private static List<AiringSoonItem> airingSoonList; private static List<AiringSoonItem> airingSoonList;
private static TransmissionService transmissionService;
private static String destinationFolder;
static void Main(string[] args) static void Main(string[] args)
{ {
Console.WriteLine("Starting..."); Console.WriteLine("Starting...");
@@ -64,6 +68,19 @@ namespace AnimeAnnouncer
if (!String.IsNullOrEmpty(anilistToken)) if (!String.IsNullOrEmpty(anilistToken))
anilistClient = new Anilist(anilistToken); anilistClient = new Anilist(anilistToken);
String? transmissionHost = config["transmissionURI"];
String? transmissionUsername = config["transmissionUsername"];
String? transmissionPassword = config["transmissionPassword"];
destinationFolder = config["destinationFolder"];
if (transmissionHost != null && transmissionUsername != null && transmissionPassword != null &&
destinationFolder != null)
{
Console.WriteLine("Connecting to transmission");
transmissionService = TransmissionService.ConnectAsync(transmissionHost, transmissionUsername, transmissionPassword).Result;
}
airingSoonList = tmdbCache.GetAiringList().Result; airingSoonList = tmdbCache.GetAiringList().Result;
@@ -401,6 +418,13 @@ namespace AnimeAnnouncer
{ {
_ = tmdbCache.SetPair($"ShowAnnounced-{supposedShowId}", latestSeasonNumber.ToString()); _ = tmdbCache.SetPair($"ShowAnnounced-{supposedShowId}", latestSeasonNumber.ToString());
_ = PostStatus(cachedShow); _ = PostStatus(cachedShow);
var existingAiringItem = airingSoonList.FirstOrDefault(s => s.ShowID == cachedShow.ShowID);
if (existingAiringItem != null && existingAiringItem.Download)
{
Console.WriteLine($"Attempting to queue {existingAiringItem.Title} in the torrent client");
_ = AddToTorrentClient(cachedShow);
}
} }
catch(Exception ex) catch(Exception ex)
{ {
@@ -411,6 +435,28 @@ namespace AnimeAnnouncer
Console.WriteLine($"Choosing {title} as a finished season!"); Console.WriteLine($"Choosing {title} as a finished season!");
} }
private static async Task AddToTorrentClient(TMDBCacheItem cacheItem)
{
var reader = new NyaaRssReader();
var titleParts = TorrentDirectoryNamer.PrepareForURI(cacheItem.Title);
var seasonNumber = $"S{cacheItem.LatestSeasonNumber:D2}";
var url = $"https://nyaa.si/?f=0&c=0_0&q={titleParts}+{seasonNumber}+CR+VARYG+DUAL";
var items = await reader.ReadAsync(url);
if (items.Count != cacheItem.LastEpisodeNumber)
{
Console.WriteLine($"{cacheItem.Title} had {items.Count} results but only {cacheItem.LastEpisodeNumber} episodes. Aborting.");
return;
}
var folderName = TorrentDirectoryNamer.FromTitle(items.First().Title, destinationFolder);
foreach (var item in items)
{
var torrentData = await reader.DownloadTorrentAsync(item);
await transmissionService.AddNewTorrentFileToServerAsync(torrentData, folderName);
}
}
private static async Task CreateAiringMonthPost() private static async Task CreateAiringMonthPost()
{ {
@@ -530,9 +576,22 @@ namespace AnimeAnnouncer
Console.WriteLine($"Not updating {cachedShow.Title} airdate due to episode number not incrementing"); Console.WriteLine($"Not updating {cachedShow.Title} airdate due to episode number not incrementing");
return; return;
} }
airingSoonItem.Download = existingAiringItem.Download;
airingSoonList.RemoveAll(s => s.ShowID == cachedShow.ShowID); airingSoonList.RemoveAll(s => s.ShowID == cachedShow.ShowID);
airingSoonList.Add(airingSoonItem); airingSoonList.Add(airingSoonItem);
} }
// The web page can flag shows for download directly in the cache, so carry those
// flags over rather than overwriting them with this process's in-memory copy.
var cachedList = await tmdbCache.GetAiringList();
if (cachedList != null)
{
foreach (var cached in cachedList.Where(c => c.Download))
{
var local = airingSoonList.FirstOrDefault(s => s.ShowID == cached.ShowID);
if (local != null)
local.Download = true;
}
}
airingSoonList.Sort((showOne, showTwo) => showOne.LastAirDate.Value.CompareTo(showTwo.LastAirDate.Value)); airingSoonList.Sort((showOne, showTwo) => showOne.LastAirDate.Value.CompareTo(showTwo.LastAirDate.Value));
_ = tmdbCache.SetAiringSoon(airingSoonList); _ = tmdbCache.SetAiringSoon(airingSoonList);
Console.WriteLine($"Updated airing soon list, setting {cachedShow.Title} Season {cachedShow.LatestSeasonNumber} to end on {cachedShow.LastAirDate ?? DateTime.MaxValue}"); Console.WriteLine($"Updated airing soon list, setting {cachedShow.Title} Season {cachedShow.LatestSeasonNumber} to end on {cachedShow.LastAirDate ?? DateTime.MaxValue}");
+4 -1
View File
@@ -16,7 +16,6 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Transmission.API.RPC" Version="2.1.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -25,4 +24,8 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AnimeAnnouncer.Torrents\AnimeAnnouncer.Torrents.csproj" />
</ItemGroup>
</Project> </Project>
+32 -63
View File
@@ -1,15 +1,11 @@
using Microsoft.Extensions.Configuration; using AnimeAnnouncer.Torrents;
using System.Xml.Linq; using Microsoft.Extensions.Configuration;
using Transmission.API.RPC;
namespace MassTorrentAdd namespace MassTorrentAdd
{ {
internal class Program internal class Program
{ {
static String watchFolder, transmissionHost, transmissionUsername, transmissionPassword, transmissionDownloadFolder, destinationFolder; static async Task Main(string[] args)
static Transmission.API.RPC.Client transmissionClient;
static void Main(string[] args)
{ {
if(args.Length == 0) if(args.Length == 0)
{ {
@@ -17,60 +13,53 @@ namespace MassTorrentAdd
return; return;
} }
String url = args[0]; String url = args[0];
if (!url.Contains("rss"))
url = url + "&page=rss";
var builder = new ConfigurationBuilder() var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) .SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appSettings.json", optional: false, reloadOnChange: true); .AddJsonFile("appSettings.json", optional: false, reloadOnChange: true);
IConfigurationRoot configuration = builder.Build(); IConfigurationRoot configuration = builder.Build();
transmissionHost = configuration.GetValue<String>("transmissionURI"); String transmissionHost = configuration.GetValue<String>("transmissionURI")!;
transmissionUsername = configuration.GetValue<String>("transmissionUsername"); String? transmissionUsername = configuration.GetValue<String>("transmissionUsername");
transmissionPassword = configuration.GetValue<String>("transmissionPassword"); String? transmissionPassword = configuration.GetValue<String>("transmissionPassword");
destinationFolder = configuration.GetValue<String>("destinationFolder"); String destinationFolder = configuration.GetValue<String>("destinationFolder")!;
Console.WriteLine($"Using transmission server {transmissionHost}"); Console.WriteLine($"Using transmission server {transmissionHost}");
transmissionClient = new Transmission.API.RPC.Client(transmissionHost, null, transmissionUsername, transmissionPassword); TransmissionService transmission;
var sessionInformation = transmissionClient.GetSessionInformation(); try
if (sessionInformation == null || String.IsNullOrEmpty(sessionInformation.Version))
{ {
Console.WriteLine("An error occurred while receiving session information from the rpc server"); transmission = await TransmissionService.ConnectAsync(transmissionHost, transmissionUsername, transmissionPassword);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return; return;
} }
var nyaa = new NyaaRssReader();
HttpClient httpClient = new HttpClient(); List<NyaaRssItem> items;
var response = httpClient.GetStringAsync(url).Result; try
if(response == null)
{ {
Console.WriteLine("An error occurred while contacting Nyaa for a response"); items = await nyaa.ReadAsync(url);
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred while contacting Nyaa for a response: {ex.Message}");
return; return;
} }
XDocument doc = XDocument.Parse(response); if (items.Count == 0)
XNamespace nyaaNS = doc.Root.GetNamespaceOfPrefix("nyaa"); {
var items = from item in doc.Descendants("channel").Elements("item") Console.WriteLine("No items found in the feed");
select new return;
{ }
Title = item.Element("title").Value,
Published = item.Element("pubDate").Value,
Category = item.Element(nyaaNS + "category").Value,
CategoryID = item.Element(nyaaNS + "categoryId").Value,
Size = item.Element(nyaaNS + "size").Value,
Link = item.Element("link").Value,
Description = item.Element("description").Value,
PostID = item.Element("guid").Value,
};
Console.WriteLine("Downloading .."); Console.WriteLine("Downloading ..");
foreach ( var item in items ) foreach ( var item in items )
{ {
Console.WriteLine($"{item.Title}"); Console.WriteLine($"{item.Title}");
} }
String transmissionDirectoryName = items.First().Title.Trim(); String transmissionDirectoryName = TorrentDirectoryNamer.FromTitle(items.First().Title, destinationFolder);
transmissionDirectoryName = System.Text.RegularExpressions.Regex.Replace(transmissionDirectoryName, @"E\d{2,3}.*?(\d{3,4}p)", " $1"); Console.WriteLine($"{items.Count} item(s) to {transmissionDirectoryName}");
transmissionDirectoryName = System.Text.RegularExpressions.Regex.Replace(transmissionDirectoryName, @"\s\(.*?\)$", "");
transmissionDirectoryName = destinationFolder + (destinationFolder.EndsWith("/") ? String.Empty : "/") + transmissionDirectoryName.Replace(" ", ".");
Console.WriteLine($"{items.Count()} item(s) to {transmissionDirectoryName}");
Console.WriteLine("Continue?"); Console.WriteLine("Continue?");
if(Console.ReadKey().KeyChar != 'y') if(Console.ReadKey().KeyChar != 'y')
@@ -80,30 +69,10 @@ namespace MassTorrentAdd
} }
foreach (var item in items) foreach (var item in items)
{ {
var torrentData = httpClient.GetByteArrayAsync(item.Link).Result; var torrentData = await nyaa.DownloadTorrentAsync(item);
AddNewTorrentFileToServer(torrentData, transmissionDirectoryName); await transmission.AddNewTorrentFileToServerAsync(torrentData, transmissionDirectoryName);
} }
Console.WriteLine("Completed"); Console.WriteLine("Completed");
} }
private static void AddNewTorrentFileToServer(byte[] torrentFileData, String saveLocation)
{
Transmission.API.RPC.Entity.NewTorrent newTorrent = new()
{
DownloadDirectory = saveLocation,
Paused = true,
Metainfo = Convert.ToBase64String(torrentFileData)
};
var torrentInfo = transmissionClient.TorrentAdd(newTorrent);
var seedSettings = new Transmission.API.RPC.Arguments.TorrentSettings()
{
SeedRatioLimit = 1.0,
SeedRatioMode = 1,
IDs = [torrentInfo.ID]
};
transmissionClient.TorrentSet(seedSettings);
//transmissionClient.TorrentVerify(new object[] { torrentInfo.ID });
}
} }
} }