diff --git a/AnimeAnnouncer/AnimeAnnouncer.csproj b/AnimeAnnouncer/AnimeAnnouncer.csproj index e076340..2ef1adf 100644 --- a/AnimeAnnouncer/AnimeAnnouncer.csproj +++ b/AnimeAnnouncer/AnimeAnnouncer.csproj @@ -1,5 +1,4 @@ - - + Exe net8.0 @@ -7,9 +6,9 @@ enable Linux - + + - - + \ No newline at end of file diff --git a/AnimeAnnouncer/Program.cs b/AnimeAnnouncer/Program.cs index 5e61698..66d2dc3 100644 --- a/AnimeAnnouncer/Program.cs +++ b/AnimeAnnouncer/Program.cs @@ -1,10 +1,84 @@ -namespace AnimeAnnouncer +using AnimeAnnouncer.RSS; +using TMDbLib.Client; + +namespace AnimeAnnouncer { internal class Program { + private static NyaaIndexer nyaaIndexer= new NyaaIndexer(); + + private const String TMDBApiKey = ""; + + private static TMDbClient tmdbClient = new TMDbClient(TMDBApiKey); static void Main(string[] args) { - Console.WriteLine("Hello, World!"); + Console.WriteLine("Starting..."); + + nyaaIndexer.NewPost += OnNewPost; + nyaaIndexer.RssReadFinished += OnRssReadFinished; + nyaaIndexer.Start(true); + + Console.WriteLine("Press enter to quit..."); + Console.ReadLine(); + } + + static async void OnNewPost(RSS.NyaaItem item) + { + try + { + ProcessNewItem(item).Wait(); + } + catch(Exception ex) + { + Console.WriteLine(ex.ToString()); + } + } + + static void OnRssReadFinished() + { + + } + + static async Task ProcessNewItem(NyaaItem item) + { + Console.WriteLine($"Processing {item.Title}"); + + //Parse release name using Anitomy library + var items = AnitomySharp.AnitomySharp.Parse(item.Title); + var title = items.FirstOrDefault(p => p.Category == AnitomySharp.Element.ElementCategory.ElementAnimeTitle)?.Value; + var episodeTitle = items.FirstOrDefault(p => p.Category == AnitomySharp.Element.ElementCategory.ElementEpisodeTitle)?.Value; + var season = items.FirstOrDefault(p => p.Category == AnitomySharp.Element.ElementCategory.ElementAnimeSeason)?.Value; + var episode = items.FirstOrDefault(p => p.Category == AnitomySharp.Element.ElementCategory.ElementEpisodeNumber)?.Value; + if(title == null || season == null || episode == null) + { + Console.WriteLine("Skipping release due to inability to determine title, season, or episode number."); + return; + } + + var searchResults = await tmdbClient.SearchTvShowAsync(title); + + var supposedShowId = searchResults.Results.First().Id; + + var showResult = await tmdbClient.GetTvShowAsync(supposedShowId); + + var latestSeason = showResult.Seasons.OrderByDescending(s => s.SeasonNumber).First(); + + if(latestSeason.SeasonNumber != int.Parse(season)) + { + Console.WriteLine($"Failing release due to TMDB's season number {latestSeason.SeasonNumber} not matching title season {season}"); + return; + } + + if(latestSeason.EpisodeCount != int.Parse(episode)) + { + Console.WriteLine($"Failing release due to TMDB's last episode number of {latestSeason.EpisodeCount} not matching title episode number {episode}"); + return; + } + + // + + //AnnounceFinishedSeason(title); + Console.WriteLine($"Choosing {title} as a finished season!"); } } } diff --git a/AnimeAnnouncer/RSS/IndexerBase.cs b/AnimeAnnouncer/RSS/IndexerBase.cs new file mode 100644 index 0000000..9b36a3f --- /dev/null +++ b/AnimeAnnouncer/RSS/IndexerBase.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Xml.Linq; + +namespace AnimeAnnouncer.RSS +{ + public abstract class IndexerBase + where T : IndexerItem + { + public IndexerBase() + { + IterationSleepTime = 300; + PostEventSleepTime = 1; + } + public virtual void Start(Boolean populateSeenURLs = true) + { + Running = true; + //Populate SeenURLs + if(populateSeenURLs) + try + { + fillSeenURLs(GetRSSResponse()); + } + catch(Exception ex) + { + Console.WriteLine(ex.ToString()); + } + ThreadPool.QueueUserWorkItem(p => rssLoop()); + } + public virtual void Stop() + { + Running = false; + } + + protected virtual String GetRSSResponse() + { + HttpWebRequest req = WebRequest.Create(GetRSSURL()) as HttpWebRequest; + HttpWebResponse resp = req.GetResponse() as HttpWebResponse; + return new StreamReader(resp.GetResponseStream()).ReadToEnd(); + } + + protected abstract String GetRSSURL(); + + protected abstract void HandleRSSResponse(String rssResponse); + + protected virtual void OnNewPost(T post) + { + NewPost?.Invoke(post); + } + protected virtual void OnRssFinished() + { + RssReadFinished?.Invoke(); + } + + private void rssLoop() + { + while (Running) + { + Thread.Sleep(IterationSleepTime * 1000); + + try + { + String xml = GetRSSResponse(); + HandleRSSResponse(xml); + RssReadFinished?.Invoke(); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + + } + } + private void fillSeenURLs(String xml) + { + XDocument doc = XDocument.Parse(xml); + var items = doc.Descendants("channel").Elements("item"); + foreach (var item in items) + SeenURLs.Add(item.Element("link").Value); + } + + public event Action NewPost; + public event Action RssReadFinished; + /// + /// Time in seconds to wait before raising the new post event. To avoid site hammering. + /// + public Int32 PostEventSleepTime { get; set; } + /// + /// Time in seconds to wait before refreshing RSS again + /// + public Int32 IterationSleepTime { get; set; } + + + protected readonly List SeenURLs = new List(); + private Boolean Running; + } +} diff --git a/AnimeAnnouncer/RSS/IndexerItem.cs b/AnimeAnnouncer/RSS/IndexerItem.cs new file mode 100644 index 0000000..a646a71 --- /dev/null +++ b/AnimeAnnouncer/RSS/IndexerItem.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace AnimeAnnouncer.RSS +{ + public class IndexerItem + { + public String Title { get; set; } + public String GUID { get; set; } + public String Link { get; set; } + public String Comments { get; set; } + public DateTime PublicationDate { get; set; } + public String Category { get; set; } + public String Description { get; set; } + } +} diff --git a/AnimeAnnouncer/RSS/NyaaIndexer.cs b/AnimeAnnouncer/RSS/NyaaIndexer.cs new file mode 100644 index 0000000..998a833 --- /dev/null +++ b/AnimeAnnouncer/RSS/NyaaIndexer.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Linq; + +namespace AnimeAnnouncer.RSS +{ + public class NyaaIndexer : IndexerBase + { + + public NyaaIndexer() + { + // 30 minutes + IterationSleepTime = (30 * 60); + + //Slow down for debugging to avoid API hammer + PostEventSleepTime = 5; + } + protected override string GetRSSURL() + { + return "https://nyaa.si/?page=rss&q=Dual+Audio&c=0_0&f=0"; + } + + protected override void HandleRSSResponse(string rssResponse) + { + XDocument doc = null; + XNamespace nyaaNS; + + try + { + doc = XDocument.Parse(rssResponse); + nyaaNS = doc.Root.GetNamespaceOfPrefix("nyaa"); + } + catch (Exception) + { + throw; + } + try + { + 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, + }; + foreach (var item in items) + { + if (SeenURLs.Contains(item.Link)) + continue; + + NyaaItem post = new NyaaItem() + { + Title = item.Title, + PublicationDate = DateTime.ParseExact(item.Published, "ddd, dd MMM yyyy HH:mm:ss K", new System.Globalization.CultureInfo("en-US")).ToUniversalTime(), + Category = item.Category, + Link = item.Link, + Description = item.Description, + GUID = item.PostID, + Size = Double.Parse(item.Size.Substring(0,item.Size.IndexOf(' '))) + }; + + SeenURLs.Add(item.Link); + OnNewPost(post); + Thread.Sleep(PostEventSleepTime * 1000); + } + } + catch (Exception) + { + throw; + } + } + } +} diff --git a/AnimeAnnouncer/RSS/NyaaItem.cs b/AnimeAnnouncer/RSS/NyaaItem.cs new file mode 100644 index 0000000..97f425f --- /dev/null +++ b/AnimeAnnouncer/RSS/NyaaItem.cs @@ -0,0 +1,10 @@ +namespace AnimeAnnouncer.RSS; + +public class NyaaItem : IndexerItem +{ + /// + /// The size of the release in GB + /// + public Double Size { get; set; } + +}