From 2849a917365479f1ec67d4478ae4a9209a70cfb2 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sat, 18 Apr 2026 23:52:24 +0200 Subject: [PATCH] fix: complete anime at final episode --- internal/features/playback/handler.go | 77 +++++++++++++++++++++++++++ internal/server/routes.go | 1 + static/player.ts | 21 ++++++++ 3 files changed, 99 insertions(+) diff --git a/internal/features/playback/handler.go b/internal/features/playback/handler.go index bfc93db..0cf21ca 100644 --- a/internal/features/playback/handler.go +++ b/internal/features/playback/handler.go @@ -297,6 +297,83 @@ func (h *Handler) HandleSaveProgress(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } +func (h *Handler) HandleCompleteAnime(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + user := middleware.GetUser(r.Context()) + if user == nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + type completeAnimeRequest struct { + MalID int `json:"mal_id"` + Episode int `json:"episode"` + } + + var payload completeAnimeRequest + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + + if payload.MalID <= 0 || payload.Episode <= 0 { + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + + animeID := int64(payload.MalID) + + if _, err := h.svc.db.GetAnime(r.Context(), animeID); err != nil { + anime, fetchErr := h.jikanClient.GetAnimeByID(r.Context(), payload.MalID) + if fetchErr != nil { + log.Printf("complete anime failed to fetch anime user_id=%s mal_id=%d err=%v", user.ID, payload.MalID, fetchErr) + http.Error(w, "failed to mark anime completed", http.StatusInternalServerError) + return + } + + if _, upsertErr := h.svc.db.UpsertAnime(r.Context(), database.UpsertAnimeParams{ + ID: animeID, + TitleOriginal: anime.Title, + TitleEnglish: sql.NullString{String: anime.TitleEnglish, Valid: anime.TitleEnglish != ""}, + TitleJapanese: sql.NullString{String: anime.TitleJapanese, Valid: anime.TitleJapanese != ""}, + ImageUrl: anime.ImageURL(), + Airing: sql.NullBool{Bool: anime.Airing, Valid: true}, + }); upsertErr != nil { + log.Printf("complete anime failed to upsert anime user_id=%s mal_id=%d err=%v", user.ID, payload.MalID, upsertErr) + http.Error(w, "failed to mark anime completed", http.StatusInternalServerError) + return + } + } + + if _, err := h.svc.db.UpsertWatchListEntry(r.Context(), database.UpsertWatchListEntryParams{ + ID: uuid.New().String(), + UserID: user.ID, + AnimeID: animeID, + Status: "completed", + CurrentEpisode: sql.NullInt64{Int64: int64(payload.Episode), Valid: true}, + CurrentTimeSeconds: 0, + }); err != nil { + log.Printf("complete anime failed to upsert watchlist user_id=%s mal_id=%d err=%v", user.ID, payload.MalID, err) + http.Error(w, "failed to mark anime completed", http.StatusInternalServerError) + return + } + + if err := h.svc.db.DeleteContinueWatchingEntry(r.Context(), database.DeleteContinueWatchingEntryParams{ + UserID: user.ID, + AnimeID: animeID, + }); err != nil { + log.Printf("complete anime failed to delete continue entry user_id=%s mal_id=%d err=%v", user.ID, payload.MalID, err) + http.Error(w, "failed to mark anime completed", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + func (h *Handler) proxyUpstream(w http.ResponseWriter, r *http.Request, targetURL string, referer string) { parsed, err := url.Parse(targetURL) if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") { diff --git a/internal/server/routes.go b/internal/server/routes.go index ad4dce9..95e36ce 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -61,6 +61,7 @@ func NewRouter(cfg Config) http.Handler { mux.HandleFunc("/watch/proxy/segment", playbackHandler.HandleProxySegment) mux.HandleFunc("/watch/proxy/subtitle", playbackHandler.HandleProxySubtitle) mux.HandleFunc("/api/watch-progress", playbackHandler.HandleSaveProgress) + mux.HandleFunc("/api/watch-complete", playbackHandler.HandleCompleteAnime) // Auth Endpoints mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { diff --git a/static/player.ts b/static/player.ts index 3698c08..2ac9343 100644 --- a/static/player.ts +++ b/static/player.ts @@ -605,6 +605,7 @@ const initPlayer = (): void => { if (Number.isNaN(currentEpisode)) return if (Number.isInteger(totalEpisodes) && totalEpisodes > 0 && currentEpisode >= totalEpisodes) { + completeAnime(currentEpisode) return } @@ -615,6 +616,26 @@ const initPlayer = (): void => { window.location.href = nextUrl } + const completeAnime = async (episodeNumber: number): Promise => { + if (!Number.isInteger(malID) || malID <= 0) return + if (!Number.isInteger(episodeNumber) || episodeNumber <= 0) return + + try { + await fetch('/api/watch-complete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + mal_id: malID, + episode: episodeNumber, + }), + }) + } catch { + return + } + } + playPause?.addEventListener('click', () => { if (video.paused) { video.play()