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;