From 345853406c8f68874170141fb07261965907d694 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Wed, 13 May 2026 11:20:49 +0200 Subject: [PATCH] refactor: general architectural cleanup and bug fixes --- cmd/server/main.go | 1 - integrations/jikan/client.go | 4 +- integrations/jikan/module.go | 2 - internal/anime/handler/handler.go | 165 ++++++++++++++---- internal/anime/service/service.go | 4 +- internal/app/app.go | 15 +- internal/auth/handler/handler.go | 7 +- internal/auth/module.go | 6 + internal/auth/repository/repository.go | 2 - internal/database/migrations/001_init.sql | 8 + .../migrations/002_add_anime_titles.sql | 3 + .../migrations/003_add_anime_airing.sql | 3 + .../migrations/004_add_notifications.sql | 3 + .../migrations/005_add_anime_relations.sql | 3 + .../migrations/006_add_jikan_cache.sql | 3 + .../migrations/007_add_query_indexes.sql | 3 + .../migrations/009_add_anime_fetch_retry.sql | 3 + .../010_add_watch_progress_seconds.sql | 4 +- .../migrations/011_add_continue_watching.sql | 3 + .../migrations/012_remove_recovery_key.sql | 3 + .../database/migrations/013_drop_account.sql | 4 +- .../migrations/014_add_watchlist_statuses.sql | 3 + .../database/migrations/015_add_duration.sql | 4 +- .../migrations/016_add_avatar_url.sql | 4 +- internal/db/sqlite.go | 9 +- internal/server/server.go | 3 +- internal/templates/renderer.go | 27 ++- internal/watchlist/handler/handler.go | 31 +++- internal/watchlist/service/service.go | 1 - static/assets/style.css | 4 +- templates/anime.gohtml | 8 +- templates/base.gohtml | 24 ++- templates/components/continue_watching.gohtml | 4 +- templates/components/watchlist_actions.gohtml | 5 +- 34 files changed, 274 insertions(+), 102 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 8e7d227..860e6a1 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,7 +1,6 @@ package main import ( - "log" "mal/internal/app" "github.com/joho/godotenv" diff --git a/integrations/jikan/client.go b/integrations/jikan/client.go index 1c88d76..2ba59b6 100644 --- a/integrations/jikan/client.go +++ b/integrations/jikan/client.go @@ -25,7 +25,7 @@ type Client struct { lastReqTime time.Time // rate limiting: last request timestamp } -func NewClient(db db.Querier) *Client { +func NewClient(queries *db.Queries) *Client { return &Client{ httpClient: &http.Client{ Timeout: 10 * time.Second, @@ -37,7 +37,7 @@ func NewClient(db db.Querier) *Client { }, }, baseURL: "https://api.jikan.moe/v4", - db: db, + db: queries, retrySignal: make(chan struct{}, 1), } } diff --git a/integrations/jikan/module.go b/integrations/jikan/module.go index 587b6dc..811d82b 100644 --- a/integrations/jikan/module.go +++ b/integrations/jikan/module.go @@ -1,8 +1,6 @@ package jikan import ( - "mal/internal/db" - "go.uber.org/fx" ) diff --git a/internal/anime/handler/handler.go b/internal/anime/handler/handler.go index 78f122b..679df08 100644 --- a/internal/anime/handler/handler.go +++ b/internal/anime/handler/handler.go @@ -1,28 +1,29 @@ package handler import ( - "context" "fmt" "log" "mal/internal/domain" - "mal/internal/server" "net/http" "strconv" - "strings" - "time" "github.com/gin-gonic/gin" ) type AnimeHandler struct { - svc domain.AnimeService + svc domain.AnimeService + watchlistSvc domain.WatchlistService } -func NewAnimeHandler(svc domain.AnimeService) *AnimeHandler { - return &AnimeHandler{svc: svc} +func NewAnimeHandler(svc domain.AnimeService, watchlistSvc domain.WatchlistService) *AnimeHandler { + return &AnimeHandler{ + svc: svc, + watchlistSvc: watchlistSvc, + } } func (h *AnimeHandler) Register(r *gin.Engine) { + log.Println("Registering anime routes") r.GET("/", h.HandleCatalog) r.GET("/api/catalog/airing", h.HandleCatalogAiring) r.GET("/api/catalog/popular", h.HandleCatalogPopular) @@ -39,8 +40,19 @@ func (h *AnimeHandler) Register(r *gin.Engine) { } func (h *AnimeHandler) HandleCatalog(c *gin.Context) { + user, _ := c.Get("User") + watchlistMap := make(map[int]bool) + if u, ok := user.(*domain.User); ok { + entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), u.ID) + for _, e := range entries { + watchlistMap[int(e.AnimeID)] = true + } + } + c.HTML(http.StatusOK, "index.gohtml", gin.H{ - "CurrentPath": "/", + "CurrentPath": "/", + "User": user, + "WatchlistMap": watchlistMap, }) } @@ -57,21 +69,36 @@ func (h *AnimeHandler) HandleCatalogContinue(c *gin.Context) { } func (h *AnimeHandler) renderCatalogSection(c *gin.Context, section string) { - userID := "" // TODO: get from auth context + user, _ := c.Get("User") + userID := "" + if u, ok := user.(*domain.User); ok { + userID = u.ID + } data, err := h.svc.GetCatalogSection(c.Request.Context(), userID, section) if err != nil { log.Printf("catalog %s error: %v", section, err) return } + watchlistMap := make(map[int]bool) + if userID != "" { + entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), userID) + for _, e := range entries { + watchlistMap[int(e.AnimeID)] = true + } + } + data["Section"] = section data["_fragment"] = "catalog_section" + data["WatchlistMap"] = watchlistMap c.HTML(http.StatusOK, "index.gohtml", data) } func (h *AnimeHandler) HandleDiscover(c *gin.Context) { + user, _ := c.Get("User") c.HTML(http.StatusOK, "discover.gohtml", gin.H{ "CurrentPath": "/discover", + "User": user, }) } @@ -88,15 +115,28 @@ func (h *AnimeHandler) HandleDiscoverTop(c *gin.Context) { } func (h *AnimeHandler) renderDiscoverSection(c *gin.Context, section string) { - userID := "" // TODO: get from auth context + user, _ := c.Get("User") + userID := "" + if u, ok := user.(*domain.User); ok { + userID = u.ID + } data, err := h.svc.GetDiscoverSection(c.Request.Context(), userID, section) if err != nil { log.Printf("discover %s error: %v", section, err) return } + watchlistMap := make(map[int]bool) + if userID != "" { + entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), userID) + for _, e := range entries { + watchlistMap[int(e.AnimeID)] = true + } + } + data["Section"] = section data["_fragment"] = "discover_section" + data["WatchlistMap"] = watchlistMap c.HTML(http.StatusOK, "discover.gohtml", data) } @@ -145,19 +185,30 @@ func (h *AnimeHandler) HandleBrowse(c *gin.Context) { genresList, _ := h.svc.GetGenres(c.Request.Context()) + user, _ := c.Get("User") + watchlistMap := make(map[int]bool) + if u, ok := user.(*domain.User); ok { + entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), u.ID) + for _, e := range entries { + watchlistMap[int(e.AnimeID)] = true + } + } + c.HTML(http.StatusOK, "browse.gohtml", gin.H{ - "CurrentPath": "/browse", - "Query": q, - "Type": animeType, - "Status": status, - "OrderBy": orderBy, - "Sort": sort, - "Genres": genres, - "SFW": sfw, - "GenresList": genresList, - "Animes": res.Animes, - "HasNextPage": res.HasNextPage, - "NextPage": page + 1, + "CurrentPath": "/browse", + "Query": q, + "Type": animeType, + "Status": status, + "OrderBy": orderBy, + "Sort": sort, + "Genres": genres, + "SFW": sfw, + "GenresList": genresList, + "Animes": res.Animes, + "HasNextPage": res.HasNextPage, + "NextPage": page + 1, + "User": user, + "WatchlistMap": watchlistMap, }) } @@ -189,7 +240,7 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) { c.HTML(http.StatusOK, "anime.gohtml", gin.H{ "_fragment": tplName, - "Data": data, + "Items": data, }) return } @@ -200,9 +251,11 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) { return } + user, _ := c.Get("User") c.HTML(http.StatusOK, "anime.gohtml", gin.H{ "Anime": anime, "CurrentPath": fmt.Sprintf("/anime/%d", id), + "User": user, }) } @@ -213,16 +266,31 @@ func (h *AnimeHandler) HandleHTMLWatchOrder(c *gin.Context) { return } + user, _ := c.Get("User") + userID := "" + if u, ok := user.(*domain.User); ok { + userID = u.ID + } + relations, err := h.svc.GetRelations(c.Request.Context(), id) if err != nil { c.Status(http.StatusInternalServerError) return } + watchlistMap := make(map[int]bool) + if userID != "" { + entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), userID) + for _, e := range entries { + watchlistMap[int(e.AnimeID)] = true + } + } + c.HTML(http.StatusOK, "anime.gohtml", gin.H{ - "_fragment": "watch_order", - "Relations": relations, - "AnimeID": id, + "_fragment": "watch_order", + "Relations": relations, + "AnimeID": id, + "WatchlistMap": watchlistMap, }) } @@ -239,20 +307,31 @@ func (h *AnimeHandler) HandleQuickSearch(c *gin.Context) { return } + user, _ := c.Get("User") + watchlistMap := make(map[int]bool) + if u, ok := user.(*domain.User); ok { + entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), u.ID) + for _, e := range entries { + watchlistMap[int(e.AnimeID)] = true + } + } + type quickSearchResult struct { - ID int `json:"id"` - Title string `json:"title"` - Type string `json:"type"` - Image string `json:"image"` + ID int `json:"id"` + Title string `json:"title"` + Type string `json:"type"` + Image string `json:"image"` + InWatchlist bool `json:"in_watchlist"` } output := make([]quickSearchResult, len(res.Animes)) for i, anime := range res.Animes { output[i] = quickSearchResult{ - ID: anime.MalID, - Title: anime.DisplayTitle(), - Type: anime.Type, - Image: anime.ImageURL(), + ID: anime.MalID, + Title: anime.DisplayTitle(), + Type: anime.Type, + Image: anime.ImageURL(), + InWatchlist: watchlistMap[anime.MalID], } } c.JSON(http.StatusOK, output) @@ -264,5 +343,21 @@ func (h *AnimeHandler) HandleRandomAnime(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch random anime"}) return } - c.JSON(http.StatusOK, gin.H{"data": anime}) + + user, _ := c.Get("User") + inWatchlist := false + if u, ok := user.(*domain.User); ok { + entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), u.ID) + for _, e := range entries { + if int(e.AnimeID) == anime.MalID { + inWatchlist = true + break + } + } + } + + c.JSON(http.StatusOK, gin.H{ + "data": anime, + "in_watchlist": inWatchlist, + }) } diff --git a/internal/anime/service/service.go b/internal/anime/service/service.go index 6e754f7..93f2c57 100644 --- a/internal/anime/service/service.go +++ b/internal/anime/service/service.go @@ -128,7 +128,7 @@ func (s *animeService) GetAnimeByID(ctx context.Context, id int) (domain.Anime, return s.jikan.GetAnimeByID(ctx, id) } -func (s *animeService) SearchAdvanced(ctx context.Context, q, animeType, status, orderBy, sort string, genres []int, sfw bool, page, limit int) (jikan.SearchResponse, error) { +func (s *animeService) SearchAdvanced(ctx context.Context, q, animeType, status, orderBy, sort string, genres []int, sfw bool, page, limit int) (jikan.SearchResult, error) { return s.jikan.SearchAdvanced(ctx, q, animeType, status, orderBy, sort, genres, sfw, page, limit) } @@ -144,7 +144,7 @@ func (s *animeService) GetRecommendations(ctx context.Context, id int) ([]domain return s.jikan.GetAnimeRecommendations(ctx, id) } -func (s *animeService) GetRelations(ctx context.Context, id int) ([]jikan.Relation, error) { +func (s *animeService) GetRelations(ctx context.Context, id int) ([]jikan.RelationEntry, error) { return s.jikan.GetFullRelations(ctx, id) } diff --git a/internal/app/app.go b/internal/app/app.go index 4672d5b..2d431d3 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,6 +1,8 @@ package app import ( + "mal/integrations/jikan" + "mal/integrations/playback/allanime" "mal/internal/database" "mal/internal/auth" "mal/internal/anime" @@ -18,17 +20,22 @@ func NewApp() *fx.App { return fx.New( database.Module, jikan.Module, + allanime.Module, auth.Module, anime.Module, watchlist.Module, playback.Module, templates.Module, server.Module, - fx.Decorate(func(r *templates.Renderer) render.HTMLRender { + fx.Provide(func(r *templates.Renderer) render.HTMLRender { return r }), - fx.Invoke(func(r *gin.Engine, registers []server.RouteRegister) { - server.RegisterRoutes(r, registers) - }), + fx.Invoke(fx.Annotate( + func(r *gin.Engine, authMiddleware gin.HandlerFunc, registers []server.RouteRegister) { + r.Use(authMiddleware) + server.RegisterRoutes(r, registers) + }, + fx.ParamTags(``, ``, `group:"routes"`), + )), ) } diff --git a/internal/auth/handler/handler.go b/internal/auth/handler/handler.go index 1474819..e844a18 100644 --- a/internal/auth/handler/handler.go +++ b/internal/auth/handler/handler.go @@ -2,7 +2,6 @@ package handler import ( "mal/internal/domain" - "mal/internal/server" "net/http" "time" @@ -43,7 +42,11 @@ func (h *AuthHandler) HandleLogin(c *gin.Context) { } c.SetCookie("session_id", session.ID, int(24*time.Hour.Seconds()), "/", "", false, true) - c.Header("HX-Redirect", "/") + if c.GetHeader("HX-Request") == "true" { + c.Header("HX-Redirect", "/") + c.Status(http.StatusOK) + return + } c.Redirect(http.StatusSeeOther, "/") } diff --git a/internal/auth/module.go b/internal/auth/module.go index c9ea02c..e5721e6 100644 --- a/internal/auth/module.go +++ b/internal/auth/module.go @@ -2,10 +2,13 @@ package auth import ( "mal/internal/auth/handler" + "mal/internal/auth/middleware" "mal/internal/auth/repository" "mal/internal/auth/service" + "mal/internal/domain" "mal/internal/server" + "github.com/gin-gonic/gin" "go.uber.org/fx" ) @@ -14,6 +17,9 @@ var Module = fx.Options( repository.NewAuthRepository, service.NewAuthService, handler.NewAuthHandler, + func(svc domain.AuthService) gin.HandlerFunc { + return middleware.AuthMiddleware(svc) + }, ), fx.Provide( server.AsRouteRegister(func(h *handler.AuthHandler) server.RouteRegister { diff --git a/internal/auth/repository/repository.go b/internal/auth/repository/repository.go index b41f241..9ec4e8d 100644 --- a/internal/auth/repository/repository.go +++ b/internal/auth/repository/repository.go @@ -7,8 +7,6 @@ import ( "mal/internal/db" "mal/internal/domain" "time" - - "github.com/google/uuid" ) type authRepository struct { diff --git a/internal/database/migrations/001_init.sql b/internal/database/migrations/001_init.sql index f3a2932..09fc8f7 100644 --- a/internal/database/migrations/001_init.sql +++ b/internal/database/migrations/001_init.sql @@ -1,3 +1,4 @@ +-- +goose Up CREATE TABLE IF NOT EXISTS user ( id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, @@ -40,3 +41,10 @@ CREATE TABLE IF NOT EXISTS watch_list_entry ( current_time_seconds REAL NOT NULL DEFAULT 0, UNIQUE(user_id, anime_id) ); + +-- +goose Down +DROP TABLE IF EXISTS watch_list_entry; +DROP TABLE IF EXISTS anime; +DROP TABLE IF EXISTS account; +DROP TABLE IF EXISTS session; +DROP TABLE IF EXISTS user; diff --git a/internal/database/migrations/002_add_anime_titles.sql b/internal/database/migrations/002_add_anime_titles.sql index a1f2564..e0427a5 100644 --- a/internal/database/migrations/002_add_anime_titles.sql +++ b/internal/database/migrations/002_add_anime_titles.sql @@ -1,6 +1,9 @@ +-- +goose Up -- Add English and Japanese title columns to anime table ALTER TABLE anime ADD COLUMN title_english TEXT; ALTER TABLE anime ADD COLUMN title_japanese TEXT; -- Rename existing title to title_original for clarity ALTER TABLE anime RENAME COLUMN title TO title_original; + +-- +goose Down diff --git a/internal/database/migrations/003_add_anime_airing.sql b/internal/database/migrations/003_add_anime_airing.sql index 8f74ee6..dce11c7 100644 --- a/internal/database/migrations/003_add_anime_airing.sql +++ b/internal/database/migrations/003_add_anime_airing.sql @@ -1,2 +1,5 @@ +-- +goose Up -- Add airing status column to anime table ALTER TABLE anime ADD COLUMN airing BOOLEAN DEFAULT 0; + +-- +goose Down diff --git a/internal/database/migrations/004_add_notifications.sql b/internal/database/migrations/004_add_notifications.sql index a51e1ce..33936f7 100644 --- a/internal/database/migrations/004_add_notifications.sql +++ b/internal/database/migrations/004_add_notifications.sql @@ -1,3 +1,4 @@ +-- +goose Up -- Note: watch_list_entry columns now in 001_init.sql -- Add notification preferences @@ -8,3 +9,5 @@ CREATE TABLE IF NOT EXISTS notification_preference ( created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id) ); + +-- +goose Down diff --git a/internal/database/migrations/005_add_anime_relations.sql b/internal/database/migrations/005_add_anime_relations.sql index 11a82ee..f12329c 100644 --- a/internal/database/migrations/005_add_anime_relations.sql +++ b/internal/database/migrations/005_add_anime_relations.sql @@ -1,3 +1,4 @@ +-- +goose Up ALTER TABLE anime ADD COLUMN status TEXT DEFAULT ''; ALTER TABLE anime ADD COLUMN relations_synced_at DATETIME; @@ -7,3 +8,5 @@ CREATE TABLE IF NOT EXISTS anime_relation ( relation_type TEXT NOT NULL, PRIMARY KEY (anime_id, related_anime_id) ); + +-- +goose Down diff --git a/internal/database/migrations/006_add_jikan_cache.sql b/internal/database/migrations/006_add_jikan_cache.sql index bc4852a..99b63d5 100644 --- a/internal/database/migrations/006_add_jikan_cache.sql +++ b/internal/database/migrations/006_add_jikan_cache.sql @@ -1,6 +1,9 @@ +-- +goose Up CREATE TABLE IF NOT EXISTS jikan_cache ( key TEXT PRIMARY KEY, data TEXT NOT NULL, expires_at DATETIME NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); + +-- +goose Down diff --git a/internal/database/migrations/007_add_query_indexes.sql b/internal/database/migrations/007_add_query_indexes.sql index 206396f..cbacaef 100644 --- a/internal/database/migrations/007_add_query_indexes.sql +++ b/internal/database/migrations/007_add_query_indexes.sql @@ -1,3 +1,4 @@ +-- +goose Up CREATE INDEX IF NOT EXISTS idx_watch_list_entry_user_status_updated_at ON watch_list_entry(user_id, status, updated_at); @@ -9,3 +10,5 @@ ON anime(relations_synced_at, status); CREATE INDEX IF NOT EXISTS idx_jikan_cache_expires_at ON jikan_cache(expires_at); + +-- +goose Down diff --git a/internal/database/migrations/009_add_anime_fetch_retry.sql b/internal/database/migrations/009_add_anime_fetch_retry.sql index ffbbe40..11d5e10 100644 --- a/internal/database/migrations/009_add_anime_fetch_retry.sql +++ b/internal/database/migrations/009_add_anime_fetch_retry.sql @@ -1,3 +1,4 @@ +-- +goose Up CREATE TABLE IF NOT EXISTS anime_fetch_retry ( anime_id INTEGER PRIMARY KEY, attempts INTEGER NOT NULL DEFAULT 0, @@ -9,3 +10,5 @@ CREATE TABLE IF NOT EXISTS anime_fetch_retry ( CREATE INDEX IF NOT EXISTS idx_anime_fetch_retry_next_retry_at ON anime_fetch_retry(next_retry_at); + +-- +goose Down diff --git a/internal/database/migrations/010_add_watch_progress_seconds.sql b/internal/database/migrations/010_add_watch_progress_seconds.sql index c29e82b..6d59f67 100644 --- a/internal/database/migrations/010_add_watch_progress_seconds.sql +++ b/internal/database/migrations/010_add_watch_progress_seconds.sql @@ -1 +1,3 @@ --- Note: watch_list_entry columns now in 001_init.sql \ No newline at end of file +-- +goose Up +-- Note: watch_list_entry columns now in 001_init.sql +-- +goose Down diff --git a/internal/database/migrations/011_add_continue_watching.sql b/internal/database/migrations/011_add_continue_watching.sql index d12b2af..5f0ff6c 100644 --- a/internal/database/migrations/011_add_continue_watching.sql +++ b/internal/database/migrations/011_add_continue_watching.sql @@ -1,3 +1,4 @@ +-- +goose Up CREATE TABLE IF NOT EXISTS continue_watching_entry ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE, @@ -11,3 +12,5 @@ CREATE TABLE IF NOT EXISTS continue_watching_entry ( CREATE INDEX IF NOT EXISTS idx_continue_watching_user_updated ON continue_watching_entry(user_id, updated_at DESC); + +-- +goose Down diff --git a/internal/database/migrations/012_remove_recovery_key.sql b/internal/database/migrations/012_remove_recovery_key.sql index e60c060..5f33e4e 100644 --- a/internal/database/migrations/012_remove_recovery_key.sql +++ b/internal/database/migrations/012_remove_recovery_key.sql @@ -1,3 +1,4 @@ +-- +goose Up PRAGMA foreign_keys = OFF; BEGIN TRANSACTION; @@ -20,3 +21,5 @@ ALTER TABLE user_new RENAME TO user; COMMIT; PRAGMA foreign_keys = ON; + +-- +goose Down diff --git a/internal/database/migrations/013_drop_account.sql b/internal/database/migrations/013_drop_account.sql index 6a6cca9..780ca0e 100644 --- a/internal/database/migrations/013_drop_account.sql +++ b/internal/database/migrations/013_drop_account.sql @@ -1,2 +1,4 @@ +-- +goose Up DROP TABLE IF EXISTS account; -DROP TABLE IF EXISTS notification_preference; \ No newline at end of file +DROP TABLE IF EXISTS notification_preference; +-- +goose Down diff --git a/internal/database/migrations/014_add_watchlist_statuses.sql b/internal/database/migrations/014_add_watchlist_statuses.sql index b0d8bd1..2588db0 100644 --- a/internal/database/migrations/014_add_watchlist_statuses.sql +++ b/internal/database/migrations/014_add_watchlist_statuses.sql @@ -1,3 +1,4 @@ +-- +goose Up -- Add "watching" and "on_hold" to the valid statuses for watch_list_entry PRAGMA foreign_keys=OFF; @@ -24,3 +25,5 @@ FROM watch_list_entry_old; DROP TABLE watch_list_entry_old; PRAGMA foreign_keys=ON; + +-- +goose Down diff --git a/internal/database/migrations/015_add_duration.sql b/internal/database/migrations/015_add_duration.sql index 80057a5..81564ca 100644 --- a/internal/database/migrations/015_add_duration.sql +++ b/internal/database/migrations/015_add_duration.sql @@ -1,5 +1,7 @@ +-- +goose Up -- Add duration column to anime table to store episode duration in seconds ALTER TABLE anime ADD COLUMN duration_seconds REAL; -- Add duration_seconds column to continue_watching_entry to track episode duration -ALTER TABLE continue_watching_entry ADD COLUMN duration_seconds REAL; \ No newline at end of file +ALTER TABLE continue_watching_entry ADD COLUMN duration_seconds REAL; +-- +goose Down diff --git a/internal/database/migrations/016_add_avatar_url.sql b/internal/database/migrations/016_add_avatar_url.sql index bb153ac..21abe1f 100644 --- a/internal/database/migrations/016_add_avatar_url.sql +++ b/internal/database/migrations/016_add_avatar_url.sql @@ -1,3 +1,5 @@ +-- +goose Up ALTER TABLE user ADD COLUMN avatar_url TEXT NOT NULL DEFAULT ''; -UPDATE user SET avatar_url = 'https://api.dicebear.com/9.x/dylan/svg?seed=' || username WHERE avatar_url = ''; \ No newline at end of file +UPDATE user SET avatar_url = 'https://api.dicebear.com/9.x/dylan/svg?seed=' || username WHERE avatar_url = ''; +-- +goose Down diff --git a/internal/db/sqlite.go b/internal/db/sqlite.go index bec1147..db160cf 100644 --- a/internal/db/sqlite.go +++ b/internal/db/sqlite.go @@ -38,14 +38,7 @@ func GetMigrationsDir() (string, error) { return filepath.Join(wd, "migrations"), nil } -// Init opens the database, runs migrations, and returns a Queries instance +// Init opens the database and returns a Queries instance func Init(db *sql.DB) (*Queries, error) { - migrationsDir, err := GetMigrationsDir() - if err != nil { - return nil, err - } - if err := RunMigrations(db, migrationsDir); err != nil { - return nil, fmt.Errorf("failed to run migrations: %w", err) - } return New(db), nil } diff --git a/internal/server/server.go b/internal/server/server.go index ce2be58..125f6ff 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -2,7 +2,6 @@ package server import ( "context" - "fmt" "log" "net/http" "os" @@ -23,6 +22,8 @@ func ProvideRouter(htmlRender render.HTMLRender) *gin.Engine { } r := gin.New() r.Use(gin.Logger(), gin.Recovery()) + r.Static("/static", "./static") + r.Static("/dist", "./dist") r.HTMLRender = htmlRender return r } diff --git a/internal/templates/renderer.go b/internal/templates/renderer.go index fdc3bb2..0aef18d 100644 --- a/internal/templates/renderer.go +++ b/internal/templates/renderer.go @@ -1,13 +1,12 @@ package templates import ( - "context" "encoding/json" "fmt" "html/template" "io" - "log" "net/http" + "os" "path/filepath" "slices" "strconv" @@ -21,7 +20,7 @@ import ( // FS is the interface for template filesystem, to be provided by the main app or a mock. type FS interface { ReadFile(name string) ([]byte, error) - ReadDir(name string) ([]osDirEntry, error) + ReadDir(name string) ([]os.DirEntry, error) } // We will use embed.FS but wrapped in an interface if needed, or just use it directly. @@ -153,14 +152,16 @@ func ProvideRenderer() (*Renderer, error) { return nil, err } + basePath := filepath.Join(".", "templates", "base.gohtml") + for _, page := range pages { name := filepath.Base(page) if name == "base.gohtml" { continue } - tmpl := template.New(name).Funcs(funcs) - tmpl = template.Must(tmpl.ParseFiles(filepath.Join(".", "templates", "base.gohtml"))) + tmpl := template.New("base.gohtml").Funcs(funcs) + tmpl = template.Must(tmpl.ParseFiles(basePath)) if len(components) > 0 { tmpl = template.Must(tmpl.ParseFiles(components...)) } @@ -192,10 +193,18 @@ func (h HTMLRender) Render(w http.ResponseWriter) error { return fmt.Errorf("template %s not found", h.Name) } - if block, ok := h.Data.(map[string]any)["_fragment"]; ok { - if blockStr, ok := block.(string); ok { - return tmpl.ExecuteTemplate(w, blockStr, h.Data) - } + var block any + + // Handle both map[string]any and gin.H (which is map[string]any but might + // behave differently depending on the Go version/compiler in type assertions) + if dataMap, ok := h.Data.(map[string]any); ok { + block = dataMap["_fragment"] + } else if ginH, ok := h.Data.(gin.H); ok { + block = ginH["_fragment"] + } + + if blockStr, ok := block.(string); ok && blockStr != "" { + return tmpl.ExecuteTemplate(w, blockStr, h.Data) } return tmpl.ExecuteTemplate(w, "base.gohtml", h.Data) diff --git a/internal/watchlist/handler/handler.go b/internal/watchlist/handler/handler.go index 8293323..190ee3f 100644 --- a/internal/watchlist/handler/handler.go +++ b/internal/watchlist/handler/handler.go @@ -2,10 +2,8 @@ package handler import ( "mal/internal/domain" - "mal/internal/server" "net/http" "strconv" - "strings" "github.com/gin-gonic/gin" ) @@ -26,7 +24,12 @@ func (h *WatchlistHandler) Register(r *gin.Engine) { } func (h *WatchlistHandler) HandleUpdateWatchlist(c *gin.Context) { - userID := "" // TODO: get from auth context + user, _ := c.Get("User") + userID := "" + if u, ok := user.(*domain.User); ok { + userID = u.ID + } + animeID, _ := strconv.ParseInt(c.PostForm("anime_id"), 10, 64) status := c.PostForm("status") @@ -45,7 +48,12 @@ func (h *WatchlistHandler) HandleUpdateWatchlist(c *gin.Context) { } func (h *WatchlistHandler) HandleDeleteWatchlist(c *gin.Context) { - userID := "" // TODO: get from auth context + user, _ := c.Get("User") + userID := "" + if u, ok := user.(*domain.User); ok { + userID = u.ID + } + animeID, _ := strconv.ParseInt(c.Param("id"), 10, 64) if animeID <= 0 { @@ -63,7 +71,12 @@ func (h *WatchlistHandler) HandleDeleteWatchlist(c *gin.Context) { } func (h *WatchlistHandler) HandleDeleteContinueWatching(c *gin.Context) { - userID := "" // TODO: get from auth context + user, _ := c.Get("User") + userID := "" + if u, ok := user.(*domain.User); ok { + userID = u.ID + } + animeID, _ := strconv.ParseInt(c.Param("id"), 10, 64) if animeID <= 0 { @@ -81,7 +94,12 @@ func (h *WatchlistHandler) HandleDeleteContinueWatching(c *gin.Context) { } func (h *WatchlistHandler) HandleGetWatchlist(c *gin.Context) { - userID := "" // TODO: get from auth context + user, _ := c.Get("User") + userID := "" + if u, ok := user.(*domain.User); ok { + userID = u.ID + } + entries, err := h.svc.GetWatchlist(c.Request.Context(), userID) if err != nil { c.Status(http.StatusInternalServerError) @@ -91,5 +109,6 @@ func (h *WatchlistHandler) HandleGetWatchlist(c *gin.Context) { c.HTML(http.StatusOK, "watchlist.gohtml", gin.H{ "Entries": entries, "CurrentPath": "/watchlist", + "User": user, }) } diff --git a/internal/watchlist/service/service.go b/internal/watchlist/service/service.go index 09a0c03..5547fc6 100644 --- a/internal/watchlist/service/service.go +++ b/internal/watchlist/service/service.go @@ -3,7 +3,6 @@ package service import ( "context" "database/sql" - "fmt" "mal/integrations/jikan" "mal/internal/db" "mal/internal/domain" diff --git a/static/assets/style.css b/static/assets/style.css index 51980bd..5520eb5 100644 --- a/static/assets/style.css +++ b/static/assets/style.css @@ -2,8 +2,8 @@ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&display=swap'); @import '@toolwind/anchors'; -@source "."; -@source "../web/**/*.templ"; +@source "../../templates/**/*.gohtml"; +@source "../**/*.ts"; @theme { --color-background: light-dark(#ffffff, #080808); diff --git a/templates/anime.gohtml b/templates/anime.gohtml index 0236b7f..1069a8e 100644 --- a/templates/anime.gohtml +++ b/templates/anime.gohtml @@ -2,7 +2,7 @@

Characters & Cast

- {{range (slice . 0 (min (len .) 10))}} + {{range (slice .Items 0 (min (len .Items) 10))}}
{{.Character.Name}} @@ -21,11 +21,11 @@ {{end}} {{define "anime_recommendations"}} -{{if .}} +{{if .Items}}

Recommendations

- {{range (slice . 0 (min (len .) 8))}} + {{range (slice .Items 0 (min (len .Items) 8))}}
{{.Entry.Title}} @@ -43,7 +43,7 @@ {{if .WatchlistIDs}}{{end}} {{$anime := .Anime}} -
+
diff --git a/templates/base.gohtml b/templates/base.gohtml index bbc9f42..61201d9 100644 --- a/templates/base.gohtml +++ b/templates/base.gohtml @@ -33,7 +33,7 @@ html[data-theme="light"] .theme-icon-light { display: none; } html[data-theme="light"] .theme-icon-dark { display: block; } - + - - - - + + + + - - - - + + + +