diff --git a/internal/database/queries.sql b/internal/database/queries.sql index eb6633b..7490c2d 100644 --- a/internal/database/queries.sql +++ b/internal/database/queries.sql @@ -89,25 +89,60 @@ ON CONFLICT (anime_id, related_anime_id) DO UPDATE SET UPDATE anime SET status = ?, relations_synced_at = CURRENT_TIMESTAMP WHERE id = ?; -- name: GetAnimeNeedingRelationSync :many -SELECT a.id, a.title_original -FROM watch_list_entry w -JOIN anime a ON w.anime_id = a.id -WHERE w.status IN ('completed', 'watching') - AND (a.relations_synced_at IS NULL OR a.relations_synced_at < datetime('now', '-7 days')) -GROUP BY a.id -ORDER BY MAX(w.updated_at) DESC +WITH RECURSIVE sequel_chain AS ( + SELECT a.id, a.title_original, a.relations_synced_at, w.updated_at as base_updated_at, 0 as depth + FROM watch_list_entry w + JOIN anime a ON w.anime_id = a.id + WHERE w.status IN ('completed', 'watching') + + UNION + + SELECT a.id, a.title_original, a.relations_synced_at, sc.base_updated_at, sc.depth + 1 + FROM sequel_chain sc + JOIN anime_relation r ON sc.id = r.anime_id AND r.relation_type = 'Sequel' + JOIN anime a ON r.related_anime_id = a.id + WHERE sc.depth < 10 +) +SELECT id, title_original +FROM sequel_chain +WHERE relations_synced_at IS NULL OR relations_synced_at < datetime('now', '-7 days') +GROUP BY id, title_original +ORDER BY MAX(base_updated_at) DESC, MIN(depth) ASC LIMIT 50; -- name: GetUpcomingSeasons :many +WITH RECURSIVE sequel_chain AS ( + SELECT + w.anime_id as root_id, + a.title_original as root_title, + r.related_anime_id as current_id, + 1 as depth + FROM watch_list_entry w + JOIN anime a ON w.anime_id = a.id + JOIN anime_relation r ON w.anime_id = r.anime_id + WHERE w.user_id = sqlc.arg('user_id') + AND w.status IN ('completed', 'watching') + AND r.relation_type = 'Sequel' + + UNION + + SELECT + sc.root_id, + sc.root_title, + r.related_anime_id, + sc.depth + 1 + FROM sequel_chain sc + JOIN anime_relation r ON sc.current_id = r.anime_id + WHERE r.relation_type = 'Sequel' AND sc.depth < 10 +) SELECT DISTINCT related.*, - a.title_original AS prequel_title -FROM watch_list_entry w -JOIN anime_relation r ON w.anime_id = r.anime_id -JOIN anime a ON w.anime_id = a.id -JOIN anime related ON r.related_anime_id = related.id -WHERE w.user_id = ? - AND w.status IN ('completed', 'watching') - AND r.relation_type = 'Sequel' - AND related.status IN ('Not yet aired', 'Currently Airing') + sc.root_title AS prequel_title +FROM sequel_chain sc +JOIN anime related ON sc.current_id = related.id +WHERE related.status IN ('Not yet aired', 'Currently Airing') + AND NOT EXISTS ( + SELECT 1 FROM watch_list_entry we + WHERE we.user_id = sqlc.arg('user_id') AND we.anime_id = related.id + ) ORDER BY related.id DESC; diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 5401333..c9b143f 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -114,13 +114,25 @@ func (q *Queries) GetAnime(ctx context.Context, id int64) (Anime, error) { } const getAnimeNeedingRelationSync = `-- name: GetAnimeNeedingRelationSync :many -SELECT a.id, a.title_original -FROM watch_list_entry w -JOIN anime a ON w.anime_id = a.id -WHERE w.status IN ('completed', 'watching') - AND (a.relations_synced_at IS NULL OR a.relations_synced_at < datetime('now', '-7 days')) -GROUP BY a.id -ORDER BY MAX(w.updated_at) DESC +WITH RECURSIVE sequel_chain AS ( + SELECT a.id, a.title_original, a.relations_synced_at, w.updated_at as base_updated_at, 0 as depth + FROM watch_list_entry w + JOIN anime a ON w.anime_id = a.id + WHERE w.status IN ('completed', 'watching') + + UNION + + SELECT a.id, a.title_original, a.relations_synced_at, sc.base_updated_at, sc.depth + 1 + FROM sequel_chain sc + JOIN anime_relation r ON sc.id = r.anime_id AND r.relation_type = 'Sequel' + JOIN anime a ON r.related_anime_id = a.id + WHERE sc.depth < 10 +) +SELECT id, title_original +FROM sequel_chain +WHERE relations_synced_at IS NULL OR relations_synced_at < datetime('now', '-7 days') +GROUP BY id, title_original +ORDER BY MAX(base_updated_at) DESC, MIN(depth) ASC LIMIT 50 ` @@ -169,17 +181,40 @@ func (q *Queries) GetSession(ctx context.Context, id string) (Session, error) { } const getUpcomingSeasons = `-- name: GetUpcomingSeasons :many +WITH RECURSIVE sequel_chain AS ( + SELECT + w.anime_id as root_id, + a.title_original as root_title, + r.related_anime_id as current_id, + 1 as depth + FROM watch_list_entry w + JOIN anime a ON w.anime_id = a.id + JOIN anime_relation r ON w.anime_id = r.anime_id + WHERE w.user_id = ?1 + AND w.status IN ('completed', 'watching') + AND r.relation_type = 'Sequel' + + UNION + + SELECT + sc.root_id, + sc.root_title, + r.related_anime_id, + sc.depth + 1 + FROM sequel_chain sc + JOIN anime_relation r ON sc.current_id = r.anime_id + WHERE r.relation_type = 'Sequel' AND sc.depth < 10 +) SELECT DISTINCT related.id, related.title_original, related.image_url, related.created_at, related.title_english, related.title_japanese, related.airing, related.status, related.relations_synced_at, - a.title_original AS prequel_title -FROM watch_list_entry w -JOIN anime_relation r ON w.anime_id = r.anime_id -JOIN anime a ON w.anime_id = a.id -JOIN anime related ON r.related_anime_id = related.id -WHERE w.user_id = ? - AND w.status IN ('completed', 'watching') - AND r.relation_type = 'Sequel' - AND related.status IN ('Not yet aired', 'Currently Airing') + sc.root_title AS prequel_title +FROM sequel_chain sc +JOIN anime related ON sc.current_id = related.id +WHERE related.status IN ('Not yet aired', 'Currently Airing') + AND NOT EXISTS ( + SELECT 1 FROM watch_list_entry we + WHERE we.user_id = sqlc.arg('user_id') AND we.anime_id = related.id + ) ORDER BY related.id DESC ` diff --git a/internal/features/anime/handler.go b/internal/features/anime/handler.go index 9af903c..a494c6d 100644 --- a/internal/features/anime/handler.go +++ b/internal/features/anime/handler.go @@ -328,6 +328,20 @@ func (h *Handler) HandleNotifications(w http.ResponseWriter, r *http.Request) { return } + templates.Notifications(watching).Render(r.Context(), w) +} + +func (h *Handler) HandleNotificationsUpcoming(w http.ResponseWriter, r *http.Request) { + userID := "" + if user, ok := r.Context().Value(middleware.UserContextKey).(*database.User); ok && user != nil { + userID = user.ID + } + + if userID == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + upcomingSeasons, err := h.svc.GetUpcomingSeasons(r.Context(), userID) if err != nil { log.Printf("upcoming seasons error: %v", err) @@ -335,5 +349,5 @@ func (h *Handler) HandleNotifications(w http.ResponseWriter, r *http.Request) { return } - templates.Notifications(watching, upcomingSeasons).Render(r.Context(), w) + templates.UpcomingSeasonsList(upcomingSeasons).Render(r.Context(), w) } diff --git a/internal/server/routes.go b/internal/server/routes.go index 2265e4b..15e8ebb 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -37,6 +37,7 @@ func NewRouter(cfg Config) http.Handler { mux.HandleFunc("/discover", animeHandler.HandleDiscover) mux.HandleFunc("/schedule", animeHandler.HandleSchedule) mux.HandleFunc("/notifications", animeHandler.HandleNotifications) + mux.HandleFunc("/notifications/upcoming", animeHandler.HandleNotificationsUpcoming) mux.HandleFunc("/api/schedule", animeHandler.HandleAPISchedule) mux.HandleFunc("/api/discover/airing", animeHandler.HandleAPIDiscoverAiring) mux.HandleFunc("/api/discover/upcoming", animeHandler.HandleAPIDiscoverUpcoming) diff --git a/internal/templates/notifications.templ b/internal/templates/notifications.templ index 601b913..d5d485f 100644 --- a/internal/templates/notifications.templ +++ b/internal/templates/notifications.templ @@ -9,7 +9,7 @@ type WatchingAnimeWithDetails struct { Anime jikan.Anime } -templ Notifications(watching []WatchingAnimeWithDetails, upcomingSeasons []database.GetUpcomingSeasonsRow) { +templ Notifications(watching []WatchingAnimeWithDetails) { @Layout("mal - notifications") {
because you've watched prequels
- if len(upcomingSeasons) == 0 { -no upcoming seasons for anime you've watched
-as you watch more shows, new seasons will appear here
-no upcoming seasons for anime you've watched
+as you watch more shows, new seasons will appear here
+because you've watched prequels
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if len(upcomingSeasons) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "no upcoming seasons for anime you've watched
as you watch more shows, new seasons will appear here
because you've watched prequels
no upcoming seasons for anime you've watched
as you watch more shows, new seasons will appear here