diff --git a/AnimeAnnouncer/Cache/AiringSoonItem.cs b/AnimeAnnouncer/Cache/AiringSoonItem.cs new file mode 100644 index 0000000..2a7ac9c --- /dev/null +++ b/AnimeAnnouncer/Cache/AiringSoonItem.cs @@ -0,0 +1,10 @@ + +namespace AnimeAnnouncer.Cache +{ + public class AiringSoonItem + { + public required String Title { get; set; } + public int ShowID { get; set; } + public DateTime? LastAirDate { get; set; } + } +} \ No newline at end of file diff --git a/AnimeAnnouncer/Cache/TMDBCache.cs b/AnimeAnnouncer/Cache/TMDBCache.cs index 8e62cdd..18dcf3b 100644 --- a/AnimeAnnouncer/Cache/TMDBCache.cs +++ b/AnimeAnnouncer/Cache/TMDBCache.cs @@ -33,6 +33,25 @@ public class TMDBCache else return null; } + + public async Task SetAiringSoon(List list) + { + var db = redis.GetDatabase(); + String jsonItem = JsonSerializer.Serialize(list); + await db.StringSetAsync("AiringSoon", jsonItem, CacheExpiration); + } + + + public async Task> GetAiringList() + { + var db = redis.GetDatabase(); + String jsonItem = await db.StringGetAsync("AiringSoon"); + + if(!String.IsNullOrEmpty(jsonItem)) + return JsonSerializer.Deserialize>(jsonItem); + else + return null; + } public async Task KeyExists(String key) { var db = redis.GetDatabase(); diff --git a/AnimeAnnouncer/Cache/TMDBCacheItem.cs b/AnimeAnnouncer/Cache/TMDBCacheItem.cs index 251548b..c1a5a05 100644 --- a/AnimeAnnouncer/Cache/TMDBCacheItem.cs +++ b/AnimeAnnouncer/Cache/TMDBCacheItem.cs @@ -15,5 +15,9 @@ namespace AnimeAnnouncer.Cache public double VoteAverage { get; set; } public string? Overview { get; set; } public int? LatestOrdinalEpisodeNumber { get; set; } + + public DateTime? LastAirDate { get; set; } + //This is the latest episode number to air, NOT the last episode in the series + public int? LatestEpisodeNumber { get; set; } } } \ No newline at end of file diff --git a/AnimeAnnouncer/Program.cs b/AnimeAnnouncer/Program.cs index 9d62fc0..dbe6316 100644 --- a/AnimeAnnouncer/Program.cs +++ b/AnimeAnnouncer/Program.cs @@ -18,6 +18,8 @@ namespace AnimeAnnouncer private static TMDbClient tmdbClient; private static TMDBCache tmdbCache; private static MastodonClient mastodonClient; + + private static List airingSoonList; static void Main(string[] args) { Console.WriteLine("Starting..."); @@ -52,6 +54,11 @@ namespace AnimeAnnouncer mastodonClient = new MastodonClient(mastodonInstance, mastodonToken); } + airingSoonList = tmdbCache.GetAiringList().Result; + + if(airingSoonList == null) + airingSoonList = new List(); + nyaaIndexer.NewPost += OnNewPost; nyaaIndexer.RssReadFinished += OnRssReadFinished; nyaaIndexer.Start(true); @@ -93,10 +100,14 @@ namespace AnimeAnnouncer return; } - int latestSeasonNumber = 0, latestEpisodeNumber = 0, supposedShowId = 0; + int latestSeasonNumber = 0, lastEpisodeNumber = 0, latestEpisodeNumber = 0, supposedShowId = 0; int? latestOrdinalEpisodeNumber = null; + bool finaleConfirmed = false; + + DateTime? lastAirDate = null; + TMDBCacheItem? cachedShow = null; try @@ -106,8 +117,9 @@ namespace AnimeAnnouncer { Console.WriteLine("Cache hit"); latestSeasonNumber = cachedShow.LatestSeasonNumber; - latestEpisodeNumber = cachedShow.LastEpisodeNumber; + lastEpisodeNumber = cachedShow.LastEpisodeNumber; latestOrdinalEpisodeNumber = cachedShow.LatestOrdinalEpisodeNumber; + lastAirDate = cachedShow.LastAirDate; supposedShowId = cachedShow.ShowID; } } @@ -129,7 +141,8 @@ namespace AnimeAnnouncer return; } - supposedShowId = searchResults.Results.First().Id; + //supposedShowId = searchResults.Results.First().Id; + supposedShowId = searchResults.Results.OrderBy(r => ComputeLevenshteinDistance(title, r.Name)).First().Id; var showResult = await tmdbClient.GetTvShowAsync(supposedShowId); @@ -143,20 +156,27 @@ namespace AnimeAnnouncer latestSeasonNumber = latestSeason.SeasonNumber; - latestEpisodeNumber = latestSeason.EpisodeCount; + lastEpisodeNumber = latestSeason.EpisodeCount; + + latestEpisodeNumber = int.Parse(episode); //if nearing the end of a season, confirm it's marked with season finale - if(int.Parse(episode) >= (latestEpisodeNumber - 2)) + if(latestEpisodeNumber >= (lastEpisodeNumber - 2) || !HasAiringEndDate(supposedShowId)) { + Console.WriteLine("Querying episode information"); var latestSeasonDetail = await tmdbClient.GetTvSeasonAsync(supposedShowId, latestSeasonNumber); var seasonFinale = latestSeasonDetail.Episodes.LastOrDefault(e => e.EpisodeType.Equals("finale", StringComparison.InvariantCultureIgnoreCase)); - if(seasonFinale != null && seasonFinale.EpisodeNumber != latestEpisodeNumber) + if(seasonFinale != null) { - Console.WriteLine($"Overriding previous finale choice of {latestEpisodeNumber} due to season detail response where it's {seasonFinale.EpisodeNumber}"); - latestEpisodeNumber = seasonFinale.EpisodeNumber; + if(seasonFinale.EpisodeNumber != lastEpisodeNumber) + { + Console.WriteLine($"Overriding previous finale choice of {lastEpisodeNumber} due to season detail response where it's {seasonFinale.EpisodeNumber}"); + lastEpisodeNumber = seasonFinale.EpisodeNumber; + } + finaleConfirmed = true; } } @@ -164,6 +184,11 @@ namespace AnimeAnnouncer { latestOrdinalEpisodeNumber = showResult.Seasons.Where(s => s.SeasonNumber != 0).Sum(s => s.EpisodeCount); } + //try to figure out Last Air Date based on this new airing episode + if(lastEpisodeNumber > latestEpisodeNumber) + { + lastAirDate = DateTime.Today.AddDays(7 * (lastEpisodeNumber - latestEpisodeNumber)); + } if(tmdbCache != null && searchResults != null) { @@ -181,10 +206,13 @@ namespace AnimeAnnouncer VoteAverage = showResult.VoteAverage, ShowID = supposedShowId, LatestSeasonNumber = latestSeasonNumber, - LastEpisodeNumber = latestEpisodeNumber + LastEpisodeNumber = lastEpisodeNumber, + LastAirDate = lastAirDate, + LatestEpisodeNumber = latestEpisodeNumber }; _ = tmdbCache.SetCacheItem($"ShowCache-{title}", cachedShow); Console.WriteLine($"{title} Added to cache"); + UpdateAiringShowList(cachedShow); } catch(Exception ex) { @@ -199,7 +227,7 @@ namespace AnimeAnnouncer { var titleSeason = int.Parse(season); bool seasonOverride = false; - if(latestSeasonNumber ==1 && titleSeason > 1 && latestEpisodeNumber > 3) + if(latestSeasonNumber ==1 && titleSeason > 1 && lastEpisodeNumber > 3) { //Check episode groups to see if this is a single-season default show var showResult = await tmdbClient.GetTvShowAsync(supposedShowId, TMDbLib.Objects.TvShows.TvShowMethods.EpisodeGroups); var seasonEpisodeGroup = showResult.EpisodeGroups.Results.FirstOrDefault(eg => eg.Name == "Seasons"); @@ -215,11 +243,21 @@ namespace AnimeAnnouncer //Confirm it's aired within the last week if(targetFinale != null && targetEpisodeGroup.Groups.OrderByDescending(g => g.Order).FirstOrDefault() == targetSeason) { //confirm the episode is marked as finale and that this season is the latest one in the episode group - Console.WriteLine($"!!Choosing S{titleSeason}E{targetFinale.Order + 1} for finale using episode groups"); + Console.WriteLine($"!!Choosing S{titleSeason}E{targetFinale.Order + 1} airing on {targetFinale.AirDate} for finale using episode groups"); cachedShow.LatestSeasonNumber = latestSeasonNumber = titleSeason; - cachedShow.LastEpisodeNumber = latestEpisodeNumber = targetFinale.Order + 1; - seasonOverride = true; + cachedShow.LastEpisodeNumber = lastEpisodeNumber = targetFinale.Order + 1; + if(cachedShow.LastEpisodeNumber > cachedShow.LatestEpisodeNumber) + { + cachedShow.LastAirDate = DateTime.Today.AddDays(7 * (lastEpisodeNumber - latestEpisodeNumber)); + } + else + { + cachedShow.LastAirDate = lastAirDate = targetFinale.AirDate; + } + + seasonOverride = finaleConfirmed = true; _ = tmdbCache.SetCacheItem($"ShowCache-{title}", cachedShow); + UpdateAiringShowList(cachedShow); } else Console.WriteLine("Could not find a finale episode."); @@ -235,16 +273,16 @@ namespace AnimeAnnouncer } } - if(latestEpisodeNumber != int.Parse(episode)) + if(lastEpisodeNumber != int.Parse(episode)) { if(!latestOrdinalEpisodeNumber.HasValue || (latestOrdinalEpisodeNumber.HasValue && latestOrdinalEpisodeNumber.Value != int.Parse(episode))) { - Console.WriteLine($"Failing release due to TMDB's last episode number of {latestEpisodeNumber}|{latestOrdinalEpisodeNumber ?? 0} not matching title episode number {episode}"); + Console.WriteLine($"Failing release due to TMDB's last episode number of {lastEpisodeNumber}|{latestOrdinalEpisodeNumber ?? 0} not matching title episode number {episode}"); return; } } //Sometimes TMDB metadata hasn't been filled in yet - if(latestEpisodeNumber < 3) + if(lastEpisodeNumber < 3) { Console.WriteLine("Skipping announcement due to a low episode number"); return; @@ -256,6 +294,10 @@ namespace AnimeAnnouncer Console.WriteLine($"{title} has been previously announced, so avoiding announcing it again."); return; } + else if (!finaleConfirmed) + { + Console.WriteLine($"Would have announced finale for {title}, on S{cachedShow.LatestSeasonNumber}E{cachedShow.LastEpisodeNumber} but the finale episode type was not found."); + } else { _ = tmdbCache.SetPair($"ShowAnnounced-{supposedShowId}", "1"); @@ -308,5 +350,71 @@ namespace AnimeAnnouncer Visibility.Public, mediaIds: attachment != null ? new String[] { attachment.Id } : null); } + private static async void UpdateAiringShowList(TMDBCacheItem cachedShow) + { + var airingSoonItem = new AiringSoonItem() + { + Title = cachedShow.Title, + ShowID = cachedShow.ShowID, + LastAirDate = cachedShow.LastAirDate ?? DateTime.MaxValue + }; + if(!airingSoonList.Any(s => s.ShowID == cachedShow.ShowID)) + { + airingSoonList.Add(airingSoonItem); + } + else + { + airingSoonList.RemoveAll(s => s.ShowID == cachedShow.ShowID); + airingSoonList.Add(airingSoonItem); + } + airingSoonList.Sort((showOne, showTwo) => showOne.LastAirDate.Value.CompareTo(showTwo.LastAirDate.Value)); + _ = tmdbCache.SetAiringSoon(airingSoonList); + Console.WriteLine("Updated airing soon list"); + } + private static bool HasAiringEndDate(int showID) + { + return airingSoonList.Any(s => s.ShowID == showID && s.LastAirDate.HasValue); + } + private static int ComputeLevenshteinDistance(string s, string t) + { + int n = s.Length; + int m = t.Length; + int[,] d = new int[n + 1, m + 1]; + + // Verify arguments. + if (n == 0) + { + return m; + } + + if (m == 0) + { + return n; + } + + // Initialize arrays. + for (int i = 0; i <= n; d[i, 0] = i++) + { + } + + for (int j = 0; j <= m; d[0, j] = j++) + { + } + + // Begin looping. + for (int i = 1; i <= n; i++) + { + for (int j = 1; j <= m; j++) + { + // Compute cost. + int cost = (t[j - 1] == s[i - 1]) ? 0 : 1; + d[i, j] = Math.Min( + Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), + d[i - 1, j - 1] + cost); + } + } + // Return cost. + return d[n, m]; + } } }