Added Genre and Tags
This commit is contained in:
@@ -117,6 +117,30 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (show.Genres.Count > 0 || show.Tags.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="airing-taxonomy">
|
||||||
|
@foreach (var genre in show.Genres)
|
||||||
|
{
|
||||||
|
<span class="genre-chip">@genre</span>
|
||||||
|
}
|
||||||
|
@foreach (var tag in show.VisibleTags)
|
||||||
|
{
|
||||||
|
<span class="tag-chip">#@tag</span>
|
||||||
|
}
|
||||||
|
@if (show.OverflowTags.Count > 0)
|
||||||
|
{
|
||||||
|
<details class="tag-overflow">
|
||||||
|
<summary class="tag-chip tag-more">+@show.OverflowTags.Count more</summary>
|
||||||
|
@foreach (var tag in show.OverflowTags)
|
||||||
|
{
|
||||||
|
<span class="tag-chip">#@tag</span>
|
||||||
|
}
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (show.EpisodeProgressPercent.HasValue)
|
@if (show.EpisodeProgressPercent.HasValue)
|
||||||
{
|
{
|
||||||
<div class="airing-progress" title="@show.LatestEpisodeNumber of @show.TotalEpisodes episodes aired">
|
<div class="airing-progress" title="@show.LatestEpisodeNumber of @show.TotalEpisodes episodes aired">
|
||||||
|
|||||||
@@ -158,6 +158,42 @@ public class IndexModel : PageModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Genres & tags ---------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>How many tags are shown before the rest are tucked behind a "more" toggle.</summary>
|
||||||
|
public const int VisibleTagCount = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Genres => MergeDistinct(AnilistDetails?.Genres, CachedDetails?.Genres)
|
||||||
|
.Where(g => !string.Equals(g, "Animation", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
/// <summary>AniList tags followed by TMDB keywords, minus anything already listed as a genre.</summary>
|
||||||
|
public List<string> Tags
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var genres = new HashSet<string>(Genres, StringComparer.OrdinalIgnoreCase);
|
||||||
|
return MergeDistinct(AnilistDetails?.Tags, CachedDetails?.Keywords)
|
||||||
|
.Where(t => !genres.Contains(t))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> VisibleTags => Tags.Take(VisibleTagCount).ToList();
|
||||||
|
public List<string> OverflowTags => Tags.Skip(VisibleTagCount).ToList();
|
||||||
|
|
||||||
|
private static IEnumerable<string> MergeDistinct(params IEnumerable<string>?[] sources) =>
|
||||||
|
sources
|
||||||
|
.Where(s => s != null)
|
||||||
|
.SelectMany(s => s!)
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||||
|
.Select(s => s.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
/// <summary>Anilist descriptions are HTML fragments; flatten them to plain text for display.</summary>
|
/// <summary>Anilist descriptions are HTML fragments; flatten them to plain text for display.</summary>
|
||||||
public string? AnilistDescription
|
public string? AnilistDescription
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -245,6 +245,57 @@ a:hover {
|
|||||||
background: rgba(255, 212, 59, 0.12);
|
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 {
|
.airing-progress {
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ public class Anilist
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<String>> GetMediaTags(int mediaId)
|
||||||
|
{
|
||||||
|
var results = await client.GetMediaTagsAsync(mediaId);
|
||||||
|
return results.Select(p => p.Name).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task Authenticate()
|
private async Task Authenticate()
|
||||||
{
|
{
|
||||||
Boolean success = await client.TryAuthenticateAsync(Token);
|
Boolean success = await client.TryAuthenticateAsync(Token);
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ namespace AnimeAnnouncer.Cache
|
|||||||
public string? BannerImageUrl { get; set; }
|
public string? BannerImageUrl { get; set; }
|
||||||
public string? CoverImageUrl { get; set; }
|
public string? CoverImageUrl { get; set; }
|
||||||
public int? AverageScore { get; set; }
|
public int? AverageScore { get; set; }
|
||||||
|
public List<String> Genres { get; set; }
|
||||||
|
public List<String> Tags { get; set; }
|
||||||
|
|
||||||
public DateTime? Updated { get; set; }
|
public DateTime? Updated { get; set; }
|
||||||
|
|
||||||
public static AnilistCacheItem FromMedia(Media media)
|
public static AnilistCacheItem FromMedia(Media media, List<String> tags)
|
||||||
{
|
{
|
||||||
return new AnilistCacheItem
|
return new AnilistCacheItem
|
||||||
{
|
{
|
||||||
@@ -34,7 +36,9 @@ namespace AnimeAnnouncer.Cache
|
|||||||
Description = media.Description,
|
Description = media.Description,
|
||||||
BannerImageUrl = media.BannerImageUrl?.ToString(),
|
BannerImageUrl = media.BannerImageUrl?.ToString(),
|
||||||
CoverImageUrl = media.Cover?.ExtraLargeImageUrl?.ToString(),
|
CoverImageUrl = media.Cover?.ExtraLargeImageUrl?.ToString(),
|
||||||
AverageScore = media.AverageScore
|
AverageScore = media.AverageScore,
|
||||||
|
Genres = media.Genres.ToList(),
|
||||||
|
Tags = tags
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ namespace AnimeAnnouncer.Cache
|
|||||||
public Boolean? AnilistPopulated { get; set; }
|
public Boolean? AnilistPopulated { get; set; }
|
||||||
|
|
||||||
public DateTime? Updated { get; set; }
|
public DateTime? Updated { get; set; }
|
||||||
|
|
||||||
|
public List<String> Genres { get; set; }
|
||||||
|
public List<String> Keywords { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TMDbLib.Client;
|
using TMDbLib.Client;
|
||||||
using TMDbLib.Objects.Authentication;
|
using TMDbLib.Objects.Authentication;
|
||||||
|
using TMDbLib.Objects.TvShows;
|
||||||
|
|
||||||
namespace AnimeAnnouncer
|
namespace AnimeAnnouncer
|
||||||
{
|
{
|
||||||
@@ -183,7 +184,7 @@ namespace AnimeAnnouncer
|
|||||||
//supposedShowId = searchResults.Results.First().Id;
|
//supposedShowId = searchResults.Results.First().Id;
|
||||||
supposedShowId = searchResults.Results.OrderBy(r => ComputeLevenshteinDistance(title, r.Name)).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();
|
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);
|
var anilistMedia = await anilistClient.RetrieveAiringShowInformation(title);
|
||||||
if (anilistMedia != null)
|
if (anilistMedia != null)
|
||||||
{
|
{
|
||||||
anilistCacheItem = AnilistCacheItem.FromMedia(anilistMedia);
|
var tags = await anilistClient.GetMediaTags(anilistMedia.Id);
|
||||||
|
anilistCacheItem = AnilistCacheItem.FromMedia(anilistMedia, tags);
|
||||||
anilistCacheItem.Updated = DateTime.Now;
|
anilistCacheItem.Updated = DateTime.Now;
|
||||||
_ = tmdbCache.SetCacheItem($"AnilistCache-{title}", anilistCacheItem);
|
_ = tmdbCache.SetCacheItem($"AnilistCache-{title}", anilistCacheItem);
|
||||||
}
|
}
|
||||||
@@ -290,7 +292,9 @@ namespace AnimeAnnouncer
|
|||||||
LastAirDate = lastAirDate,
|
LastAirDate = lastAirDate,
|
||||||
LatestEpisodeNumber = latestEpisodeNumber,
|
LatestEpisodeNumber = latestEpisodeNumber,
|
||||||
AnilistPopulated = anilistPosterPath != null,
|
AnilistPopulated = anilistPosterPath != null,
|
||||||
Updated = DateTime.Now
|
Updated = DateTime.Now,
|
||||||
|
Keywords = showResult.Keywords != null ? showResult.Keywords.Results.Select(p => p.Name).ToList() : new List<string>(),
|
||||||
|
Genres = showResult.Genres != null ? showResult.Genres.Select(p => p.Name).ToList() : new List<string>(),
|
||||||
};
|
};
|
||||||
_ = tmdbCache.SetCacheItem($"ShowCache-{title}", cachedShow);
|
_ = tmdbCache.SetCacheItem($"ShowCache-{title}", cachedShow);
|
||||||
Console.WriteLine($"{title} Added to cache");
|
Console.WriteLine($"{title} Added to cache");
|
||||||
@@ -396,7 +400,7 @@ namespace AnimeAnnouncer
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ = tmdbCache.SetPair($"ShowAnnounced-{supposedShowId}", latestSeasonNumber.ToString());
|
_ = tmdbCache.SetPair($"ShowAnnounced-{supposedShowId}", latestSeasonNumber.ToString());
|
||||||
PostStatus(cachedShow);
|
_ = PostStatus(cachedShow);
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
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
|
//get poster
|
||||||
String? posterUrl = cachedShow.LatestSeasonPosterPath ?? cachedShow.PosterPath;
|
String? posterUrl = cachedShow.LatestSeasonPosterPath ?? cachedShow.PosterPath;
|
||||||
@@ -521,7 +525,7 @@ namespace AnimeAnnouncer
|
|||||||
}
|
}
|
||||||
else
|
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");
|
Console.WriteLine($"Not updating {cachedShow.Title} airdate due to episode number not incrementing");
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user