summaryrefslogtreecommitdiff
path: root/internal/service
AgeCommit message (Collapse)Author
2026-05-17Restructure repo: move Go server into player-server/Paul Buetow
2026-05-17Add progress service completion methodsPaul Buetow
2026-05-10Decompose podcastService (SRP): split subscription, episode, and ↵Paul Buetow
feed-checker into sub-services Extract three role-focused sub-services from the monolithic podcastService: - podcast_sub.go: podcastSubscriptionService handles SubscribeFeed, ListFeeds, EditFeed, UnsubscribeFeed and podcast set lifecycle. - podcast_episode.go: podcastEpisodeService handles ListEpisodes, DownloadEpisode, ToggleEpisodeComplete and enclosure downloading. - podcast_checker.go: podcastFeedChecker handles CheckFeeds, conditional GET, backoff and episode upserting. - podcast_helpers.go: shared helpers (podcastFolderName, sanitizeSetName, sanitizeFilename). podcast.go becomes a thin composite that embeds the three sub-services and exposes a PodcastEpisodeService-compatible interface, preserving existing API-layer wiring and tests.
2026-05-10Move self-deletion guard from handler into AdminService (DeleteUser) to fix ↵Paul Buetow
SoC violation - Add ErrCannotDeleteSelf sentinel error in service layer. - Change DeleteUser signature to (ctx, callerID, id) across all layers. - Move self-deletion guard from handleDeleteUser handler into userAdminService.DeleteUser. - Update handleError to map ErrCannotDeleteSelf to 400 BadRequest. - Adjust all affected tests to use the new signature.
2026-05-10refactor(service,scanner): return concrete types from constructorsPaul Buetow
Apply the Go best-practice convention 'accept interfaces, return concrete types' across the service and scanner packages: - NewBrowseService -> *browseService - NewWriteService -> *writeService - NewMediaStreamer -> *mediaStreamer - NewFSScanner -> *FSScanner - NewFSScannerWithLogger -> *FSScanner - NewPodcastBrowseService -> *podcastBrowseService - NewTagService -> *tagService - NewShareService -> *shareService - NewProgressService -> *progressService - NewAdminService -> *adminService - NewAdminServiceWithLogger -> *adminService - NewAuthService -> *authService - NewNoteService -> *noteService - NewFavService -> *favService - NewMediaService -> *mediaService - NewMediaServiceWithPodcastBrowser -> *mediaService Callers continue to work unchanged because Go allows assigning a concrete type to an interface variable. All tests pass with -race -cover.
2026-05-10fix(browse): return ErrNotFound when set cover is missingPaul Buetow
GetSetCover was returning a stale os.Stat wrapped error when no thumbnail candidate was found (line 465), and was not translating os.IsNotExist to ErrNotFound when the candidate thumbnail was missing (line 468). Both cases now return service.ErrNotFound so the caller/handler can respond with 404 instead of 500.
2026-05-10internal/service: stop swallowing CreateEpisode errors in ↵Paul Buetow
insertPodcastEpisodes and upsertFeedEpisodes - insertPodcastEpisodes now returns an error when CreateEpisode fails so callers (e.g. SubscribeFeed) can log it instead of silently dropping episodes. - upsertFeedEpisodes now logs per-episode lookup/insert failures, collects the failed GUIDs, and returns a summary error so the background CheckFeeds caller can log a warning while continuing to process the rest of the feed. All tests pass with -race -cover.
2026-05-10podcast: propagate UpdateFeed error on 304 Not Modified pathPaul Buetow
Previously checkFeed silently discarded the DB update error when a feed returned 304 Not Modified. If the update failed, LastCheckedAt was not persisted, causing the next background check to re-fetch the same unchanged feed unnecessarily. Now the error is propagated back to CheckFeeds so it is logged like any other feed-check failure.
2026-05-10player: log downloadCover errors in podcast subscribe and feed checkPaul Buetow
2026-05-10internal/service: fix ListEpisodes global pagination by using cross-feed SQL ↵Paul Buetow
query Previously ListEpisodes requested per-feed episodes with the full limit from every feed, concatenated them, and then sliced the resulting slice in memory. This silently skipped episodes from deeper pages because the per-feed DB limit never returned them. Fix: Add a new repository method ListEpisodesByFeedIDsWithStatus that takes a slice of feed IDs and applies LIMIT/OFFSET globally in a single SQL query (IN (...)). The service builds the feed ID list and delegates pagination to the database instead of emulating it in memory. Files changed: - internal/repository/podcast.go: add ListEpisodesByFeedIDsWithStatus - internal/repository/podcast_repo.go: extend PodcastRepo interface - internal/repository/mock.go: add mock implementation - internal/service/podcast.go: replace per-feed loop with new method - internal/service/podcast_test.go: add tests for ListEpisodes - internal/repository/podcast_test.go: add integration tests for new repo method - internal/api/handlers_test.go: add mockPingStore method
2026-05-10Add circuit breaker / backoff for failing podcast feeds in CheckFeedsPaul Buetow
- DB: add consecutive_failures and next_check_at to podcast_feeds - model: add ConsecutiveFailures and NextCheckAt to PodcastFeed struct - repository: update CRUD queries to handle new columns and ListFeedsNeedingCheck to skip feeds with future next_check_at - service: update CheckFeeds to use new query, update checkFeed to reset counters on success and increment exponential backoff on failure - tests: adapt mock signatures, add backoff integration test
2026-05-10fix: close destination file and clean up partial copies in copyFilePaul Buetow
- Add defer out.Close() after os.Create in copyFile to prevent fd leak. - Remove redundant explicit out.Close() calls in error and success paths. - Best-effort remove partially written destination file on io.Copy error.
2026-05-10internal/service: swap GC hard-delete orderingPaul Buetow
Remove the media file from disk before deleting the DB row so that a failed os.Remove doesn't create an orphaned file. If the file is already missing (os.IsNotExist), proceed with the DB deletion. Other removal errors abort the iteration for that item. Also update the existing ordering test to assert removal happens before HardDeleteMedia, and add a test covering the file-remove-failure short-circuit.
2026-05-10Decouple podcast browse logic from BrowseSet via PodcastBrowser interfacePaul Buetow
Extract PodcastBrowser interface and podcastBrowseService implementation to move podcast-specific grid augmentation out of browseService.BrowseSet. - Remove PodcastRepo from BrowseServiceStore and MediaServiceStore. - browseService now delegates podcast set augmentation to an injected PodcastBrowser strategy (kept optional/nil-safe). - Add NewMediaServiceWithPodcastBrowser to wire a real PodcastBrowser (backed by PodcastRepo) in production while keeping the plain NewMediaService for existing callers/tests. - Update main.go to instantiate NewPodcastBrowseService and inject it. - Update browse_test.go to test PodcastBrowser-augmented paths through the new constructor signature. This keeps BrowseSet focused on files and folders only and removes the podcast dependency from the browse contract.
2026-05-09style: gofmt and goimports formatting fixesPaul Buetow
2026-05-09Extract SessionManager interface to fix DIP violationPaul Buetow
- Introduce auth.SessionManager interface in internal/auth/interfaces.go - Rename concrete struct to sessionManager (unexported) and have it satisfy interface - Update api/service layers to depend on interface, not concrete type - Add MockSessionManager for testing - Update all call sites and tests to use interface
2026-05-09Harden external process and network callsPaul Buetow
- Add cmd.WaitDelay (10-15s) to ffprobe, ffmpeg remux, and thumbnail generation. - Add bounded retries with exponential backoff to FFProber.Probe (max 3 attempts). - Inject *http.Client into podcast ParseFeed, replacing gofeed's default client. - Update all callers (podcast service, tests, integration tests) to pass client. - Ensure go test ./... -race -cover passes.
2026-05-09Handle background worker panics for task 42Paul Buetow
2026-05-09more on thisPaul Buetow
2026-05-09s1 fix immediate scan progress statePaul Buetow
2026-05-09Refine media browsing and set coversPaul Buetow
2026-05-07k1 avoid duplicate downloaded podcast episodesPaul Buetow
2026-05-07Fix podcast browser workflows for k1Paul Buetow
2026-05-07i1 omit empty share thumbnail URLPaul Buetow
2026-05-07Fix missing share thumbnails for i1Paul Buetow
2026-05-07j1 fix admin media lifecycle QA defectsPaul Buetow
2026-05-07Fix scan cancellation progress error for q0Paul Buetow
2026-05-07task 31: introduce media streamer servicePaul Buetow
2026-05-07Task 21: move thumbnail regeneration to write servicePaul Buetow
2026-05-07task 81: replace test sleeps with deterministic syncPaul Buetow
2026-05-06internal/service: inject http.Client into podcastService constructor (task 41)Paul Buetow
2026-05-06internal/service: log and concurrently process podcast feeds (task 91)Paul Buetow
- Add *slog.Logger to podcastService with NewPodcastServiceWithLogger. - Wire logger from cmd/player main.go. - Run CheckFeeds concurrently: one goroutine per feed with WaitGroup. - Add structured logging for start/finish per feed and overall count. - Log feed failures at Warn level so they are visible but not fatal. - Ensure nil logger safety with slog.Default fallback. - Add tests: empty list, concurrent OK, list error, single feed error, all feeds error.
2026-05-06split DownloadEpisode into smaller helpers (x0)Paul Buetow
Refactor ~112-line DownloadEpisode into four focused helpers: - resolveEpisodeAndSet: permission + path building - buildEpisodePath: filename logic - downloadEnclosure: HTTP fetch to disk - persistDownloadedEpisode: DB + probe + cleanup Also: pass ctx through to HTTP request via NewRequestWithContext. Added tests for happy path, 404, and missing episode/feed/set.
2026-05-06w0: Split SubscribeFeed into smaller helpersPaul Buetow
SubscribeFeed was ~100 lines and did too much: admin check, feed parsing, filesystem setup, DB set creation, permission grant, feed creation, cover download, episode insertion, and marking checked. Refactored into focused helpers: - verifyAdmin – admin gate - resolveSetPath – sanitize name and build disk path - createPodcastSet – mkdir, create set row, grant owner perm - rollbackSet – delete set row + remove directory - createPodcastFeed – insert feed row - insertPodcastEpisodes – bulk insert parsed episodes Injected dependency fields (parseFeed, parseFeedReader, downloadCover) into podcastService so unit tests can replace them with fakes instead of hitting real HTTP or disk. Added internal/service/podcast_test.go with coverage for: - happy path SubscribeFeed - non-admin rejection - user lookup error - feed parse error - set creation error (directory cleaned up) - grant permission error (rollback) - feed creation error (rollback) - resolveSetPath / sanitize helpers - DownloadEpisode permission check - upsertFeedEpisodes (new + skip existing) - updateFeedFromParsed - insertPodcastEpisodes
2026-05-06fix(scanner, browse): fallback to original image when thumbnail generation ↵Paul Buetow
fails for cover.jpg When scanning image files (e.g. cover.jpg in audiobook folders), thumbnailForImage can fail or produce a missing .thumbnails/cover.jpg. This caused broken thumbnails in the grid. - scanner.go buildThumbnailPath: after generating an image thumbnail, verify the output file exists via fs.Stat. If it does not exist, fall back to using the original image path as the thumbnail_path. - browse.go GetThumbnail: if os.Stat(ThumbnailPath) fails and the media type is image, fall back to serving the original AbsPath. All tests pass.
2026-05-06Fix music video duration display in progress barPaul Buetow
2026-05-05Fix podcast support critical bugs from code reviewPaul Buetow
- Fix boolean scanning from SQLite INTEGER columns (intToBool helper) - Fix BrowseSet LIMIT 0 returning zero episodes (use 1000 instead) - Fix checkFeed double-fetch by parsing from resp.Body directly + Add ParseFeedReader for body reuse - Fix file handle leak in DownloadEpisode (explicit Close before ImportMediaFile) - Fix incomplete rollback in DownloadEpisode (remove file + delete media row) - Fix 204 No Content handler writing 'null' body - Fix DownloadCoverImage to accept *http.Client with timeout - Deduplicate uniqueFilename into shared internal/service/filename.go - Wire frontend renderPodcastEpisodes into renderBrowse from API data - Refactor podcasts.js: remove extra API call, fix toggle toast, remove duplicate toast - Add Config.PodcastCheckMinutes + PODCAST_CHECK_INTERVAL_MINUTES env var - Start background CheckFeeds goroutine in main.go with ticker - Update NewPodcastService to accept checkInterval parameter - Log errors from CheckFeeds and episode creation instead of silently discarding
2026-05-05Add podcast support backend: RSS/Atom feeds, episode management, cover scrapingPaul Buetow
2026-05-04task f: validate media_id and sessionID in progress handler and servicePaul Buetow
2026-05-04task e: add unit test verifying GetNote access check rejects non-ownerPaul Buetow
2026-05-04task 7: refactor oversized functions into helpersPaul Buetow
Refactored the following functions per CLAUDE.md (50+ line limit): - internal/scanner/scanner.go: scanSet (192 → 56 lines) Extracted: ensureSet, loadExistingMedia, gatherCoverImages, thumbnailForVideo, thumbnailForImage, buildThumbnailPath, processNewFile, updateAudioThumbnails - internal/repository/migrate.go: Migrate (134 → 23 lines) Extracted: enableForeignKeys, execSchema; split schema into tablesSchema and indexesSchema constants - internal/config.go: LoadConfig (85 → 28 lines) Extracted: defaultConfig, loadNumericSettings, loadStringSettings, loadLogLevel, loadSecureCookies; added validLogLevels variable - cmd/mediaplayer/main.go: run / runWithSignal (114 → 26 / 26 lines) Extracted: parseVersionFlag, buildLogger, wireDeps, runServer, ensureSignalChannel, shutdownGracefully; introduced appDeps struct - internal/api/server.go: routes (96 → 8 lines) Extracted per route group: routesPublic, routesSharePublic, routesStatic, routesHTML, routesAuth, routesSets, routesMedia, routesNotes, routesProgress, routesShares, routesAdmin Added helpers: requireSession, requireAdmin, publicMethod - internal/service/browse.go: BrowseSet (85 → 26 lines) Extracted: prefixForParent, classifyMediaPath, buildFolderMap, folderHasCover, buildFolders All public interfaces remain unchanged. Tests pass: go test ./... -race -cover
2026-05-04task 9: Centralize file extension and media type mappings in internal/mediatypePaul Buetow
Create a new internal/mediatype package with a single source of truth for: - Extension-to-media-type mappings (TypeForExt) - Extension-to-MIME mappings (MIMETypeForExt) - Supported extension checks (IsSupportedExt, IsImageExt, IsCoverImageExt) Replace duplicated logic across: - internal/service/service.go (supportedExtensions, guessMediaType) - internal/scanner/scanner.go (mediaExtensions, imageExtensions, mediaTypeFromExt) - internal/api/handlers.go (probe.MimeTypeForFilename) - internal/probe/probe.go (imageExtensions, isImagePath) - internal/probe/remux.go (MimeTypeForFilename) Divergent defaults unified: both scanner and service now default unknown extensions to video (model.MediaTypeVideo) via mediatype.TypeForExt. Update tests to use mediatype package and remove obsolete scanner tests. Update AGENTS.md to reflect the new package.
2026-05-04task c: run gofmt and fix unwrapped error literals across the codebasePaul Buetow
2026-05-04task 3: decouple API layer from repository with MediaQueryFilter and ↵Paul Buetow
AuthService abstraction
2026-05-04task b: inject app context into AdminService and propagate cancellation to ↵Paul Buetow
background scans
2026-05-03task 2: add unit tests for decomposed sub-servicesPaul Buetow
2026-05-03task 2 review fixes: narrow BrowseService and accessHelper store interfacesPaul Buetow
2026-05-03task 2: decompose mediaService and adminService into role-focused structsPaul Buetow
Extract accessHelper and split mediaService into: - browseService (read-only browsing, streaming, thumbnails) - writeService (upload, soft-delete, restore) - shareService (share links) - tagService (tagging) - favService (favorites) - noteService (notes) Split adminService into: - trashService (list deleted media) - scanService (trigger rescan, scan progress) - userAdminService (create/list/delete users) - permissionAdminService (grant/revoke/list permissions) Add repository sub-interfaces for each service. Add negative tests for share sub-service.
2026-05-03Task 8: standardize scanner and handler loggingPaul Buetow
2026-05-03Fix GC hard-delete ordering for task dPaul Buetow