chore: cleanup files

This commit is contained in:
2026-04-08 18:02:54 +02:00
parent b3477fa7dd
commit a0617ec127
17 changed files with 82 additions and 81 deletions

1
go.mod
View File

@@ -5,7 +5,6 @@ go 1.24.0
require (
github.com/a-h/templ v0.3.1001
github.com/google/uuid v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/mattn/go-sqlite3 v1.14.40
golang.org/x/crypto v0.31.0
)

View File

@@ -10,10 +10,10 @@ import (
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
ExecContext(context.Context, string, ...any) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...any) *sql.Row
}
func New(db DBTX) *Queries {

View File

@@ -2,7 +2,6 @@ package database
import "database/sql"
// DisplayTitle returns the English title if available, otherwise Japanese, otherwise original
func DisplayTitle(titleEnglish, titleJapanese sql.NullString, titleOriginal string) string {
if titleEnglish.Valid && titleEnglish.String != "" {
return titleEnglish.String
@@ -13,7 +12,6 @@ func DisplayTitle(titleEnglish, titleJapanese sql.NullString, titleOriginal stri
return titleOriginal
}
// Deprecated: use DisplayTitle function directly
func (r GetUserWatchListRow) DisplayTitle() string {
return DisplayTitle(r.TitleEnglish, r.TitleJapanese, r.TitleOriginal)
}

View File

@@ -215,7 +215,7 @@ func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) {
if query == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode([]interface{}{})
json.NewEncoder(w).Encode([]any{})
return
}

View File

@@ -269,7 +269,7 @@ func (h *Handler) sortEntries(entries []database.GetUserWatchListRow, sortBy, so
}
// Simple bubble sort for small lists
for i := 0; i < len(entries); i++ {
for i := range len(entries) {
for j := i + 1; j < len(entries); j++ {
if less(j, i) {
entries[i], entries[j] = entries[j], entries[i]

View File

@@ -5,7 +5,6 @@ import (
"time"
)
// GetAnimeByID fetches full details for a single anime
func (c *Client) GetAnimeByID(id int) (Anime, error) {
cacheKey := fmt.Sprintf("anime:%d", id)
var cached Anime

View File

@@ -43,7 +43,7 @@ func (c *Client) waitRateLimit() {
}
}
func (c *Client) getCache(key string, out interface{}) bool {
func (c *Client) getCache(key string, out any) bool {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
@@ -56,7 +56,7 @@ func (c *Client) getCache(key string, out interface{}) bool {
return err == nil
}
func (c *Client) setCache(key string, data interface{}, ttl time.Duration) {
func (c *Client) setCache(key string, data any, ttl time.Duration) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
@@ -72,10 +72,9 @@ func (c *Client) setCache(key string, data interface{}, ttl time.Duration) {
})
}
// fetchWithRetry provides robust fetching respecting Jikan's strict 3 req/sec rate limit
func (c *Client) fetchWithRetry(urlStr string, out interface{}) error {
func (c *Client) fetchWithRetry(urlStr string, out any) error {
maxRetries := 5
for i := 0; i < maxRetries; i++ {
for range maxRetries {
c.waitRateLimit()
resp, err := c.httpClient.Get(urlStr)

View File

@@ -5,7 +5,6 @@ import (
"time"
)
// RecommendationEntry represents a single recommendation
type RecommendationEntry struct {
Entry struct {
MalID int `json:"mal_id"`
@@ -24,7 +23,6 @@ type RecommendationsResponse struct {
Data []RecommendationEntry `json:"data"`
}
// GetRecommendations fetches recommended anime
func (c *Client) GetRecommendations(animeID int, limit int) ([]Anime, error) {
cacheKey := fmt.Sprintf("recs:%d", animeID)
var cached []Anime

View File

@@ -1,6 +1,7 @@
package jikan
// findFirstAnimeRelation extracts the first related anime ID for a specific relation type
import "maps"
func findFirstAnimeRelation(groups []JikanRelationGroup, relType string) *int {
for _, group := range groups {
if group.Relation == relType {
@@ -15,7 +16,6 @@ func findFirstAnimeRelation(groups []JikanRelationGroup, relType string) *int {
return nil
}
// fetchChain recursively builds the relational chain (Prequels or Sequels)
func (c *Client) fetchChain(startID int, direction string, visited map[int]bool) ([]RelationEntry, error) {
anime, err := c.GetAnimeByID(startID)
if err != nil {
@@ -24,7 +24,7 @@ func (c *Client) fetchChain(startID int, direction string, visited map[int]bool)
nextIDPtr := findFirstAnimeRelation(anime.Relations, direction)
if nextIDPtr == nil {
return nil, nil // normal end of chain
return nil, nil
}
nextID := *nextIDPtr
@@ -50,7 +50,6 @@ func (c *Client) fetchChain(startID int, direction string, visited map[int]bool)
return append([]RelationEntry{entry}, rest...), nil
}
// GetFullRelations resolves the full Prequel/Sequel chronological chain synchronously
func (c *Client) GetFullRelations(id int) ([]RelationEntry, error) {
currentAnime, err := c.GetAnimeByID(id)
if err != nil {
@@ -62,9 +61,7 @@ func (c *Client) GetFullRelations(id int) ([]RelationEntry, error) {
prequels, err1 := c.fetchChain(id, "Prequel", visited)
visitedSeq := make(map[int]bool)
for k, v := range visited {
visitedSeq[k] = v
}
maps.Copy(visitedSeq, visited)
sequels, err2 := c.fetchChain(id, "Sequel", visitedSeq)

View File

@@ -6,7 +6,6 @@ import (
"time"
)
// Search returns the anime list with pagination support
func (c *Client) Search(query string, page int) (SearchResult, error) {
if query == "" {
return SearchResult{}, nil
@@ -36,7 +35,6 @@ func (c *Client) Search(query string, page int) (SearchResult, error) {
return res, nil
}
// GetTopAnime fetches the top anime by popularity (default) or other filters
func (c *Client) GetTopAnime(page int) (TopAnimeResult, error) {
if page < 1 {
page = 1

View File

@@ -6,14 +6,11 @@ import (
"time"
)
// ScheduleResult contains anime grouped by day
type ScheduleResult struct {
Animes []Anime
HasNextPage bool
}
// GetSchedule fetches anime airing on a specific day
// day can be: monday, tuesday, wednesday, thursday, friday, saturday, sunday, unknown, other
func (c *Client) GetSchedule(day string) (ScheduleResult, error) {
day = strings.ToLower(day)
cacheKey := fmt.Sprintf("schedule_%s", day)
@@ -38,7 +35,6 @@ func (c *Client) GetSchedule(day string) (ScheduleResult, error) {
return res, nil
}
// GetFullSchedule fetches all days at once
func (c *Client) GetFullSchedule() (map[string][]Anime, error) {
days := []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}
schedule := make(map[string][]Anime)
@@ -54,7 +50,6 @@ func (c *Client) GetFullSchedule() (map[string][]Anime, error) {
return schedule, nil
}
// GetSeasonsNow fetches currently airing anime
func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) {
if page < 1 {
page = 1
@@ -80,7 +75,6 @@ func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) {
return res, nil
}
// GetSeasonsUpcoming fetches upcoming anime
func (c *Client) GetSeasonsUpcoming(page int) (TopAnimeResult, error) {
if page < 1 {
page = 1

View File

@@ -12,20 +12,17 @@ type TopAnimeResult struct {
HasNextPage bool
}
// NamedEntity represents genres, studios, producers, etc.
type NamedEntity struct {
MalID int `json:"mal_id"`
Name string `json:"name"`
}
// Aired represents the airing date range
type Aired struct {
From string `json:"from"`
To string `json:"to"`
String string `json:"string"`
}
// Anime struct matching the Jikan v4 API structure
type Anime struct {
MalID int `json:"mal_id"`
Title string `json:"title"`
@@ -73,12 +70,10 @@ type Anime struct {
Relations []JikanRelationGroup `json:"relations"`
}
// ImageURL returns the webp large image URL
func (a Anime) ImageURL() string {
return a.Images.Webp.LargeImageURL
}
// ShortRating returns abbreviated rating (e.g., "PG-13" from "PG-13 - Teens 13 or older")
func (a Anime) ShortRating() string {
if a.Rating == "" {
return ""
@@ -92,7 +87,6 @@ func (a Anime) ShortRating() string {
return a.Rating
}
// ShortDuration returns abbreviated duration (e.g., "23m" from "23 min per ep")
func (a Anime) ShortDuration() string {
if a.Duration == "" {
return ""
@@ -112,7 +106,6 @@ func (a Anime) ShortDuration() string {
return a.Duration
}
// Premiered returns season + year (e.g., "Fall 2002")
func (a Anime) Premiered() string {
if a.Season != "" && a.Year > 0 {
return fmt.Sprintf("%s %d", a.Season, a.Year)
@@ -138,7 +131,6 @@ type TopAnimeResponse struct {
Pagination Pagination `json:"pagination"`
}
// Relation Types
type JikanRelationEntry struct {
MalID int `json:"mal_id"`
Type string `json:"type"`
@@ -160,7 +152,6 @@ type RelationEntry struct {
IsCurrent bool
}
// DisplayTitle prefers English, falls back to Japanese, then standard Title
func (a Anime) DisplayTitle() string {
if a.TitleEnglish != "" {
return a.TitleEnglish

View File

@@ -32,7 +32,6 @@ func NewRouter(cfg Config) http.Handler {
fs := http.FileServer(http.Dir("./static"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
// Anime / Search / Catalog
mux.HandleFunc("/", animeHandler.HandleCatalog)
mux.HandleFunc("/discover", animeHandler.HandleDiscover)
mux.HandleFunc("/schedule", animeHandler.HandleSchedule)
@@ -67,12 +66,12 @@ func NewRouter(cfg Config) http.Handler {
middleware.VerifyOrigin(http.HandlerFunc(authHandler.HandleLogout)).ServeHTTP(w, r)
})
// Watchlist POST endpoint (Protected)
mux.Handle("/api/watchlist/export", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleExportWatchlist)))
mux.Handle("/api/watchlist/import", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleImportWatchlist)))
mux.Handle("/api/watchlist", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleUpdateWatchlist)))
mux.Handle("/api/watchlist/", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleDeleteWatchlist)))
mux.Handle("/watchlist", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleGetWatchlist)))
// Watchlist Endpoints
mux.HandleFunc("/api/watchlist/export", watchlistHandler.HandleExportWatchlist)
mux.HandleFunc("/api/watchlist/import", watchlistHandler.HandleImportWatchlist)
mux.HandleFunc("/api/watchlist", watchlistHandler.HandleUpdateWatchlist)
mux.HandleFunc("/api/watchlist/", watchlistHandler.HandleDeleteWatchlist)
mux.HandleFunc("/watchlist", watchlistHandler.HandleGetWatchlist)
// Wrap mux with global auth checking, THEN auth context parsing
protectedHandler := middleware.RequireGlobalAuth(mux)

View File

@@ -41,7 +41,6 @@ func Auth(authService *auth.Service) func(http.Handler) http.Handler {
}
}
// RequireAuth ensures that a valid user is in the context, otherwise unauthorized
func RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(UserContextKey).(*database.User)
@@ -58,7 +57,6 @@ func RequireAuth(next http.Handler) http.Handler {
})
}
// RequireGlobalAuth ensures that a valid user is in the context for all routes except login and static
func RequireGlobalAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow unauthenticated access to login, register, search, and static files
@@ -83,7 +81,6 @@ func RequireGlobalAuth(next http.Handler) http.Handler {
})
}
// GetUser returns the user from context, or nil if not logged in
func GetUser(ctx context.Context) *database.User {
user, ok := ctx.Value(UserContextKey).(*database.User)
if !ok {

View File

@@ -5,8 +5,6 @@ import (
"net/url"
)
// VerifyOrigin prevents simple CSRF by ensuring the Origin or Referer header matches the Host header
// for state-changing endpoints (POST/PUT/DELETE).
func VerifyOrigin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions {
@@ -32,8 +30,6 @@ func VerifyOrigin(next http.Handler) http.Handler {
}
host := r.Host
// Optional: strip port if you only care about domain
// If origin doesn't match host (accounting for potential schema prefixes)
expectedHTTP := "http://" + host
expectedHTTPS := "https://" + host

View File

@@ -34,7 +34,6 @@ func cleanupVisitors() {
}
}
// getIP attempts to get the real IP, falling back to RemoteAddr
func getIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
ips := strings.Split(xff, ",")
@@ -50,7 +49,6 @@ func getIP(r *http.Request) string {
return ip
}
// RateLimitAuth limits login/register attempts to prevent brute force
func RateLimitAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := getIP(r)

View File

@@ -10,21 +10,34 @@
--link-active: #9966ff;
/* fluid typography scale */
--text-xs: clamp(0.625rem, 0.55rem + 0.25vw, 0.75rem); /* 10-12px */
--text-sm: clamp(0.6875rem, 0.6rem + 0.3vw, 0.8125rem); /* 11-13px */
--text-base: clamp(0.75rem, 0.65rem + 0.35vw, 0.9375rem); /* 12-15px */
--text-md: clamp(0.8125rem, 0.7rem + 0.4vw, 1rem); /* 13-16px */
--text-lg: clamp(0.875rem, 0.75rem + 0.45vw, 1.125rem); /* 14-18px */
--text-xl: clamp(1rem, 0.85rem + 0.5vw, 1.375rem); /* 16-22px */
--text-2xl: clamp(1.125rem, 0.95rem + 0.6vw, 1.5rem); /* 18-24px */
--text-xs: clamp(0.625rem, 0.55rem + 0.25vw, 0.75rem);
/* 10-12px */
--text-sm: clamp(0.6875rem, 0.6rem + 0.3vw, 0.8125rem);
/* 11-13px */
--text-base: clamp(0.75rem, 0.65rem + 0.35vw, 0.9375rem);
/* 12-15px */
--text-md: clamp(0.8125rem, 0.7rem + 0.4vw, 1rem);
/* 13-16px */
--text-lg: clamp(0.875rem, 0.75rem + 0.45vw, 1.125rem);
/* 14-18px */
--text-xl: clamp(1rem, 0.85rem + 0.5vw, 1.375rem);
/* 16-22px */
--text-2xl: clamp(1.125rem, 0.95rem + 0.6vw, 1.5rem);
/* 18-24px */
/* fluid spacing */
--space-xs: clamp(0.25rem, 0.2rem + 0.15vw, 0.375rem); /* 4-6px */
--space-sm: clamp(0.375rem, 0.3rem + 0.2vw, 0.5rem); /* 6-8px */
--space-md: clamp(0.5rem, 0.4rem + 0.3vw, 0.75rem); /* 8-12px */
--space-lg: clamp(0.75rem, 0.6rem + 0.5vw, 1rem); /* 12-16px */
--space-xl: clamp(1rem, 0.8rem + 0.7vw, 1.5rem); /* 16-24px */
--space-2xl: clamp(1.5rem, 1.2rem + 1vw, 2.5rem); /* 24-40px */
--space-xs: clamp(0.25rem, 0.2rem + 0.15vw, 0.375rem);
/* 4-6px */
--space-sm: clamp(0.375rem, 0.3rem + 0.2vw, 0.5rem);
/* 6-8px */
--space-md: clamp(0.5rem, 0.4rem + 0.3vw, 0.75rem);
/* 8-12px */
--space-lg: clamp(0.75rem, 0.6rem + 0.5vw, 1rem);
/* 12-16px */
--space-xl: clamp(1rem, 0.8rem + 0.7vw, 1.5rem);
/* 16-24px */
--space-2xl: clamp(1.5rem, 1.2rem + 1vw, 2.5rem);
/* 24-40px */
/* fluid sizing */
--poster-width: clamp(100px, 8vw + 60px, 180px);
@@ -214,6 +227,7 @@ header {
color: var(--text);
font-weight: 500;
display: -webkit-box;
line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -301,6 +315,7 @@ a:visited {
font-size: var(--text-base);
color: var(--text);
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -322,18 +337,38 @@ a:visited {
animation: pulse 1.5s ease-in-out infinite;
}
.loading-dot:nth-child(2) { animation-delay: 0.2s; }
.loading-dot:nth-child(3) { animation-delay: 0.4s; }
.loading-dot:nth-child(2) {
animation-delay: 0.2s;
}
.loading-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes pulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
0%,
100% {
opacity: 0.3;
}
50% {
opacity: 1;
}
}
/* HTMX Loading States */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-block; }
.htmx-request.htmx-indicator { display: inline-block; }
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline-block;
}
.htmx-request.htmx-indicator {
display: inline-block;
}
.dropdown-item.htmx-request,
.dropdown-trigger.htmx-request,
@@ -754,6 +789,7 @@ a.htmx-request {
font-size: var(--text-sm);
color: var(--text);
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -1157,6 +1193,7 @@ a.htmx-request {
margin-bottom: var(--space-xs);
line-height: 1.3;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -1280,6 +1317,7 @@ a.htmx-request {
margin-bottom: var(--space-xs);
line-height: 1.3;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -1315,8 +1353,8 @@ a.htmx-request {
color: var(--text-muted);
margin: var(--space-xs) 0 0 0;
display: -webkit-box;
line-clamp: 3;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}