feat: migrate playback module to modular domain pattern
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"mal/internal/auth"
|
"mal/internal/auth"
|
||||||
"mal/internal/anime"
|
"mal/internal/anime"
|
||||||
"mal/internal/watchlist"
|
"mal/internal/watchlist"
|
||||||
|
"mal/internal/playback"
|
||||||
"mal/internal/server"
|
"mal/internal/server"
|
||||||
"mal/internal/templates"
|
"mal/internal/templates"
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ func NewApp() *fx.App {
|
|||||||
auth.Module,
|
auth.Module,
|
||||||
anime.Module,
|
anime.Module,
|
||||||
watchlist.Module,
|
watchlist.Module,
|
||||||
|
playback.Module,
|
||||||
templates.Module,
|
templates.Module,
|
||||||
server.Module,
|
server.Module,
|
||||||
fx.Decorate(func(r *templates.Renderer) render.HTMLRender {
|
fx.Decorate(func(r *templates.Renderer) render.HTMLRender {
|
||||||
|
|||||||
18
internal/domain/playback.go
Normal file
18
internal/domain/playback.go
Normal 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)
|
||||||
|
}
|
||||||
27
internal/domain/provider.go
Normal file
27
internal/domain/provider.go
Normal 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)
|
||||||
|
}
|
||||||
63
internal/playback/handler/handler.go
Normal file
63
internal/playback/handler/handler.go
Normal 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)
|
||||||
|
}
|
||||||
24
internal/playback/module.go
Normal file
24
internal/playback/module.go
Normal 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
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
31
internal/playback/repository/repository.go
Normal file
31
internal/playback/repository/repository.go
Normal 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)
|
||||||
|
}
|
||||||
66
internal/playback/service/service.go
Normal file
66
internal/playback/service/service.go
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user