diff options
Diffstat (limited to 'internal/model')
| -rw-r--r-- | internal/model/media.go | 167 | ||||
| -rw-r--r-- | internal/model/media_test.go | 27 | ||||
| -rw-r--r-- | internal/model/podcast.go | 54 | ||||
| -rw-r--r-- | internal/model/scan.go | 88 | ||||
| -rw-r--r-- | internal/model/scan_test.go | 157 |
5 files changed, 0 insertions, 493 deletions
diff --git a/internal/model/media.go b/internal/model/media.go deleted file mode 100644 index 39ae4fd..0000000 --- a/internal/model/media.go +++ /dev/null @@ -1,167 +0,0 @@ -// Package model defines the core domain entities. -package model - -import "time" - -// MediaType distinguishes between video and audio files. -type MediaType string - -const ( - // MediaTypeVideo identifies video files. - MediaTypeVideo MediaType = "video" - // MediaTypeAudio identifies audio files. - MediaTypeAudio MediaType = "audio" - // MediaTypeImage identifies image files. - MediaTypeImage MediaType = "image" -) - -// Role defines the level of access a user has to a set. -type Role string - -const ( - // RoleOwner allows browsing, uploading, deleting and thumbnail updates. - RoleOwner Role = "owner" - // RoleViewer allows browsing and playback. - RoleViewer Role = "viewer" -) - -// User represents an application account. -type User struct { - 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 `json:"id"` - Name string `json:"name"` - RootPath string `json:"root_path"` - CoverThumbnailPath string `json:"cover_thumbnail_path"` - IsPodcast bool `json:"is_podcast"` - Permissions []SetPermission `json:"permissions"` - CreatedAt time.Time `json:"created_at"` -} - -// SetPermission grants a user access to a set. -type SetPermission struct { - SetID int64 `json:"set_id"` - UserID int64 `json:"user_id"` - Role Role `json:"role"` - CreatedAt time.Time `json:"created_at"` -} - -// Media represents a single media file within a set. -type Media struct { - 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"` - Width int `json:"width"` - Height int `json:"height"` - EXIFCamera string `json:"exif_camera"` - EXIFLens string `json:"exif_lens"` - EXIFDate string `json:"exif_date"` - EXIFISO string `json:"exif_iso"` - EXIFFNumber string `json:"exif_f_number"` - EXIFExposure string `json:"exif_exposure"` - EXIFFocalLength string `json:"exif_focal_length"` - 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 `json:"id"` - Name string `json:"name"` -} - -// Session is an authenticated browser session. -type Session struct { - 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 `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 `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 `json:"user_id"` - MediaID int64 `json:"media_id"` - PositionSeconds float64 `json:"position_seconds"` - Finished bool `json:"finished"` - UpdatedAt time.Time `json:"updated_at"` -} - -// PlaybackAccumulator tracks deltas for the 60-second playback counter rule. -type PlaybackAccumulator struct { - 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 `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 `json:"media_id"` - TagID int64 `json:"tag_id"` -} - -// Metadata holds extracted file properties from ffprobe and os.Stat. -type Metadata struct { - Duration float64 `json:"duration"` - Codec string `json:"codec"` - Resolution string `json:"resolution"` - Bitrate int `json:"bitrate"` - FileSizeBytes int64 `json:"file_size_bytes"` - Width int `json:"width"` - Height int `json:"height"` - EXIFCamera string `json:"exif_camera"` - EXIFLens string `json:"exif_lens"` - EXIFDate string `json:"exif_date"` - EXIFISO string `json:"exif_iso"` - EXIFFNumber string `json:"exif_f_number"` - EXIFExposure string `json:"exif_exposure"` - EXIFFocalLength string `json:"exif_focal_length"` -} diff --git a/internal/model/media_test.go b/internal/model/media_test.go deleted file mode 100644 index 1692698..0000000 --- a/internal/model/media_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "testing" - "time" -) - -func TestStructsInstantiate(t *testing.T) { - now := time.Now() - _ = User{ID: 1, Username: "u", PasswordHash: "h", IsAdmin: true, CreatedAt: now} - _ = Set{ID: 1, Name: "s", RootPath: "/r", CreatedAt: now} - _ = Media{ID: 1, SetID: 1, RelPath: "r", FileName: "f", AbsPath: "a", Type: MediaTypeVideo, Duration: 1, DeletedAt: &now, CreatedAt: now} - _ = Tag{ID: 1, Name: "t"} - _ = Session{ID: "s", UserID: 1, ExpiresAt: now, CreatedAt: now} - max := 1 - _ = Share{Token: "t", MediaID: 1, CreatedBy: 1, CreatedAt: now, ExpiresAt: now, MaxUses: &max} - _ = Note{ID: 1, MediaID: 1, UserID: 1, Content: "c", CreatedAt: now, UpdatedAt: now} - _ = PlaybackProgress{UserID: 1, MediaID: 1, PositionSeconds: 1, Finished: true, UpdatedAt: now} - _ = PlaybackAccumulator{SessionID: "s", MediaID: 1, UpdatedAt: now} - _ = Favorite{UserID: 1, MediaID: 1, CreatedAt: now} - _ = MediaTag{MediaID: 1, TagID: 1} - _ = SetPermission{SetID: 1, UserID: 1, Role: RoleOwner, CreatedAt: now} - _ = Metadata{Duration: 1, Codec: "c", Resolution: "r", Bitrate: 1, FileSizeBytes: 1} - if MediaTypeVideo != "video" || MediaTypeAudio != "audio" || RoleOwner != "owner" || RoleViewer != "viewer" { - t.Fatal("constants mismatch") - } -} diff --git a/internal/model/podcast.go b/internal/model/podcast.go deleted file mode 100644 index f0b016d..0000000 --- a/internal/model/podcast.go +++ /dev/null @@ -1,54 +0,0 @@ -// Package model defines domain entities for podcast feeds and episodes. -package model - -import "time" - -// PodcastFeed represents a subscribed RSS/Atom feed linked to a set. -type PodcastFeed struct { - ID int64 `json:"id"` - SetID int64 `json:"set_id"` - FeedURL string `json:"feed_url"` - Title string `json:"title"` - Description string `json:"description"` - ImageURL string `json:"image_url"` - LastCheckedAt *time.Time `json:"last_checked_at"` - LastETag string `json:"last_etag"` - CheckIntervalMinutes int `json:"check_interval_minutes"` - AutoDownload bool `json:"auto_download"` - ConsecutiveFailures int `json:"consecutive_failures"` - NextCheckAt *time.Time `json:"next_check_at"` - CreatedAt time.Time `json:"created_at"` -} - -// PodcastEpisode represents an individual episode from a feed. -type PodcastEpisode struct { - ID int64 `json:"id"` - FeedID int64 `json:"feed_id"` - MediaID *int64 `json:"media_id"` - GUID string `json:"guid"` - Title string `json:"title"` - Description string `json:"description"` - PublishedAt *time.Time `json:"published_at"` - EpisodeURL string `json:"episode_url"` - DurationSeconds *float64 `json:"duration_seconds"` - FileSize *int64 `json:"file_size"` - FileName string `json:"file_name"` - IsDownloaded bool `json:"is_downloaded"` - CreatedAt time.Time `json:"created_at"` -} - -// PodcastStatus tracks per-user completion and progress for an episode. -type PodcastStatus struct { - UserID int64 `json:"user_id"` - EpisodeID int64 `json:"episode_id"` - IsCompleted bool `json:"is_completed"` - PositionSeconds float64 `json:"position_seconds"` - UpdatedAt time.Time `json:"updated_at"` -} - -// PodcastEpisodeWithStatus is a PodcastEpisode augmented with per-user status. -type PodcastEpisodeWithStatus struct { - PodcastEpisode - IsCompleted bool `json:"is_completed"` - PositionSeconds float64 `json:"position_seconds"` -} diff --git a/internal/model/scan.go b/internal/model/scan.go deleted file mode 100644 index aeec15e..0000000 --- a/internal/model/scan.go +++ /dev/null @@ -1,88 +0,0 @@ -package model - -import "sync" - -// ScanProgress tracks the state of an in-progress or recently completed scan. -type ScanProgress struct { - mu sync.RWMutex - Running bool `json:"running"` - CurrentSet string `json:"current_set,omitempty"` - SetsTotal int `json:"sets_total"` - SetsDone int `json:"sets_done"` - FilesTotal int `json:"files_total"` - FilesDone int `json:"files_done"` - LastError string `json:"last_error,omitempty"` -} - -// Start marks a scan running and resets counters for the given set count. -func (p *ScanProgress) Start(setsTotal int) { - p.mu.Lock() - defer p.mu.Unlock() - p.Running = true - p.SetsTotal = setsTotal - p.SetsDone = 0 - p.FilesTotal = 0 - p.FilesDone = 0 - p.LastError = "" -} - -// SetCurrentSet records the set currently being scanned. -func (p *ScanProgress) SetCurrentSet(name string) { - p.mu.Lock() - defer p.mu.Unlock() - p.CurrentSet = name -} - -// IncrementFile increments the completed file count. -func (p *ScanProgress) IncrementFile() { - p.mu.Lock() - defer p.mu.Unlock() - p.FilesDone++ -} - -// SetFilesTotal records the number of files to scan. -func (p *ScanProgress) SetFilesTotal(total int) { - p.mu.Lock() - defer p.mu.Unlock() - p.FilesTotal = total -} - -// AddFilesTotal adds to the total number of files discovered during the scan. -func (p *ScanProgress) AddFilesTotal(total int) { - p.mu.Lock() - defer p.mu.Unlock() - p.FilesTotal += total -} - -// IncrementSet increments the completed set count. -func (p *ScanProgress) IncrementSet() { - p.mu.Lock() - defer p.mu.Unlock() - p.SetsDone++ -} - -// Done marks the scan complete and records an error message when provided. -func (p *ScanProgress) Done(err error) { - p.mu.Lock() - defer p.mu.Unlock() - p.Running = false - p.CurrentSet = "" - if err != nil { - p.LastError = err.Error() - } -} - -// Copy returns a race-safe snapshot of the scan progress. -func (p *ScanProgress) Copy() ScanProgress { - p.mu.RLock() - defer p.mu.RUnlock() - return ScanProgress{ - Running: p.Running, - CurrentSet: p.CurrentSet, - SetsTotal: p.SetsTotal, - SetsDone: p.SetsDone, - FilesTotal: p.FilesTotal, - FilesDone: p.FilesDone, - LastError: p.LastError, - } -} diff --git a/internal/model/scan_test.go b/internal/model/scan_test.go deleted file mode 100644 index 7d370c3..0000000 --- a/internal/model/scan_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package model - -import ( - "errors" - "sync" - "testing" -) - -func TestScanProgress_Start(t *testing.T) { - var p ScanProgress - p.Start(3) - cp := p.Copy() - if !cp.Running { - t.Error("expected Running to be true") - } - if cp.SetsTotal != 3 { - t.Errorf("SetsTotal = %d, want 3", cp.SetsTotal) - } - if cp.SetsDone != 0 { - t.Errorf("SetsDone = %d, want 0", cp.SetsDone) - } - if cp.FilesTotal != 0 { - t.Errorf("FilesTotal = %d, want 0", cp.FilesTotal) - } - if cp.FilesDone != 0 { - t.Errorf("FilesDone = %d, want 0", cp.FilesDone) - } - if cp.LastError != "" { - t.Errorf("LastError = %q, want empty", cp.LastError) - } -} - -func TestScanProgress_SetCurrentSet(t *testing.T) { - var p ScanProgress - p.Start(1) - p.SetCurrentSet("movies") - cp := p.Copy() - if cp.CurrentSet != "movies" { - t.Errorf("CurrentSet = %q, want movies", cp.CurrentSet) - } -} - -func TestScanProgress_SetFilesTotal(t *testing.T) { - var p ScanProgress - p.Start(1) - p.SetFilesTotal(42) - cp := p.Copy() - if cp.FilesTotal != 42 { - t.Errorf("FilesTotal = %d, want 42", cp.FilesTotal) - } -} - -func TestScanProgress_AddFilesTotal(t *testing.T) { - var p ScanProgress - p.Start(2) - p.AddFilesTotal(10) - p.AddFilesTotal(15) - cp := p.Copy() - if cp.FilesTotal != 25 { - t.Errorf("FilesTotal = %d, want 25", cp.FilesTotal) - } -} - -func TestScanProgress_IncrementFile(t *testing.T) { - var p ScanProgress - p.Start(1) - p.IncrementFile() - cp := p.Copy() - if cp.FilesDone != 1 { - t.Errorf("FilesDone = %d, want 1", cp.FilesDone) - } -} - -func TestScanProgress_IncrementSet(t *testing.T) { - var p ScanProgress - p.Start(2) - p.IncrementSet() - cp := p.Copy() - if cp.SetsDone != 1 { - t.Errorf("SetsDone = %d, want 1", cp.SetsDone) - } -} - -func TestScanProgress_Done(t *testing.T) { - var p ScanProgress - p.Start(1) - p.Done(nil) - cp := p.Copy() - if cp.Running { - t.Error("expected Running to be false") - } - if cp.CurrentSet != "" { - t.Errorf("CurrentSet = %q, want empty", cp.CurrentSet) - } - if cp.LastError != "" { - t.Errorf("LastError = %q, want empty", cp.LastError) - } -} - -func TestScanProgress_Done_WithError(t *testing.T) { - var p ScanProgress - p.Start(1) - p.Done(errors.New("scan failed")) - cp := p.Copy() - if cp.LastError != "scan failed" { - t.Errorf("LastError = %q, want \"scan failed\"", cp.LastError) - } -} - -func TestScanProgress_Copy_Isolation(t *testing.T) { - var p ScanProgress - p.Start(1) - cp1 := p.Copy() - p.IncrementSet() - cp2 := p.Copy() - - if cp1.SetsDone != 0 { - t.Errorf("cp1.SetsDone = %d, want 0", cp1.SetsDone) - } - if cp2.SetsDone != 1 { - t.Errorf("cp2.SetsDone = %d, want 1", cp2.SetsDone) - } -} - -func TestScanProgress_ConcurrentAccess(t *testing.T) { - var p ScanProgress - p.Start(2) - p.SetFilesTotal(100) - - start := make(chan struct{}) - done := make(chan struct{}) - var copies sync.WaitGroup - go func() { - <-start - for i := 0; i < 50; i++ { - p.IncrementFile() - } - close(done) - }() - - for i := 0; i < 50; i++ { - copies.Add(1) - go func() { - defer copies.Done() - <-start - _ = p.Copy() - }() - } - close(start) - copies.Wait() - <-done - - cp := p.Copy() - if cp.FilesDone != 50 { - t.Errorf("FilesDone = %d, want 50", cp.FilesDone) - } -} |
