refactor/significant-changes #3

Merged
mkelvers merged 71 commits from refactor/significant-changes into main 2026-05-14 11:25:25 +00:00
7 changed files with 231 additions and 0 deletions
Showing only changes of commit 0d6c7613a9 - Show all commits

View File

@@ -5,6 +5,7 @@ import (
"mal/internal/auth"
"mal/internal/anime"
"mal/internal/watchlist"
"mal/internal/playback"
"mal/internal/server"
"mal/internal/templates"
@@ -20,6 +21,7 @@ func NewApp() *fx.App {
auth.Module,
anime.Module,
watchlist.Module,
playback.Module,
templates.Module,
server.Module,
fx.Decorate(func(r *templates.Renderer) render.HTMLRender {

View File

@@ -0,0 +1,18 @@
package domain
import (
"context"
"mal/internal/db"
)
type PlaybackService interface {
BuildWatchData(ctx context.Context, animeID int, titleCandidates []string, episode string, mode string, userID string) (map[string]any, error)
SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64) error
}
type PlaybackRepository interface {
GetWatchListEntry(ctx context.Context, params db.GetWatchListEntryParams) (db.WatchListEntry, error)
GetContinueWatchingEntry(ctx context.Context, params db.GetContinueWatchingEntryParams) (db.GetContinueWatchingEntryRow, error)
SaveWatchProgress(ctx context.Context, params db.SaveWatchProgressParams) error
UpsertContinueWatchingEntry(ctx context.Context, params db.UpsertContinueWatchingEntryParams) (db.ContinueWatchingEntry, error)
}

View File

@@ -0,0 +1,27 @@
package domain
import (
"context"
)
type StreamSource struct {
URL string
Quality string
}
type StreamResult struct {
URL string
Referer string
Subtitles []Subtitle
Qualities []StreamSource
}
type Subtitle struct {
URL string
Label string
}
type Provider interface {
Name() string
GetStreams(ctx context.Context, animeID int, episode string, mode string) (*StreamResult, error)
}

View File

@@ -0,0 +1,63 @@
package handler
import (
"mal/internal/domain"
"mal/internal/server"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type PlaybackHandler struct {
svc domain.PlaybackService
}
func NewPlaybackHandler(svc domain.PlaybackService) *PlaybackHandler {
return &PlaybackHandler{svc: svc}
}
func (h *PlaybackHandler) Register(r *gin.Engine) {
r.GET("/watch/:id", h.HandleWatchPage)
r.POST("/api/watch-progress", h.HandleSaveProgress)
}
func (h *PlaybackHandler) HandleWatchPage(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
ep := c.DefaultQuery("ep", "1")
mode := c.DefaultQuery("mode", "sub")
userID := "" // TODO: get from auth context
data, err := h.svc.BuildWatchData(c.Request.Context(), id, []string{}, ep, mode, userID)
if err != nil {
c.Status(http.StatusNotFound)
return
}
c.HTML(http.StatusOK, "watch.gohtml", gin.H{
"WatchData": data,
"CurrentPath": c.Request.URL.Path,
})
}
func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) {
userID := "" // TODO: get from auth context
var req struct {
MalID int64 `json:"mal_id"`
Episode int `json:"episode"`
TimeSeconds float64 `json:"time_seconds"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.Status(http.StatusBadRequest)
return
}
err := h.svc.SaveProgress(c.Request.Context(), userID, req.MalID, req.Episode, req.TimeSeconds)
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
c.Status(http.StatusOK)
}

View File

@@ -0,0 +1,24 @@
package playback
import (
"mal/internal/domain"
"mal/internal/playback/handler"
"mal/internal/playback/repository"
"mal/internal/playback/service"
"mal/internal/server"
"go.uber.org/fx"
)
var Module = fx.Options(
fx.Provide(
repository.NewPlaybackRepository,
service.NewPlaybackService,
handler.NewPlaybackHandler,
),
fx.Provide(
server.AsRouteRegister(func(h *handler.PlaybackHandler) server.RouteRegister {
return h
}),
),
)

View File

@@ -0,0 +1,31 @@
package repository
import (
"context"
"mal/internal/db"
"mal/internal/domain"
)
type playbackRepository struct {
queries *db.Queries
}
func NewPlaybackRepository(queries *db.Queries) domain.PlaybackRepository {
return &playbackRepository{queries: queries}
}
func (r *playbackRepository) GetWatchListEntry(ctx context.Context, params db.GetWatchListEntryParams) (db.WatchListEntry, error) {
return r.queries.GetWatchListEntry(ctx, params)
}
func (r *playbackRepository) GetContinueWatchingEntry(ctx context.Context, params db.GetContinueWatchingEntryParams) (db.GetContinueWatchingEntryRow, error) {
return r.queries.GetContinueWatchingEntry(ctx, params)
}
func (r *playbackRepository) SaveWatchProgress(ctx context.Context, params db.SaveWatchProgressParams) error {
return r.queries.SaveWatchProgress(ctx, params)
}
func (r *playbackRepository) UpsertContinueWatchingEntry(ctx context.Context, params db.UpsertContinueWatchingEntryParams) (db.ContinueWatchingEntry, error) {
return r.queries.UpsertContinueWatchingEntry(ctx, params)
}

View File

@@ -0,0 +1,66 @@
package service
import (
"context"
"fmt"
"mal/internal/db"
"mal/internal/domain"
"strconv"
)
type playbackService struct {
repo domain.PlaybackRepository
providers []domain.Provider
}
func NewPlaybackService(repo domain.PlaybackRepository, providers []domain.Provider) domain.PlaybackService {
return &playbackService{repo: repo, providers: providers}
}
func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, titleCandidates []string, episode string, mode string, userID string) (map[string]any, error) {
// Minimal implementation for now to show the pattern
var result *domain.StreamResult
var err error
for _, p := range s.providers {
result, err = p.GetStreams(ctx, animeID, episode, mode)
if err == nil && result != nil {
break
}
}
if result == nil {
return nil, fmt.Errorf("no streams found")
}
startTime := 0.0
if userID != "" {
entry, err := s.repo.GetWatchListEntry(ctx, db.GetWatchListEntryParams{
UserID: userID,
AnimeID: int64(animeID),
})
if err == nil {
if entry.CurrentEpisode.Valid && strconv.FormatInt(entry.CurrentEpisode.Int64, 10) == episode {
startTime = entry.CurrentTimeSeconds
}
}
}
return map[string]any{
"URL": result.URL,
"Referer": result.Referer,
"StartTime": startTime,
"Subtitles": result.Subtitles,
"Qualities": result.Qualities,
}, nil
}
func (s *playbackService) SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64) error {
params := db.SaveWatchProgressParams{
UserID: userID,
AnimeID: animeID,
CurrentEpisode: sql.NullInt64{Int64: int64(episode), Valid: true},
CurrentTimeSeconds: timeSeconds,
}
return s.repo.SaveWatchProgress(ctx, params)
}