summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/api/handlers_test.go51
-rw-r--r--internal/model/media.go138
-rw-r--r--internal/service/media_test.go13
-rw-r--r--internal/service/service.go18
4 files changed, 136 insertions, 84 deletions
diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go
index 99c009b..e65b854 100644
--- a/internal/api/handlers_test.go
+++ b/internal/api/handlers_test.go
@@ -705,17 +705,28 @@ func TestServer_MediaList(t *testing.T) {
func TestServer_MediaDetail(t *testing.T) {
tests := []struct {
- name string
- id string
- result *service.MediaDetail
- err error
- wantCode int
- wantMedia bool
+ name string
+ id string
+ result *service.MediaDetail
+ err error
+ wantCode int
+ wantMedia bool
+ wantResumeFrom float64
+ wantProgressNil bool
}{
- {"ok", "42", &service.MediaDetail{Media: &model.Media{ID: 42, FileName: "a.mp4"}}, nil, http.StatusOK, true},
- {"invalid id", "abc", nil, nil, http.StatusBadRequest, false},
- {"not found", "7", nil, nil, http.StatusNotFound, false},
- {"service error", "7", nil, errors.New("boom"), http.StatusInternalServerError, false},
+ {
+ name: "ok with progress",
+ id: "42",
+ result: &service.MediaDetail{Media: &model.Media{ID: 42, FileName: "a.mp4"}, Progress: &model.PlaybackProgress{UserID: 1, MediaID: 42, PositionSeconds: 77}},
+ err: nil,
+ wantCode: http.StatusOK,
+ wantMedia: true,
+ wantResumeFrom: 77,
+ },
+ {"ok without progress", "42", &service.MediaDetail{Media: &model.Media{ID: 42, FileName: "a.mp4"}}, nil, http.StatusOK, true, 0, true},
+ {"invalid id", "abc", nil, nil, http.StatusBadRequest, false, 0, false},
+ {"not found", "7", nil, nil, http.StatusNotFound, false, 0, false},
+ {"service error", "7", nil, errors.New("boom"), http.StatusInternalServerError, false, 0, false},
}
for _, tt := range tests {
@@ -736,6 +747,26 @@ func TestServer_MediaDetail(t *testing.T) {
if rr.Code != tt.wantCode {
t.Fatalf("expected %d, got %d", tt.wantCode, rr.Code)
}
+ if tt.wantCode != http.StatusOK {
+ return
+ }
+ var resp struct {
+ Media *model.Media `json:"media"`
+ Progress *model.PlaybackProgress `json:"progress"`
+ }
+ if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("unmarshal detail: %v", err)
+ }
+ if tt.wantMedia && resp.Media == nil {
+ t.Fatal("expected media in response")
+ }
+ var gotResume float64
+ if resp.Progress != nil {
+ gotResume = resp.Progress.PositionSeconds
+ }
+ if gotResume != tt.wantResumeFrom {
+ t.Fatalf("expected resume_from %v, got %v", tt.wantResumeFrom, gotResume)
+ }
})
}
}
diff --git a/internal/model/media.go b/internal/model/media.go
index 1cb2949..b1b4ef3 100644
--- a/internal/model/media.go
+++ b/internal/model/media.go
@@ -21,121 +21,121 @@ const (
// User represents an application account.
type User struct {
- ID int64
- Username string
- PasswordHash string
- IsAdmin bool
- CreatedAt time.Time
+ ID int64 `json:"id"`
+ Username string `json:"username"`
+ PasswordHash string `json:"-"`
+ IsAdmin bool `json:"is_admin"`
+ CreatedAt time.Time `json:"created_at"`
}
// Set represents a top-level media collection (a directory under MEDIA_ROOT).
type Set struct {
- ID int64
- Name string
- RootPath string
- CoverThumbnailPath string
- Permissions []SetPermission
- CreatedAt time.Time
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ RootPath string `json:"root_path"`
+ CoverThumbnailPath string `json:"cover_thumbnail_path"`
+ Permissions []SetPermission `json:"permissions"`
+ CreatedAt time.Time `json:"created_at"`
}
// SetPermission grants a user access to a set.
type SetPermission struct {
- SetID int64
- UserID int64
- Role Role
- CreatedAt time.Time
+ SetID int64 `json:"set_id"`
+ UserID int64 `json:"user_id"`
+ Role Role `json:"role"`
+ CreatedAt time.Time `json:"created_at"`
}
// Media represents a single audio or video file within a set.
type Media struct {
- ID int64
- SetID int64
- RelPath string
- FileName string
- AbsPath string
- Type MediaType
- Duration float64
- Codec string
- Resolution string
- Bitrate int
- FileSizeBytes int64
- ThumbnailPath string
- PlayCount int
- DeletedAt *time.Time
- CreatedAt time.Time
+ ID int64 `json:"id"`
+ SetID int64 `json:"set_id"`
+ RelPath string `json:"rel_path"`
+ FileName string `json:"file_name"`
+ AbsPath string `json:"abs_path"`
+ Type MediaType `json:"type"`
+ Duration float64 `json:"duration"`
+ Codec string `json:"codec"`
+ Resolution string `json:"resolution"`
+ Bitrate int `json:"bitrate"`
+ FileSizeBytes int64 `json:"file_size_bytes"`
+ ThumbnailPath string `json:"thumbnail_path"`
+ PlayCount int `json:"play_count"`
+ DeletedAt *time.Time `json:"deleted_at"`
+ CreatedAt time.Time `json:"created_at"`
}
// Tag is a label that can be attached to media items.
type Tag struct {
- ID int64
- Name string
+ ID int64 `json:"id"`
+ Name string `json:"name"`
}
// Session is an authenticated browser session.
type Session struct {
- ID string
- UserID int64
- ExpiresAt time.Time
- CreatedAt time.Time
+ ID string `json:"id"`
+ UserID int64 `json:"user_id"`
+ ExpiresAt time.Time `json:"expires_at"`
+ CreatedAt time.Time `json:"created_at"`
}
// Share is a time-bounded public link to a media item.
type Share struct {
- Token string
- MediaID int64
- CreatedBy int64
- CreatedAt time.Time
- ExpiresAt time.Time
- MaxUses *int
- UsedCount int
+ Token string `json:"token"`
+ MediaID int64 `json:"media_id"`
+ CreatedBy int64 `json:"created_by"`
+ CreatedAt time.Time `json:"created_at"`
+ ExpiresAt time.Time `json:"expires_at"`
+ MaxUses *int `json:"max_uses"`
+ UsedCount int `json:"used_count"`
}
// Note is a per-user, per-media text note.
type Note struct {
- ID int64
- MediaID int64
- UserID int64
- Content string
- CreatedAt time.Time
- UpdatedAt time.Time
+ ID int64 `json:"id"`
+ MediaID int64 `json:"media_id"`
+ UserID int64 `json:"user_id"`
+ Content string `json:"content"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
}
// PlaybackProgress stores the last known playback position.
type PlaybackProgress struct {
- UserID int64
- MediaID int64
- PositionSeconds float64
- UpdatedAt time.Time
+ UserID int64 `json:"user_id"`
+ MediaID int64 `json:"media_id"`
+ PositionSeconds float64 `json:"position_seconds"`
+ UpdatedAt time.Time `json:"updated_at"`
}
// PlaybackAccumulator tracks deltas for the 60-second playback counter rule.
type PlaybackAccumulator struct {
- SessionID string
- MediaID int64
- LastPosition float64
- AccumulatedSeconds float64
- Counted bool
- UpdatedAt time.Time
+ SessionID string `json:"session_id"`
+ MediaID int64 `json:"media_id"`
+ LastPosition float64 `json:"last_position"`
+ AccumulatedSeconds float64 `json:"accumulated_seconds"`
+ Counted bool `json:"counted"`
+ UpdatedAt time.Time `json:"updated_at"`
}
// Favorite records that a user has favorited a media item.
type Favorite struct {
- UserID int64
- MediaID int64
- CreatedAt time.Time
+ UserID int64 `json:"user_id"`
+ MediaID int64 `json:"media_id"`
+ CreatedAt time.Time `json:"created_at"`
}
// MediaTag is the join table between media and tags.
type MediaTag struct {
- MediaID int64
- TagID int64
+ MediaID int64 `json:"media_id"`
+ TagID int64 `json:"tag_id"`
}
// Metadata holds extracted file properties from ffprobe and os.Stat.
type Metadata struct {
- Duration float64
- Codec string
- Resolution string
- Bitrate int
- FileSizeBytes int64
+ Duration float64 `json:"duration"`
+ Codec string `json:"codec"`
+ Resolution string `json:"resolution"`
+ Bitrate int `json:"bitrate"`
+ FileSizeBytes int64 `json:"file_size_bytes"`
}
diff --git a/internal/service/media_test.go b/internal/service/media_test.go
index 8dccef9..e3e7b42 100644
--- a/internal/service/media_test.go
+++ b/internal/service/media_test.go
@@ -242,6 +242,19 @@ func TestMediaService_GetMediaDetail(t *testing.T) {
if detail.Media.ID != tt.mediaID {
t.Fatalf("unexpected media id %d", detail.Media.ID)
}
+ if tt.progress != nil {
+ if detail.Progress == nil {
+ t.Fatal("expected progress in detail")
+ }
+ if detail.Progress.PositionSeconds != tt.progress.PositionSeconds {
+ t.Fatalf("expected position %v, got %v", tt.progress.PositionSeconds, detail.Progress.PositionSeconds)
+ }
+ if detail.ResumeFrom() != tt.progress.PositionSeconds {
+ t.Fatalf("expected ResumeFrom %v, got %v", tt.progress.PositionSeconds, detail.ResumeFrom())
+ }
+ } else if detail.Progress != nil {
+ t.Fatal("unexpected progress in detail")
+ }
})
}
}
diff --git a/internal/service/service.go b/internal/service/service.go
index 59f4e7d..467aaf6 100644
--- a/internal/service/service.go
+++ b/internal/service/service.go
@@ -62,9 +62,17 @@ type FileResult struct {
// MediaDetail combines media with related data.
type MediaDetail struct {
- Media *model.Media
- Tags []model.Tag
- Favorite bool
- Note *model.Note
- Progress *model.PlaybackProgress
+ Media *model.Media `json:"media"`
+ Tags []model.Tag `json:"tags"`
+ Favorite bool `json:"favorite"`
+ Note *model.Note `json:"note,omitempty"`
+ Progress *model.PlaybackProgress `json:"progress,omitempty"`
+}
+
+// ResumeFrom returns the saved playback position in seconds, or 0 if none.
+func (d *MediaDetail) ResumeFrom() float64 {
+ if d.Progress != nil {
+ return d.Progress.PositionSeconds
+ }
+ return 0
}