Compare commits

3 Commits

4 changed files with 207 additions and 18 deletions

View File

@@ -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; }
}
}

View File

@@ -33,6 +33,25 @@ public class TMDBCache
else
return null;
}
public async Task SetAiringSoon(List<AiringSoonItem> list)
{
var db = redis.GetDatabase();
String jsonItem = JsonSerializer.Serialize(list);
await db.StringSetAsync("AiringSoon", jsonItem, CacheExpiration);
}
public async Task<List<AiringSoonItem>> GetAiringList()
{
var db = redis.GetDatabase();
String jsonItem = await db.StringGetAsync("AiringSoon");
if(!String.IsNullOrEmpty(jsonItem))
return JsonSerializer.Deserialize<List<AiringSoonItem>>(jsonItem);
else
return null;
}
public async Task<bool> KeyExists(String key)
{
var db = redis.GetDatabase();

View File

@@ -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; }
}
}

View File

@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Runtime.ExceptionServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using AnimeAnnouncer.Cache;
@@ -18,6 +19,8 @@ namespace AnimeAnnouncer
private static TMDbClient tmdbClient;
private static TMDBCache tmdbCache;
private static MastodonClient mastodonClient;
private static List<AiringSoonItem> airingSoonList;
static void Main(string[] args)
{
Console.WriteLine("Starting...");
@@ -52,6 +55,11 @@ namespace AnimeAnnouncer
mastodonClient = new MastodonClient(mastodonInstance, mastodonToken);
}
airingSoonList = tmdbCache.GetAiringList().Result;
if(airingSoonList == null)
airingSoonList = new List<AiringSoonItem>();
nyaaIndexer.NewPost += OnNewPost;
nyaaIndexer.RssReadFinished += OnRssReadFinished;
nyaaIndexer.Start(true);
@@ -74,6 +82,14 @@ namespace AnimeAnnouncer
static void OnRssReadFinished()
{
if(DateTime.Today.Day == 1)
{
if(!tmdbCache.KeyExists($"UpcomingAnnounced-{DateTime.Today.Year}-{DateTime.Today.Month}").Result)
{
CreateAiringMonthPost().Wait();
tmdbCache.SetPair($"UpcomingAnnounced-{DateTime.Today.Year}-{DateTime.Today.Month}", "1").Wait();
}
}
}
@@ -93,10 +109,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 +126,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 +150,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 +165,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 +193,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 +215,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 +236,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 +252,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 +282,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,16 +303,19 @@ 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");
_ = tmdbCache.SetPair($"ShowAnnounced-{supposedShowId}", latestSeasonNumber.ToString());
}
if(mastodonClient != null && cachedShow != null)
{
try
{
PostStatus(cachedShow);
}
catch(Exception ex)
{
@@ -276,6 +326,46 @@ namespace AnimeAnnouncer
Console.WriteLine($"Choosing {title} as a finished season!");
}
private static async Task CreateAiringMonthPost()
{
var airingSoon = await tmdbCache.GetAiringList();
DateTime beginningOfMonth = DateTime.Today;
DateTime endOfMonth = DateTime.Today.AddMonths(1);
var airingThisMonth = airingSoon.Where(p => p.LastAirDate >= beginningOfMonth && p.LastAirDate <= endOfMonth).ToList();
String statusText = $"DUBBED anime airing this month (probably){Environment.NewLine + Environment.NewLine}";
int cnt = 1;
for(int i = 0;i < airingThisMonth.Count;i++, cnt++)
{
var airing = airingThisMonth[i];
if(!await tmdbCache.KeyExists($"ShowAnnounced-{airing.ShowID}"))
{ //TODO: fix for seasons
statusText += $"{cnt}. {airing.Title} on {airing.LastAirDate:MM/dd/yyyy}{Environment.NewLine}";
}
}
int neededPostCount = (int)Math.Ceiling((decimal)statusText.Length / 500);
if(neededPostCount >= 2)
{
statusText = statusText[..497] + "...";
}
_ = mastodonClient.PublishStatus(statusText,
Visibility.Public, null);
/*
else
{
for(cnt = 1; cnt < neededPostCount; cnt++)
{
//go back and find a line break
String postPart = "";
//_ = mastodonClient.PublishStatus(${statusText},
// Visibility.Public, null);
System.Threading.Thread.Sleep(1000);
}
}
*/
}
private static async void PostStatus(TMDBCacheItem cachedShow)
{
//get poster
@@ -308,5 +398,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];
}
}
}