diff --git a/AnimeAnnouncer.Web/Pages/Index.cshtml b/AnimeAnnouncer.Web/Pages/Index.cshtml
index 9d842c1..40aebe4 100644
--- a/AnimeAnnouncer.Web/Pages/Index.cshtml
+++ b/AnimeAnnouncer.Web/Pages/Index.cshtml
@@ -117,6 +117,30 @@
}
+ @if (show.Genres.Count > 0 || show.Tags.Count > 0)
+ {
+
+ @foreach (var genre in show.Genres)
+ {
+ @genre
+ }
+ @foreach (var tag in show.VisibleTags)
+ {
+ #@tag
+ }
+ @if (show.OverflowTags.Count > 0)
+ {
+
+ +@show.OverflowTags.Count more
+ @foreach (var tag in show.OverflowTags)
+ {
+ #@tag
+ }
+
+ }
+
+ }
+
@if (show.EpisodeProgressPercent.HasValue)
{
diff --git a/AnimeAnnouncer.Web/Pages/Index.cshtml.cs b/AnimeAnnouncer.Web/Pages/Index.cshtml.cs
index c28de69..c83773d 100644
--- a/AnimeAnnouncer.Web/Pages/Index.cshtml.cs
+++ b/AnimeAnnouncer.Web/Pages/Index.cshtml.cs
@@ -158,6 +158,42 @@ public class IndexModel : PageModel
}
}
+ // --- Genres & tags ---------------------------------------------------
+
+ /// How many tags are shown before the rest are tucked behind a "more" toggle.
+ public const int VisibleTagCount = 10;
+
+ ///
+ /// Genres from both sources, AniList first. TMDB's "Animation" genre is dropped since every
+ /// show on this page is anime, so it carries no information.
+ ///
+ public List Genres => MergeDistinct(AnilistDetails?.Genres, CachedDetails?.Genres)
+ .Where(g => !string.Equals(g, "Animation", StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ /// AniList tags followed by TMDB keywords, minus anything already listed as a genre.
+ public List Tags
+ {
+ get
+ {
+ var genres = new HashSet(Genres, StringComparer.OrdinalIgnoreCase);
+ return MergeDistinct(AnilistDetails?.Tags, CachedDetails?.Keywords)
+ .Where(t => !genres.Contains(t))
+ .ToList();
+ }
+ }
+
+ public List VisibleTags => Tags.Take(VisibleTagCount).ToList();
+ public List OverflowTags => Tags.Skip(VisibleTagCount).ToList();
+
+ private static IEnumerable MergeDistinct(params IEnumerable?[] sources) =>
+ sources
+ .Where(s => s != null)
+ .SelectMany(s => s!)
+ .Where(s => !string.IsNullOrWhiteSpace(s))
+ .Select(s => s.Trim())
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+
/// Anilist descriptions are HTML fragments; flatten them to plain text for display.
public string? AnilistDescription
{
diff --git a/AnimeAnnouncer.Web/wwwroot/css/site.css b/AnimeAnnouncer.Web/wwwroot/css/site.css
index f58973a..bb5efd7 100644
--- a/AnimeAnnouncer.Web/wwwroot/css/site.css
+++ b/AnimeAnnouncer.Web/wwwroot/css/site.css
@@ -245,6 +245,57 @@ a:hover {
background: rgba(255, 212, 59, 0.12);
}
+/* --- Genres & tags --- */
+.airing-taxonomy {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.35rem;
+}
+
+.genre-chip {
+ padding: 0.15rem 0.6rem;
+ border-radius: 999px;
+ font-size: 0.78rem;
+ font-weight: 600;
+ color: #d0bfff;
+ background: rgba(151, 117, 250, 0.18);
+ border: 1px solid rgba(151, 117, 250, 0.45);
+ backdrop-filter: blur(4px);
+}
+
+.tag-chip {
+ padding: 0.1rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.72rem;
+ color: #ced4da;
+ background: rgba(0, 0, 0, 0.35);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+/* The overflow list flows inline with the other chips once opened. */
+.tag-overflow {
+ display: contents;
+}
+
+.tag-overflow > summary {
+ list-style: none;
+ cursor: pointer;
+}
+
+.tag-overflow > summary::-webkit-details-marker {
+ display: none;
+}
+
+.tag-more {
+ color: #74c0fc;
+ border-style: dashed;
+}
+
+.tag-overflow[open] > .tag-more {
+ display: none;
+}
+
.airing-progress {
height: 6px;
border-radius: 999px;
diff --git a/AnimeAnnouncer/Anilist.cs b/AnimeAnnouncer/Anilist.cs
index 95efc8f..409afd8 100644
--- a/AnimeAnnouncer/Anilist.cs
+++ b/AnimeAnnouncer/Anilist.cs
@@ -38,6 +38,12 @@ public class Anilist
return result;
}
+ public async Task> GetMediaTags(int mediaId)
+ {
+ var results = await client.GetMediaTagsAsync(mediaId);
+ return results.Select(p => p.Name).ToList();
+ }
+
private async Task Authenticate()
{
Boolean success = await client.TryAuthenticateAsync(Token);
diff --git a/AnimeAnnouncer/Cache/AnilistCacheItem.cs b/AnimeAnnouncer/Cache/AnilistCacheItem.cs
index 6db4676..045ee5f 100644
--- a/AnimeAnnouncer/Cache/AnilistCacheItem.cs
+++ b/AnimeAnnouncer/Cache/AnilistCacheItem.cs
@@ -16,10 +16,12 @@ namespace AnimeAnnouncer.Cache
public string? BannerImageUrl { get; set; }
public string? CoverImageUrl { get; set; }
public int? AverageScore { get; set; }
+ public List Genres { get; set; }
+ public List Tags { get; set; }
public DateTime? Updated { get; set; }
- public static AnilistCacheItem FromMedia(Media media)
+ public static AnilistCacheItem FromMedia(Media media, List tags)
{
return new AnilistCacheItem
{
@@ -34,7 +36,9 @@ namespace AnimeAnnouncer.Cache
Description = media.Description,
BannerImageUrl = media.BannerImageUrl?.ToString(),
CoverImageUrl = media.Cover?.ExtraLargeImageUrl?.ToString(),
- AverageScore = media.AverageScore
+ AverageScore = media.AverageScore,
+ Genres = media.Genres.ToList(),
+ Tags = tags
};
}
}
diff --git a/AnimeAnnouncer/Cache/TMDBCacheItem.cs b/AnimeAnnouncer/Cache/TMDBCacheItem.cs
index 44be978..390f407 100644
--- a/AnimeAnnouncer/Cache/TMDBCacheItem.cs
+++ b/AnimeAnnouncer/Cache/TMDBCacheItem.cs
@@ -23,7 +23,9 @@ namespace AnimeAnnouncer.Cache
public Boolean? AnilistPopulated { get; set; }
public DateTime? Updated { get; set; }
-
+
+ public List Genres { get; set; }
+ public List Keywords { get; set; }
}
}
\ No newline at end of file
diff --git a/AnimeAnnouncer/Program.cs b/AnimeAnnouncer/Program.cs
index d20b9c6..8ad0ab5 100644
--- a/AnimeAnnouncer/Program.cs
+++ b/AnimeAnnouncer/Program.cs
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using TMDbLib.Client;
using TMDbLib.Objects.Authentication;
+using TMDbLib.Objects.TvShows;
namespace AnimeAnnouncer
{
@@ -183,7 +184,7 @@ namespace AnimeAnnouncer
//supposedShowId = searchResults.Results.First().Id;
supposedShowId = searchResults.Results.OrderBy(r => ComputeLevenshteinDistance(title, r.Name)).First().Id;
- var showResult = await tmdbClient.GetTvShowAsync(supposedShowId);
+ var showResult = await tmdbClient.GetTvShowAsync(supposedShowId, TvShowMethods.Keywords);
var latestSeason = showResult.Seasons.Where(s => s.AirDate.HasValue && s.AirDate.Value < DateTime.Now).OrderByDescending(s => s.SeasonNumber).FirstOrDefault();
@@ -234,7 +235,8 @@ namespace AnimeAnnouncer
var anilistMedia = await anilistClient.RetrieveAiringShowInformation(title);
if (anilistMedia != null)
{
- anilistCacheItem = AnilistCacheItem.FromMedia(anilistMedia);
+ var tags = await anilistClient.GetMediaTags(anilistMedia.Id);
+ anilistCacheItem = AnilistCacheItem.FromMedia(anilistMedia, tags);
anilistCacheItem.Updated = DateTime.Now;
_ = tmdbCache.SetCacheItem($"AnilistCache-{title}", anilistCacheItem);
}
@@ -290,7 +292,9 @@ namespace AnimeAnnouncer
LastAirDate = lastAirDate,
LatestEpisodeNumber = latestEpisodeNumber,
AnilistPopulated = anilistPosterPath != null,
- Updated = DateTime.Now
+ Updated = DateTime.Now,
+ Keywords = showResult.Keywords != null ? showResult.Keywords.Results.Select(p => p.Name).ToList() : new List(),
+ Genres = showResult.Genres != null ? showResult.Genres.Select(p => p.Name).ToList() : new List(),
};
_ = tmdbCache.SetCacheItem($"ShowCache-{title}", cachedShow);
Console.WriteLine($"{title} Added to cache");
@@ -396,7 +400,7 @@ namespace AnimeAnnouncer
try
{
_ = tmdbCache.SetPair($"ShowAnnounced-{supposedShowId}", latestSeasonNumber.ToString());
- PostStatus(cachedShow);
+ _ = PostStatus(cachedShow);
}
catch(Exception ex)
{
@@ -469,7 +473,7 @@ namespace AnimeAnnouncer
*/
}
- private static async void PostStatus(TMDBCacheItem cachedShow)
+ private static async Task PostStatus(TMDBCacheItem cachedShow)
{
//get poster
String? posterUrl = cachedShow.LatestSeasonPosterPath ?? cachedShow.PosterPath;
@@ -521,7 +525,7 @@ namespace AnimeAnnouncer
}
else
{
- if(existingAiringItem.LatestEpisodeNumber >= cachedShow.LatestEpisodeNumber)
+ if(existingAiringItem.LatestEpisodeNumber >= cachedShow.LatestEpisodeNumber && existingAiringItem.LastAirDate == airingSoonItem.LastAirDate)
{
Console.WriteLine($"Not updating {cachedShow.Title} airdate due to episode number not incrementing");
return;