diff --git a/AnimeAnnouncer.Web/Pages/Index.cshtml b/AnimeAnnouncer.Web/Pages/Index.cshtml index 40aebe4..0f79d98 100644 --- a/AnimeAnnouncer.Web/Pages/Index.cshtml +++ b/AnimeAnnouncer.Web/Pages/Index.cshtml @@ -35,7 +35,7 @@
@foreach (var show in Model.AiringShows) { -
+
@if (!string.IsNullOrWhiteSpace(show.HeroImageUrl)) { @@ -169,6 +169,17 @@ { AniList } + @if (show.Download) + { + ✓ Marked for download + } + else + { +
+ + +
+ } @if (show.SecondaryImages.Count > 0) { diff --git a/AnimeAnnouncer.Web/Pages/Index.cshtml.cs b/AnimeAnnouncer.Web/Pages/Index.cshtml.cs index c83773d..1df3064 100644 --- a/AnimeAnnouncer.Web/Pages/Index.cshtml.cs +++ b/AnimeAnnouncer.Web/Pages/Index.cshtml.cs @@ -1,6 +1,7 @@ using System.Net; using System.Text.RegularExpressions; using AnimeAnnouncer.Cache; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace AnimeAnnouncer.Web.Pages; @@ -95,6 +96,25 @@ public class IndexModel : PageModel } } + public async Task 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 required string Url { get; set; } @@ -112,6 +132,7 @@ public class IndexModel : PageModel public string Title => AiringSoon.Title; public int ShowID => AiringSoon.ShowID; + public bool Download => AiringSoon.Download; public DateTime? EffectiveLastAirDate => AiringSoon.LastAirDate ?? CachedDetails?.LastAirDate; public int LatestEpisodeNumber => AiringSoon.LatestEpisodeNumber > 0 ? AiringSoon.LatestEpisodeNumber diff --git a/AnimeAnnouncer.sln b/AnimeAnnouncer.sln index 01b2177..3ea7ea4 100644 --- a/AnimeAnnouncer.sln +++ b/AnimeAnnouncer.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTorrentAdd", "MassTorre EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnimeAnnouncer.Web", "AnimeAnnouncer.Web\AnimeAnnouncer.Web.csproj", "{DD4F134D-B455-4A8C-8DDE-AA0ED735DA42}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnimeAnnouncer.Torrents", "AnimeAnnouncer.Torrents\AnimeAnnouncer.Torrents.csproj", "{901424DF-F8CD-449B-9517-73CA42D61B54}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution 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|x86.ActiveCfg = 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 GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AnimeAnnouncer/AnimeAnnouncer.csproj b/AnimeAnnouncer/AnimeAnnouncer.csproj index 5d49f0f..aa2fedc 100644 --- a/AnimeAnnouncer/AnimeAnnouncer.csproj +++ b/AnimeAnnouncer/AnimeAnnouncer.csproj @@ -27,4 +27,7 @@ ..\Libraries\TMDbLib.dll + + + \ No newline at end of file diff --git a/AnimeAnnouncer/Cache/AiringSoonItem.cs b/AnimeAnnouncer/Cache/AiringSoonItem.cs index 3d2aa2e..43bd397 100644 --- a/AnimeAnnouncer/Cache/AiringSoonItem.cs +++ b/AnimeAnnouncer/Cache/AiringSoonItem.cs @@ -7,5 +7,6 @@ namespace AnimeAnnouncer.Cache public int ShowID { get; set; } public DateTime? LastAirDate { get; set; } public int LatestEpisodeNumber { get; set; } + public bool Download { get; set; } } } \ No newline at end of file diff --git a/AnimeAnnouncer/Cache/TMDBCache.cs b/AnimeAnnouncer/Cache/TMDBCache.cs index 2d9729f..cb86b61 100644 --- a/AnimeAnnouncer/Cache/TMDBCache.cs +++ b/AnimeAnnouncer/Cache/TMDBCache.cs @@ -60,6 +60,19 @@ public class TMDBCache else return null; } + /// Sets the Download flag on one airing list entry. Returns false when the show is not in the list. + public async Task 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 KeyExists(String key) { var db = redis.GetDatabase(); diff --git a/AnimeAnnouncer/Program.cs b/AnimeAnnouncer/Program.cs index 8ad0ab5..d23be7d 100644 --- a/AnimeAnnouncer/Program.cs +++ b/AnimeAnnouncer/Program.cs @@ -4,6 +4,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using AnimeAnnouncer.Cache; using AnimeAnnouncer.RSS; +using AnimeAnnouncer.Torrents; using Mastonet; using Mastonet.Entities; using Microsoft.Extensions.Configuration; @@ -24,6 +25,9 @@ namespace AnimeAnnouncer private static Anilist? anilistClient; private static List airingSoonList; + + private static TransmissionService transmissionService; + private static String destinationFolder; static void Main(string[] args) { Console.WriteLine("Starting..."); @@ -64,6 +68,19 @@ namespace AnimeAnnouncer if (!String.IsNullOrEmpty(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; @@ -401,6 +418,13 @@ namespace AnimeAnnouncer { _ = tmdbCache.SetPair($"ShowAnnounced-{supposedShowId}", latestSeasonNumber.ToString()); _ = 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) { @@ -411,6 +435,28 @@ namespace AnimeAnnouncer 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() { @@ -530,9 +576,22 @@ namespace AnimeAnnouncer Console.WriteLine($"Not updating {cachedShow.Title} airdate due to episode number not incrementing"); return; } + airingSoonItem.Download = existingAiringItem.Download; airingSoonList.RemoveAll(s => s.ShowID == cachedShow.ShowID); 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)); _ = tmdbCache.SetAiringSoon(airingSoonList); Console.WriteLine($"Updated airing soon list, setting {cachedShow.Title} Season {cachedShow.LatestSeasonNumber} to end on {cachedShow.LastAirDate ?? DateTime.MaxValue}"); diff --git a/MassTorrentAdd/MassTorrentAdd.csproj b/MassTorrentAdd/MassTorrentAdd.csproj index 553a6bb..85fdf37 100644 --- a/MassTorrentAdd/MassTorrentAdd.csproj +++ b/MassTorrentAdd/MassTorrentAdd.csproj @@ -16,7 +16,6 @@ - @@ -25,4 +24,8 @@ + + + + diff --git a/MassTorrentAdd/Program.cs b/MassTorrentAdd/Program.cs index a040beb..b2cbbd5 100644 --- a/MassTorrentAdd/Program.cs +++ b/MassTorrentAdd/Program.cs @@ -1,15 +1,11 @@ -using Microsoft.Extensions.Configuration; -using System.Xml.Linq; -using Transmission.API.RPC; +using AnimeAnnouncer.Torrents; +using Microsoft.Extensions.Configuration; namespace MassTorrentAdd { internal class Program { - static String watchFolder, transmissionHost, transmissionUsername, transmissionPassword, transmissionDownloadFolder, destinationFolder; - static Transmission.API.RPC.Client transmissionClient; - - static void Main(string[] args) + static async Task Main(string[] args) { if(args.Length == 0) { @@ -17,60 +13,53 @@ namespace MassTorrentAdd return; } String url = args[0]; - if (!url.Contains("rss")) - url = url + "&page=rss"; var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appSettings.json", optional: false, reloadOnChange: true); IConfigurationRoot configuration = builder.Build(); - transmissionHost = configuration.GetValue("transmissionURI"); - transmissionUsername = configuration.GetValue("transmissionUsername"); - transmissionPassword = configuration.GetValue("transmissionPassword"); - destinationFolder = configuration.GetValue("destinationFolder"); + String transmissionHost = configuration.GetValue("transmissionURI")!; + String? transmissionUsername = configuration.GetValue("transmissionUsername"); + String? transmissionPassword = configuration.GetValue("transmissionPassword"); + String destinationFolder = configuration.GetValue("destinationFolder")!; Console.WriteLine($"Using transmission server {transmissionHost}"); - transmissionClient = new Transmission.API.RPC.Client(transmissionHost, null, transmissionUsername, transmissionPassword); - var sessionInformation = transmissionClient.GetSessionInformation(); - if (sessionInformation == null || String.IsNullOrEmpty(sessionInformation.Version)) + TransmissionService transmission; + try { - 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; } - - HttpClient httpClient = new HttpClient(); - var response = httpClient.GetStringAsync(url).Result; - if(response == null) + var nyaa = new NyaaRssReader(); + List items; + try { - 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; } - XDocument doc = XDocument.Parse(response); - XNamespace nyaaNS = doc.Root.GetNamespaceOfPrefix("nyaa"); - var items = from item in doc.Descendants("channel").Elements("item") - select new - { - 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, - }; + if (items.Count == 0) + { + Console.WriteLine("No items found in the feed"); + return; + } + Console.WriteLine("Downloading .."); foreach ( var item in items ) { Console.WriteLine($"{item.Title}"); } - String transmissionDirectoryName = items.First().Title.Trim(); - transmissionDirectoryName = System.Text.RegularExpressions.Regex.Replace(transmissionDirectoryName, @"E\d{2,3}.*?(\d{3,4}p)", " $1"); - transmissionDirectoryName = System.Text.RegularExpressions.Regex.Replace(transmissionDirectoryName, @"\s\(.*?\)$", ""); - transmissionDirectoryName = destinationFolder + (destinationFolder.EndsWith("/") ? String.Empty : "/") + transmissionDirectoryName.Replace(" ", "."); - Console.WriteLine($"{items.Count()} item(s) to {transmissionDirectoryName}"); + String transmissionDirectoryName = TorrentDirectoryNamer.FromTitle(items.First().Title, destinationFolder); + Console.WriteLine($"{items.Count} item(s) to {transmissionDirectoryName}"); Console.WriteLine("Continue?"); if(Console.ReadKey().KeyChar != 'y') @@ -80,30 +69,10 @@ namespace MassTorrentAdd } foreach (var item in items) { - var torrentData = httpClient.GetByteArrayAsync(item.Link).Result; - AddNewTorrentFileToServer(torrentData, transmissionDirectoryName); + var torrentData = await nyaa.DownloadTorrentAsync(item); + await transmission.AddNewTorrentFileToServerAsync(torrentData, transmissionDirectoryName); } 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 }); - - } } }