refactor/significant-changes #3
@@ -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 {
|
||||
|
||||
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