From 914bd7cd6aa14e839332a98d91c30b19865b0cf2 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 17 May 2026 15:25:52 +0300 Subject: Restructure repo: move Go server into player-server/ --- AGENTS.md | 256 --- Dockerfile | 37 - Magefile.go | 76 - README.md | 71 - cmd/player/main.go | 269 --- cmd/player/main_test.go | 285 --- docs/admin.md | 50 - docs/api.md | 84 - docs/configuration.md | 19 - docs/keyboard-shortcuts.md | 33 - docs/podcasts.md | 31 - docs/quick-start.md | 56 - docs/theming.md | 53 - go.mod | 31 - go.sum | 88 - internal/api/handlers.go | 193 -- internal/api/handlers_admin.go | 146 -- internal/api/handlers_auth.go | 126 -- internal/api/handlers_config.go | 17 - internal/api/handlers_file.go | 109 - internal/api/handlers_media.go | 538 ----- internal/api/handlers_more_test.go | 2191 ------------------- internal/api/handlers_podcast.go | 161 -- internal/api/handlers_podcast_test.go | 370 ---- internal/api/handlers_share.go | 215 -- internal/api/handlers_test.go | 1673 -------------- internal/api/middleware.go | 116 - internal/api/server.go | 296 --- internal/api/server_test.go | 19 - internal/auth/auth.go | 41 - internal/auth/auth_test.go | 184 -- internal/auth/interfaces.go | 15 - internal/auth/mock.go | 50 - internal/auth/mock_test.go | 61 - internal/auth/session.go | 85 - internal/clock/clock.go | 28 - internal/clock/clock_test.go | 24 - internal/config.go | 209 -- internal/config_test.go | 209 -- internal/mediatype/mediatype.go | 129 -- internal/mediatype/mediatype_test.go | 163 -- internal/model/media.go | 167 -- internal/model/media_test.go | 27 - internal/model/podcast.go | 54 - internal/model/scan.go | 88 - internal/model/scan_test.go | 157 -- internal/podcast/cover.go | 39 - internal/podcast/feed.go | 137 -- internal/podcast/podcast_test.go | 560 ----- internal/probe/image_test.go | 232 -- internal/probe/probe.go | 242 -- internal/probe/probe_test.go | 126 -- internal/probe/remux.go | 119 - internal/probe/remux_test.go | 85 - internal/probe/testdata/golden.json | 14 - internal/repository/favorite.go | 62 - internal/repository/media.go | 319 --- internal/repository/mock.go | 1194 ---------- internal/repository/note.go | 48 - internal/repository/playback_accumulator.go | 47 - internal/repository/playback_progress.go | 122 -- internal/repository/podcast.go | 417 ---- internal/repository/podcast_repo.go | 52 - internal/repository/podcast_test.go | 311 --- internal/repository/repository.go | 321 --- internal/repository/schema.go | 262 --- internal/repository/session.go | 60 - internal/repository/set.go | 84 - internal/repository/set_permission.go | 87 - internal/repository/share.go | 116 - internal/repository/sqlite.go | 77 - internal/repository/sqlite_no_rows_test.go | 132 -- internal/repository/sqlite_test.go | 1925 ---------------- internal/repository/tag.go | 110 - internal/repository/user.go | 94 - internal/scanner/fs.go | 28 - internal/scanner/scanner.go | 516 ----- internal/scanner/scanner_test.go | 610 ------ internal/service/access.go | 117 - internal/service/admin.go | 81 - internal/service/admin_test.go | 506 ----- internal/service/auth.go | 96 - internal/service/browse.go | 523 ----- internal/service/browse_test.go | 179 -- internal/service/fav.go | 28 - internal/service/fav_test.go | 67 - internal/service/filename.go | 31 - internal/service/gc.go | 169 -- internal/service/gc_test.go | 322 --- internal/service/import.go | 94 - internal/service/media.go | 47 - internal/service/media_test.go | 2304 -------------------- internal/service/mock.go | 442 ---- internal/service/mock_test.go | 176 -- internal/service/no_rows_test.go | 190 -- internal/service/note.go | 47 - internal/service/note_test.go | 114 - internal/service/perm.go | 62 - internal/service/perm_test.go | 61 - internal/service/podcast.go | 121 - internal/service/podcast_checker.go | 179 -- internal/service/podcast_episode.go | 246 --- internal/service/podcast_helpers.go | 35 - internal/service/podcast_sub.go | 296 --- internal/service/podcast_test.go | 995 --------- internal/service/progress.go | 164 -- internal/service/progress_test.go | 387 ---- internal/service/recover.go | 18 - internal/service/scan.go | 101 - internal/service/scan_test.go | 177 -- internal/service/service.go | 285 --- internal/service/share.go | 227 -- internal/service/share_test.go | 171 -- internal/service/streamer.go | 66 - internal/service/streamer_test.go | 122 -- internal/service/tag.go | 64 - internal/service/tag_test.go | 90 - internal/service/trash.go | 22 - internal/service/trash_test.go | 64 - internal/service/user.go | 55 - internal/service/user_test.go | 78 - internal/service/write.go | 277 --- internal/service/write_test.go | 100 - internal/setassign/setassign.go | 2 - internal/setassign/setassign_test.go | 9 - internal/thumb/thumb.go | 80 - internal/thumb/thumb_test.go | 151 -- internal/version.go | 4 - k8s/deployment.yaml | 73 - k8s/pvc-db.yaml | 12 - k8s/pvc-media.yaml | 12 - k8s/secret.yaml | 10 - k8s/service.yaml | 14 - logo.png | Bin 35131 -> 0 bytes logo.svg | 60 - player-server/AGENTS.md | 256 +++ player-server/Dockerfile | 37 + player-server/Magefile.go | 76 + player-server/README.md | 71 + player-server/cmd/player/main.go | 269 +++ player-server/cmd/player/main_test.go | 285 +++ player-server/docs/admin.md | 50 + player-server/docs/api.md | 84 + player-server/docs/configuration.md | 19 + player-server/docs/keyboard-shortcuts.md | 33 + player-server/docs/podcasts.md | 31 + player-server/docs/quick-start.md | 56 + player-server/docs/theming.md | 53 + player-server/go.mod | 31 + player-server/go.sum | 88 + player-server/internal/api/handlers.go | 193 ++ player-server/internal/api/handlers_admin.go | 146 ++ player-server/internal/api/handlers_auth.go | 126 ++ player-server/internal/api/handlers_config.go | 17 + player-server/internal/api/handlers_file.go | 109 + player-server/internal/api/handlers_media.go | 538 +++++ player-server/internal/api/handlers_more_test.go | 2191 +++++++++++++++++++ player-server/internal/api/handlers_podcast.go | 161 ++ .../internal/api/handlers_podcast_test.go | 370 ++++ player-server/internal/api/handlers_share.go | 215 ++ player-server/internal/api/handlers_test.go | 1673 ++++++++++++++ player-server/internal/api/middleware.go | 116 + player-server/internal/api/server.go | 296 +++ player-server/internal/api/server_test.go | 19 + player-server/internal/auth/auth.go | 41 + player-server/internal/auth/auth_test.go | 184 ++ player-server/internal/auth/interfaces.go | 15 + player-server/internal/auth/mock.go | 50 + player-server/internal/auth/mock_test.go | 61 + player-server/internal/auth/session.go | 85 + player-server/internal/clock/clock.go | 28 + player-server/internal/clock/clock_test.go | 24 + player-server/internal/config.go | 209 ++ player-server/internal/config_test.go | 209 ++ player-server/internal/mediatype/mediatype.go | 129 ++ player-server/internal/mediatype/mediatype_test.go | 163 ++ player-server/internal/model/media.go | 167 ++ player-server/internal/model/media_test.go | 27 + player-server/internal/model/podcast.go | 54 + player-server/internal/model/scan.go | 88 + player-server/internal/model/scan_test.go | 157 ++ player-server/internal/podcast/cover.go | 39 + player-server/internal/podcast/feed.go | 137 ++ player-server/internal/podcast/podcast_test.go | 560 +++++ player-server/internal/probe/image_test.go | 232 ++ player-server/internal/probe/probe.go | 242 ++ player-server/internal/probe/probe_test.go | 126 ++ player-server/internal/probe/remux.go | 119 + player-server/internal/probe/remux_test.go | 85 + player-server/internal/probe/testdata/golden.json | 14 + player-server/internal/repository/favorite.go | 62 + player-server/internal/repository/media.go | 319 +++ player-server/internal/repository/mock.go | 1194 ++++++++++ player-server/internal/repository/note.go | 48 + .../internal/repository/playback_accumulator.go | 47 + .../internal/repository/playback_progress.go | 122 ++ player-server/internal/repository/podcast.go | 417 ++++ player-server/internal/repository/podcast_repo.go | 52 + player-server/internal/repository/podcast_test.go | 311 +++ player-server/internal/repository/repository.go | 321 +++ player-server/internal/repository/schema.go | 262 +++ player-server/internal/repository/session.go | 60 + player-server/internal/repository/set.go | 84 + .../internal/repository/set_permission.go | 87 + player-server/internal/repository/share.go | 116 + player-server/internal/repository/sqlite.go | 77 + .../internal/repository/sqlite_no_rows_test.go | 132 ++ player-server/internal/repository/sqlite_test.go | 1925 ++++++++++++++++ player-server/internal/repository/tag.go | 110 + player-server/internal/repository/user.go | 94 + player-server/internal/scanner/fs.go | 28 + player-server/internal/scanner/scanner.go | 516 +++++ player-server/internal/scanner/scanner_test.go | 610 ++++++ player-server/internal/service/access.go | 117 + player-server/internal/service/admin.go | 81 + player-server/internal/service/admin_test.go | 506 +++++ player-server/internal/service/auth.go | 96 + player-server/internal/service/browse.go | 523 +++++ player-server/internal/service/browse_test.go | 179 ++ player-server/internal/service/fav.go | 28 + player-server/internal/service/fav_test.go | 67 + player-server/internal/service/filename.go | 31 + player-server/internal/service/gc.go | 169 ++ player-server/internal/service/gc_test.go | 322 +++ player-server/internal/service/import.go | 94 + player-server/internal/service/media.go | 47 + player-server/internal/service/media_test.go | 2304 ++++++++++++++++++++ player-server/internal/service/mock.go | 442 ++++ player-server/internal/service/mock_test.go | 176 ++ player-server/internal/service/no_rows_test.go | 190 ++ player-server/internal/service/note.go | 47 + player-server/internal/service/note_test.go | 114 + player-server/internal/service/perm.go | 62 + player-server/internal/service/perm_test.go | 61 + player-server/internal/service/podcast.go | 121 + player-server/internal/service/podcast_checker.go | 179 ++ player-server/internal/service/podcast_episode.go | 246 +++ player-server/internal/service/podcast_helpers.go | 35 + player-server/internal/service/podcast_sub.go | 296 +++ player-server/internal/service/podcast_test.go | 995 +++++++++ player-server/internal/service/progress.go | 164 ++ player-server/internal/service/progress_test.go | 387 ++++ player-server/internal/service/recover.go | 18 + player-server/internal/service/scan.go | 101 + player-server/internal/service/scan_test.go | 177 ++ player-server/internal/service/service.go | 285 +++ player-server/internal/service/share.go | 227 ++ player-server/internal/service/share_test.go | 171 ++ player-server/internal/service/streamer.go | 66 + player-server/internal/service/streamer_test.go | 122 ++ player-server/internal/service/tag.go | 64 + player-server/internal/service/tag_test.go | 90 + player-server/internal/service/trash.go | 22 + player-server/internal/service/trash_test.go | 64 + player-server/internal/service/user.go | 55 + player-server/internal/service/user_test.go | 78 + player-server/internal/service/write.go | 277 +++ player-server/internal/service/write_test.go | 100 + player-server/internal/setassign/setassign.go | 2 + player-server/internal/setassign/setassign_test.go | 9 + player-server/internal/thumb/thumb.go | 80 + player-server/internal/thumb/thumb_test.go | 151 ++ player-server/internal/version.go | 4 + player-server/k8s/deployment.yaml | 73 + player-server/k8s/pvc-db.yaml | 12 + player-server/k8s/pvc-media.yaml | 12 + player-server/k8s/secret.yaml | 10 + player-server/k8s/service.yaml | 14 + player-server/logo.png | Bin 0 -> 35131 bytes player-server/logo.svg | 60 + player-server/start.sh | 152 ++ player-server/web/bootstrap.html | 36 + player-server/web/css/components.css | 262 +++ player-server/web/css/layout.css | 729 +++++++ player-server/web/css/lightbox.css | 79 + player-server/web/css/login.css | 65 + player-server/web/css/player.css | 302 +++ player-server/web/css/theme.css | 117 + player-server/web/detach.html | 45 + player-server/web/favicon.ico | Bin 0 -> 117889 bytes player-server/web/favicon.svg | 1 + player-server/web/index.html | 305 +++ player-server/web/js/admin.js | 168 ++ player-server/web/js/api.js | 117 + player-server/web/js/app.js | 427 ++++ player-server/web/js/detach.js | 152 ++ player-server/web/js/detachPopup.js | 296 +++ player-server/web/js/dom.js | 27 + player-server/web/js/imageViewer.js | 193 ++ player-server/web/js/keyboard.js | 308 +++ player-server/web/js/lightbox.js | 177 ++ player-server/web/js/notes.js | 45 + player-server/web/js/playback.js | 727 ++++++ player-server/web/js/player.js | 1 + player-server/web/js/podcasts.js | 157 ++ player-server/web/js/pwa.js | 7 + player-server/web/js/search.js | 175 ++ player-server/web/js/selection.js | 157 ++ player-server/web/js/shuffle.js | 36 + player-server/web/js/state.js | 26 + player-server/web/js/tests/admin-rescan.test.js | 178 ++ player-server/web/js/tests/keyboard-enter.test.js | 75 + .../web/js/tests/media-pagination.test.js | 56 + player-server/web/js/tests/playback-resume.test.js | 51 + .../web/js/tests/podcast-episodes-api.test.js | 74 + .../web/js/tests/progress-actions.test.js | 120 + player-server/web/js/tests/search-parser.test.js | 33 + player-server/web/js/tests/shuffle.test.js | 165 ++ player-server/web/js/themes.js | 20 + player-server/web/js/utils.js | 21 + player-server/web/js/views/admin-status.js | 52 + player-server/web/js/views/help.js | 31 + player-server/web/js/views/media-actions.js | 93 + player-server/web/js/views/media-grid.js | 562 +++++ player-server/web/js/views/media-info.js | 142 ++ player-server/web/js/views/playback-nav.js | 251 +++ player-server/web/js/views/sets.js | 100 + player-server/web/js/views/shares.js | 131 ++ player-server/web/js/views/tags.js | 129 ++ player-server/web/js/views/upload.js | 47 + player-server/web/login.html | 30 + player-server/web/logo.png | Bin 0 -> 35131 bytes player-server/web/logo.svg | 60 + player-server/web/manifest.json | 11 + player-server/web/share.html | 116 + player-server/web/sw.js | 72 + start.sh | 152 -- web/bootstrap.html | 36 - web/css/components.css | 262 --- web/css/layout.css | 729 ------- web/css/lightbox.css | 79 - web/css/login.css | 65 - web/css/player.css | 302 --- web/css/theme.css | 117 - web/detach.html | 45 - web/favicon.ico | Bin 117889 -> 0 bytes web/favicon.svg | 1 - web/index.html | 305 --- web/js/admin.js | 168 -- web/js/api.js | 117 - web/js/app.js | 427 ---- web/js/detach.js | 152 -- web/js/detachPopup.js | 296 --- web/js/dom.js | 27 - web/js/imageViewer.js | 193 -- web/js/keyboard.js | 308 --- web/js/lightbox.js | 177 -- web/js/notes.js | 45 - web/js/playback.js | 727 ------ web/js/player.js | 1 - web/js/podcasts.js | 157 -- web/js/pwa.js | 7 - web/js/search.js | 175 -- web/js/selection.js | 157 -- web/js/shuffle.js | 36 - web/js/state.js | 26 - web/js/tests/admin-rescan.test.js | 178 -- web/js/tests/keyboard-enter.test.js | 75 - web/js/tests/media-pagination.test.js | 56 - web/js/tests/playback-resume.test.js | 51 - web/js/tests/podcast-episodes-api.test.js | 74 - web/js/tests/progress-actions.test.js | 120 - web/js/tests/search-parser.test.js | 33 - web/js/tests/shuffle.test.js | 165 -- web/js/themes.js | 20 - web/js/utils.js | 21 - web/js/views/admin-status.js | 52 - web/js/views/help.js | 31 - web/js/views/media-actions.js | 93 - web/js/views/media-grid.js | 562 ----- web/js/views/media-info.js | 142 -- web/js/views/playback-nav.js | 251 --- web/js/views/sets.js | 100 - web/js/views/shares.js | 131 -- web/js/views/tags.js | 129 -- web/js/views/upload.js | 47 - web/login.html | 30 - web/logo.png | Bin 35131 -> 0 bytes web/logo.svg | 60 - web/manifest.json | 11 - web/share.html | 116 - web/sw.js | 72 - 382 files changed, 36180 insertions(+), 36180 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 Dockerfile delete mode 100644 Magefile.go delete mode 100644 README.md delete mode 100644 cmd/player/main.go delete mode 100644 cmd/player/main_test.go delete mode 100644 docs/admin.md delete mode 100644 docs/api.md delete mode 100644 docs/configuration.md delete mode 100644 docs/keyboard-shortcuts.md delete mode 100644 docs/podcasts.md delete mode 100644 docs/quick-start.md delete mode 100644 docs/theming.md delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 internal/api/handlers.go delete mode 100644 internal/api/handlers_admin.go delete mode 100644 internal/api/handlers_auth.go delete mode 100644 internal/api/handlers_config.go delete mode 100644 internal/api/handlers_file.go delete mode 100644 internal/api/handlers_media.go delete mode 100644 internal/api/handlers_more_test.go delete mode 100644 internal/api/handlers_podcast.go delete mode 100644 internal/api/handlers_podcast_test.go delete mode 100644 internal/api/handlers_share.go delete mode 100644 internal/api/handlers_test.go delete mode 100644 internal/api/middleware.go delete mode 100644 internal/api/server.go delete mode 100644 internal/api/server_test.go delete mode 100644 internal/auth/auth.go delete mode 100644 internal/auth/auth_test.go delete mode 100644 internal/auth/interfaces.go delete mode 100644 internal/auth/mock.go delete mode 100644 internal/auth/mock_test.go delete mode 100644 internal/auth/session.go delete mode 100644 internal/clock/clock.go delete mode 100644 internal/clock/clock_test.go delete mode 100644 internal/config.go delete mode 100644 internal/config_test.go delete mode 100644 internal/mediatype/mediatype.go delete mode 100644 internal/mediatype/mediatype_test.go delete mode 100644 internal/model/media.go delete mode 100644 internal/model/media_test.go delete mode 100644 internal/model/podcast.go delete mode 100644 internal/model/scan.go delete mode 100644 internal/model/scan_test.go delete mode 100644 internal/podcast/cover.go delete mode 100644 internal/podcast/feed.go delete mode 100644 internal/podcast/podcast_test.go delete mode 100644 internal/probe/image_test.go delete mode 100644 internal/probe/probe.go delete mode 100644 internal/probe/probe_test.go delete mode 100644 internal/probe/remux.go delete mode 100644 internal/probe/remux_test.go delete mode 100644 internal/probe/testdata/golden.json delete mode 100644 internal/repository/favorite.go delete mode 100644 internal/repository/media.go delete mode 100644 internal/repository/mock.go delete mode 100644 internal/repository/note.go delete mode 100644 internal/repository/playback_accumulator.go delete mode 100644 internal/repository/playback_progress.go delete mode 100644 internal/repository/podcast.go delete mode 100644 internal/repository/podcast_repo.go delete mode 100644 internal/repository/podcast_test.go delete mode 100644 internal/repository/repository.go delete mode 100644 internal/repository/schema.go delete mode 100644 internal/repository/session.go delete mode 100644 internal/repository/set.go delete mode 100644 internal/repository/set_permission.go delete mode 100644 internal/repository/share.go delete mode 100644 internal/repository/sqlite.go delete mode 100644 internal/repository/sqlite_no_rows_test.go delete mode 100644 internal/repository/sqlite_test.go delete mode 100644 internal/repository/tag.go delete mode 100644 internal/repository/user.go delete mode 100644 internal/scanner/fs.go delete mode 100644 internal/scanner/scanner.go delete mode 100644 internal/scanner/scanner_test.go delete mode 100644 internal/service/access.go delete mode 100644 internal/service/admin.go delete mode 100644 internal/service/admin_test.go delete mode 100644 internal/service/auth.go delete mode 100644 internal/service/browse.go delete mode 100644 internal/service/browse_test.go delete mode 100644 internal/service/fav.go delete mode 100644 internal/service/fav_test.go delete mode 100644 internal/service/filename.go delete mode 100644 internal/service/gc.go delete mode 100644 internal/service/gc_test.go delete mode 100644 internal/service/import.go delete mode 100644 internal/service/media.go delete mode 100644 internal/service/media_test.go delete mode 100644 internal/service/mock.go delete mode 100644 internal/service/mock_test.go delete mode 100644 internal/service/no_rows_test.go delete mode 100644 internal/service/note.go delete mode 100644 internal/service/note_test.go delete mode 100644 internal/service/perm.go delete mode 100644 internal/service/perm_test.go delete mode 100644 internal/service/podcast.go delete mode 100644 internal/service/podcast_checker.go delete mode 100644 internal/service/podcast_episode.go delete mode 100644 internal/service/podcast_helpers.go delete mode 100644 internal/service/podcast_sub.go delete mode 100644 internal/service/podcast_test.go delete mode 100644 internal/service/progress.go delete mode 100644 internal/service/progress_test.go delete mode 100644 internal/service/recover.go delete mode 100644 internal/service/scan.go delete mode 100644 internal/service/scan_test.go delete mode 100644 internal/service/service.go delete mode 100644 internal/service/share.go delete mode 100644 internal/service/share_test.go delete mode 100644 internal/service/streamer.go delete mode 100644 internal/service/streamer_test.go delete mode 100644 internal/service/tag.go delete mode 100644 internal/service/tag_test.go delete mode 100644 internal/service/trash.go delete mode 100644 internal/service/trash_test.go delete mode 100644 internal/service/user.go delete mode 100644 internal/service/user_test.go delete mode 100644 internal/service/write.go delete mode 100644 internal/service/write_test.go delete mode 100644 internal/setassign/setassign.go delete mode 100644 internal/setassign/setassign_test.go delete mode 100644 internal/thumb/thumb.go delete mode 100644 internal/thumb/thumb_test.go delete mode 100644 internal/version.go delete mode 100644 k8s/deployment.yaml delete mode 100644 k8s/pvc-db.yaml delete mode 100644 k8s/pvc-media.yaml delete mode 100644 k8s/secret.yaml delete mode 100644 k8s/service.yaml delete mode 100644 logo.png delete mode 100644 logo.svg create mode 100644 player-server/AGENTS.md create mode 100644 player-server/Dockerfile create mode 100644 player-server/Magefile.go create mode 100644 player-server/README.md create mode 100644 player-server/cmd/player/main.go create mode 100644 player-server/cmd/player/main_test.go create mode 100644 player-server/docs/admin.md create mode 100644 player-server/docs/api.md create mode 100644 player-server/docs/configuration.md create mode 100644 player-server/docs/keyboard-shortcuts.md create mode 100644 player-server/docs/podcasts.md create mode 100644 player-server/docs/quick-start.md create mode 100644 player-server/docs/theming.md create mode 100644 player-server/go.mod create mode 100644 player-server/go.sum create mode 100644 player-server/internal/api/handlers.go create mode 100644 player-server/internal/api/handlers_admin.go create mode 100644 player-server/internal/api/handlers_auth.go create mode 100644 player-server/internal/api/handlers_config.go create mode 100644 player-server/internal/api/handlers_file.go create mode 100644 player-server/internal/api/handlers_media.go create mode 100644 player-server/internal/api/handlers_more_test.go create mode 100644 player-server/internal/api/handlers_podcast.go create mode 100644 player-server/internal/api/handlers_podcast_test.go create mode 100644 player-server/internal/api/handlers_share.go create mode 100644 player-server/internal/api/handlers_test.go create mode 100644 player-server/internal/api/middleware.go create mode 100644 player-server/internal/api/server.go create mode 100644 player-server/internal/api/server_test.go create mode 100644 player-server/internal/auth/auth.go create mode 100644 player-server/internal/auth/auth_test.go create mode 100644 player-server/internal/auth/interfaces.go create mode 100644 player-server/internal/auth/mock.go create mode 100644 player-server/internal/auth/mock_test.go create mode 100644 player-server/internal/auth/session.go create mode 100644 player-server/internal/clock/clock.go create mode 100644 player-server/internal/clock/clock_test.go create mode 100644 player-server/internal/config.go create mode 100644 player-server/internal/config_test.go create mode 100644 player-server/internal/mediatype/mediatype.go create mode 100644 player-server/internal/mediatype/mediatype_test.go create mode 100644 player-server/internal/model/media.go create mode 100644 player-server/internal/model/media_test.go create mode 100644 player-server/internal/model/podcast.go create mode 100644 player-server/internal/model/scan.go create mode 100644 player-server/internal/model/scan_test.go create mode 100644 player-server/internal/podcast/cover.go create mode 100644 player-server/internal/podcast/feed.go create mode 100644 player-server/internal/podcast/podcast_test.go create mode 100644 player-server/internal/probe/image_test.go create mode 100644 player-server/internal/probe/probe.go create mode 100644 player-server/internal/probe/probe_test.go create mode 100644 player-server/internal/probe/remux.go create mode 100644 player-server/internal/probe/remux_test.go create mode 100644 player-server/internal/probe/testdata/golden.json create mode 100644 player-server/internal/repository/favorite.go create mode 100644 player-server/internal/repository/media.go create mode 100644 player-server/internal/repository/mock.go create mode 100644 player-server/internal/repository/note.go create mode 100644 player-server/internal/repository/playback_accumulator.go create mode 100644 player-server/internal/repository/playback_progress.go create mode 100644 player-server/internal/repository/podcast.go create mode 100644 player-server/internal/repository/podcast_repo.go create mode 100644 player-server/internal/repository/podcast_test.go create mode 100644 player-server/internal/repository/repository.go create mode 100644 player-server/internal/repository/schema.go create mode 100644 player-server/internal/repository/session.go create mode 100644 player-server/internal/repository/set.go create mode 100644 player-server/internal/repository/set_permission.go create mode 100644 player-server/internal/repository/share.go create mode 100644 player-server/internal/repository/sqlite.go create mode 100644 player-server/internal/repository/sqlite_no_rows_test.go create mode 100644 player-server/internal/repository/sqlite_test.go create mode 100644 player-server/internal/repository/tag.go create mode 100644 player-server/internal/repository/user.go create mode 100644 player-server/internal/scanner/fs.go create mode 100644 player-server/internal/scanner/scanner.go create mode 100644 player-server/internal/scanner/scanner_test.go create mode 100644 player-server/internal/service/access.go create mode 100644 player-server/internal/service/admin.go create mode 100644 player-server/internal/service/admin_test.go create mode 100644 player-server/internal/service/auth.go create mode 100644 player-server/internal/service/browse.go create mode 100644 player-server/internal/service/browse_test.go create mode 100644 player-server/internal/service/fav.go create mode 100644 player-server/internal/service/fav_test.go create mode 100644 player-server/internal/service/filename.go create mode 100644 player-server/internal/service/gc.go create mode 100644 player-server/internal/service/gc_test.go create mode 100644 player-server/internal/service/import.go create mode 100644 player-server/internal/service/media.go create mode 100644 player-server/internal/service/media_test.go create mode 100644 player-server/internal/service/mock.go create mode 100644 player-server/internal/service/mock_test.go create mode 100644 player-server/internal/service/no_rows_test.go create mode 100644 player-server/internal/service/note.go create mode 100644 player-server/internal/service/note_test.go create mode 100644 player-server/internal/service/perm.go create mode 100644 player-server/internal/service/perm_test.go create mode 100644 player-server/internal/service/podcast.go create mode 100644 player-server/internal/service/podcast_checker.go create mode 100644 player-server/internal/service/podcast_episode.go create mode 100644 player-server/internal/service/podcast_helpers.go create mode 100644 player-server/internal/service/podcast_sub.go create mode 100644 player-server/internal/service/podcast_test.go create mode 100644 player-server/internal/service/progress.go create mode 100644 player-server/internal/service/progress_test.go create mode 100644 player-server/internal/service/recover.go create mode 100644 player-server/internal/service/scan.go create mode 100644 player-server/internal/service/scan_test.go create mode 100644 player-server/internal/service/service.go create mode 100644 player-server/internal/service/share.go create mode 100644 player-server/internal/service/share_test.go create mode 100644 player-server/internal/service/streamer.go create mode 100644 player-server/internal/service/streamer_test.go create mode 100644 player-server/internal/service/tag.go create mode 100644 player-server/internal/service/tag_test.go create mode 100644 player-server/internal/service/trash.go create mode 100644 player-server/internal/service/trash_test.go create mode 100644 player-server/internal/service/user.go create mode 100644 player-server/internal/service/user_test.go create mode 100644 player-server/internal/service/write.go create mode 100644 player-server/internal/service/write_test.go create mode 100644 player-server/internal/setassign/setassign.go create mode 100644 player-server/internal/setassign/setassign_test.go create mode 100644 player-server/internal/thumb/thumb.go create mode 100644 player-server/internal/thumb/thumb_test.go create mode 100644 player-server/internal/version.go create mode 100644 player-server/k8s/deployment.yaml create mode 100644 player-server/k8s/pvc-db.yaml create mode 100644 player-server/k8s/pvc-media.yaml create mode 100644 player-server/k8s/secret.yaml create mode 100644 player-server/k8s/service.yaml create mode 100644 player-server/logo.png create mode 100644 player-server/logo.svg create mode 100755 player-server/start.sh create mode 100644 player-server/web/bootstrap.html create mode 100644 player-server/web/css/components.css create mode 100644 player-server/web/css/layout.css create mode 100644 player-server/web/css/lightbox.css create mode 100644 player-server/web/css/login.css create mode 100644 player-server/web/css/player.css create mode 100644 player-server/web/css/theme.css create mode 100644 player-server/web/detach.html create mode 100644 player-server/web/favicon.ico create mode 100644 player-server/web/favicon.svg create mode 100644 player-server/web/index.html create mode 100644 player-server/web/js/admin.js create mode 100644 player-server/web/js/api.js create mode 100644 player-server/web/js/app.js create mode 100644 player-server/web/js/detach.js create mode 100644 player-server/web/js/detachPopup.js create mode 100644 player-server/web/js/dom.js create mode 100644 player-server/web/js/imageViewer.js create mode 100644 player-server/web/js/keyboard.js create mode 100644 player-server/web/js/lightbox.js create mode 100644 player-server/web/js/notes.js create mode 100644 player-server/web/js/playback.js create mode 100644 player-server/web/js/player.js create mode 100644 player-server/web/js/podcasts.js create mode 100644 player-server/web/js/pwa.js create mode 100644 player-server/web/js/search.js create mode 100644 player-server/web/js/selection.js create mode 100644 player-server/web/js/shuffle.js create mode 100644 player-server/web/js/state.js create mode 100644 player-server/web/js/tests/admin-rescan.test.js create mode 100644 player-server/web/js/tests/keyboard-enter.test.js create mode 100644 player-server/web/js/tests/media-pagination.test.js create mode 100644 player-server/web/js/tests/playback-resume.test.js create mode 100644 player-server/web/js/tests/podcast-episodes-api.test.js create mode 100644 player-server/web/js/tests/progress-actions.test.js create mode 100644 player-server/web/js/tests/search-parser.test.js create mode 100644 player-server/web/js/tests/shuffle.test.js create mode 100644 player-server/web/js/themes.js create mode 100644 player-server/web/js/utils.js create mode 100644 player-server/web/js/views/admin-status.js create mode 100644 player-server/web/js/views/help.js create mode 100644 player-server/web/js/views/media-actions.js create mode 100644 player-server/web/js/views/media-grid.js create mode 100644 player-server/web/js/views/media-info.js create mode 100644 player-server/web/js/views/playback-nav.js create mode 100644 player-server/web/js/views/sets.js create mode 100644 player-server/web/js/views/shares.js create mode 100644 player-server/web/js/views/tags.js create mode 100644 player-server/web/js/views/upload.js create mode 100644 player-server/web/login.html create mode 100644 player-server/web/logo.png create mode 100644 player-server/web/logo.svg create mode 100644 player-server/web/manifest.json create mode 100644 player-server/web/share.html create mode 100644 player-server/web/sw.js delete mode 100755 start.sh delete mode 100644 web/bootstrap.html delete mode 100644 web/css/components.css delete mode 100644 web/css/layout.css delete mode 100644 web/css/lightbox.css delete mode 100644 web/css/login.css delete mode 100644 web/css/player.css delete mode 100644 web/css/theme.css delete mode 100644 web/detach.html delete mode 100644 web/favicon.ico delete mode 100644 web/favicon.svg delete mode 100644 web/index.html delete mode 100644 web/js/admin.js delete mode 100644 web/js/api.js delete mode 100644 web/js/app.js delete mode 100644 web/js/detach.js delete mode 100644 web/js/detachPopup.js delete mode 100644 web/js/dom.js delete mode 100644 web/js/imageViewer.js delete mode 100644 web/js/keyboard.js delete mode 100644 web/js/lightbox.js delete mode 100644 web/js/notes.js delete mode 100644 web/js/playback.js delete mode 100644 web/js/player.js delete mode 100644 web/js/podcasts.js delete mode 100644 web/js/pwa.js delete mode 100644 web/js/search.js delete mode 100644 web/js/selection.js delete mode 100644 web/js/shuffle.js delete mode 100644 web/js/state.js delete mode 100644 web/js/tests/admin-rescan.test.js delete mode 100644 web/js/tests/keyboard-enter.test.js delete mode 100644 web/js/tests/media-pagination.test.js delete mode 100644 web/js/tests/playback-resume.test.js delete mode 100644 web/js/tests/podcast-episodes-api.test.js delete mode 100644 web/js/tests/progress-actions.test.js delete mode 100644 web/js/tests/search-parser.test.js delete mode 100644 web/js/tests/shuffle.test.js delete mode 100644 web/js/themes.js delete mode 100644 web/js/utils.js delete mode 100644 web/js/views/admin-status.js delete mode 100644 web/js/views/help.js delete mode 100644 web/js/views/media-actions.js delete mode 100644 web/js/views/media-grid.js delete mode 100644 web/js/views/media-info.js delete mode 100644 web/js/views/playback-nav.js delete mode 100644 web/js/views/sets.js delete mode 100644 web/js/views/shares.js delete mode 100644 web/js/views/tags.js delete mode 100644 web/js/views/upload.js delete mode 100644 web/login.html delete mode 100644 web/logo.png delete mode 100644 web/logo.svg delete mode 100644 web/manifest.json delete mode 100644 web/share.html delete mode 100644 web/sw.js diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index db60e36..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,256 +0,0 @@ -# Player — Agent Documentation - -This file is written for coding agents working on the `player` project. - ---- - -## Architecture Overview - -The project is a **self-hosted media player** designed for simplicity (KISS): minimal dependencies, no frontend frameworks, interface-driven Go code for easy testing. - -### Backend - -- **Language / Runtime:** Go 1.23 -- **HTTP Server:** `net/http` stdlib only; `http.ServeMux` with pattern matching -- **Database:** SQLite via `modernc.org/sqlite` -- **Media Processing:** `ffmpeg` / `ffprobe` installed in runtime container -- **Password Hashing:** `golang.org/x/crypto/bcrypt` - -**Layered architecture:** - -| Layer | Package | Role | -|-------|---------|------| -| Entrypoint | `cmd/player` | Flags, config, dependency wiring, server start | - -| `mage build` | Compile the binary (`go build -o player ./cmd/player`) | -| `mage test` | Run `go test ./...` | -| `mage install` | Build and copy `player` to `$GOPATH/bin` (or `~/go/bin`) | -| `mage clean` | Remove the `player` binary | -| `mage docker-build` | Build container image as `player:latest` | -| `mage docker-push` | Push `player:latest` to registry | - ---- - -## Kubernetes Deployment - -The `k8s/` directory contains: - -| File | Resource | -|------|----------| -| `k8s/deployment.yaml` | `Deployment` (1 replica, non-root `65534:65534`, probes) | -| `k8s/service.yaml` | `ClusterIP` Service on port 8080 | -| `k8s/pvc-db.yaml` | `PersistentVolumeClaim` (`ReadWriteOnce`, 1Gi) for `/data` | -| `k8s/pvc-media.yaml` | `PersistentVolumeClaim` (`ReadWriteMany`, 10Gi) for `/media` | -| `k8s/secret.yaml` | `Secret` (optional) for environment overrides (e.g., `ADMIN_PASSWORD`) | - -Deploy everything: - -```bash -kubectl apply -f k8s/ -``` - -The `Deployment` overrides two critical settings for K8s: -- `DB_PATH=/data/media.db` -- `MEDIA_ROOT=/media` - -Probes: -- **Liveness:** `GET /healthz` (no DB dependency) -- **Readiness:** `GET /readyz` (DB ping) - -Security: -- `runAsNonRoot: true` -- `runAsUser: 65534` / `runAsGroup: 65534` -- `allowPrivilegeEscalation: false` -- `readOnlyRootFilesystem: true` - ---- - -## Theming Guide - -All colors live in `web/css/theme.css` as CSS Custom Properties on `:root`. - -### Current Implementation - -`themes.js` swaps the active theme by setting `document.documentElement.setAttribute('data-theme', ...)` and saves the preference to `localStorage`. Override blocks in `theme.css` handle the light variant: - -```css -/* Default (dark) — defined on :root */ -:root { - --bg-body: #0f1117; - --text-primary: #e6e8ef; - --accent: #5e9eff; - ... -} - -/* Light theme overrides */ -[data-theme="light"] { - --bg-body: #f4f5f8; - --text-primary: #12131a; - --accent: #2b6cb0; - ... -} -``` - -### Adding a New Theme - -Option A — inline override (recommended for small additions): - -1. Open `web/css/theme.css`. -2. Append a new attribute selector after the light block, e.g.: - -```css -[data-theme="solarized"] { - --bg-body: #002b36; - --text-primary: #839496; - --accent: #268bd2; - ... -} -``` - -3. Wire the toggle in `web/js/themes.js` (or expose a selector UI in `index.html`) to call `apply('solarized')`. - -Option B — separate file (if you prefer a stylesheet swap): - -1. Create `web/css/themes/.css` containing `:root { ... }` overrides. -2. Dynamically create or swap a `` in `themes.js` instead of using `data-theme`. - -**Rules:** -- No color literals in component styles — everything must go through `var(--*)`. -- Do not add inline styles in HTML or JS. - ---- - -## Keyboard Shortcuts - -Global shortcuts are registered in `web/js/keyboard.js`. They are **disabled** while the user is focused on an `INPUT`, `TEXTAREA`, or `contentEditable` element (except `Escape` to blur). - -| Key | Action | -|-----|--------| -| `↑` / `↓` | Navigate media list (up / down) | -| `k` / `j` | Navigate media list (up / down) | -| `←` / `→` | Switch sets / pages | -| `h` / `l` | Switch sets / pages | -| `Enter` | Open selected media (navigate to detail) | -| `Space` / `p` | Play / pause / switch to selected item | -| `f` | Toggle fullscreen on the player wrapper | -| `Esc` | Exit fullscreen, or deselect current item | -| `r` | Enable shuffle, or reshuffle if shuffle is already active | -| `s` | Generate a share link for the selected media | -| `/` | Focus the quick search bar (debounced) | -| `n` | Open notes modal for the selected media | - ---- - -## Admin Tasks - -Admin endpoints are gated by `RequireAdmin` middleware (checks `users.is_admin`). The admin panel is opened via the hidden "Admin" button in the SPA header (shown only when the current user is an admin). - -### Creating Users - -1. Open the admin panel. -2. Enter username, password, and check "Is admin" if desired. -3. Submit — the frontend calls `POST /api/admin/users`. -4. Admins cannot delete themselves via `DELETE /api/admin/users/:id`. - -### Managing Set Permissions - -- `GET /api/admin/permissions` — list permissions matrix -- `POST /api/admin/permissions` — grant access to a set (`body: { set_id, user_id, role: "owner" | "viewer" }`) -- `DELETE /api/admin/permissions` — revoke access (`body: { set_id, user_id }`) - -Roles: -- `owner` — can upload to the set, soft-delete / restore media, regenerate thumbnails -- `viewer` — can browse and play media in the set - -Admins implicitly see all sets without explicit permission rows. - -### Rescanning the Library - -Click **Rescan** in the admin panel, or call: - -```bash -curl -X POST -b session= http:///api/admin/rescan -``` - -This triggers `FSScanner.Scan()`, which: -1. Scans immediate subdirectories of `MEDIA_ROOT` as **sets** -2. Recursively walks each set for supported media files -3. Probes new files with `ffprobe` -4. Generates thumbnails for video files -5. Inserts new records into the `media` table - ---- - -## Configuration via Environment Variables - -`internal/config.go` loads all settings from the environment. - -| Variable | Default | Validation | Description | -|----------|---------|------------|-------------| -| `PORT` | `8080` | 0–65535 | HTTP listen port (0 = ephemeral, used in tests) | -| `MEDIA_ROOT` | `./media` | — | Root path for media set directories | -| `DB_PATH` | `data.db` | — | SQLite database file path | -| `MAX_UPLOAD_SIZE_MB` | `100` | ≥ 1 | Max upload size per file (MB) | -| `SESSION_TIMEOUT_HOURS` | `24` | ≥ 1 | Cookie / session expiry | -| `GC_INTERVAL_MINUTES` | `30` | ≥ 1 | Garbage collector tick interval | -| `SHARE_DEFAULT_EXPIRY_DAYS` | `7` | ≥ 1 | Default share link lifetime | -| `MEDIA_PAGE_SIZE` | `100` | ≥ 1 | Items displayed per thumbnail grid page | -| `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` | Log verbosity | -| `SECURE_COOKIES` | `true` | `true` / `false` | Set `Secure` flag on session cookies; set to `false` for plain-HTTP local deployments | - -**Important:** The K8s `Deployment` overrides `DB_PATH` to `/data/media.db` and `MEDIA_ROOT` to `/media` so the PVC mounts are used. Do not rely on the local defaults in a container. - ---- - -## Podcast Support - -Podcasts live under one shared **podcast set** (`sets.is_podcast = 1`, `root_path = "podcast"`). Each subscribed podcast is a `podcast_feeds` row and gets its own folder inside that single set. - -### Subscribing - -Admin opens the **Podcasts** button in the admin panel (or calls `POST /api/podcasts`): -- Submit an RSS/Atom feed URL. -- Server creates or reuses the shared `podcast` set, creates a feed folder inside it, parses the feed, downloads the cover image, and inserts episodes into `podcast_episodes`. - -### Episode Management - -Episodes are stored in `podcast_episodes` and rendered in the browse grid under the shared podcast set: -- **Undownloaded** episodes show a **Download to server** button (calls `POST /api/podcasts/episodes/{id}/download`). -- **Downloaded** episodes become regular `media` rows and appear as normal media cards. -- Users can mark episodes as listened/unlistened via the checkmark button. - -### Background Feed Checker - -A background goroutine (`CheckFeeds`) refreshes feeds every hour (configurable via `PODCAST_CHECK_INTERVAL_MINUTES`). It uses conditional GET (`If-None-Match`, `If-Modified-Since`) to avoid re-downloading unchanged feeds. - -### New Endpoints - -| Method | Path | Description | -|--------|------|-------------| -| `GET` | `/api/podcasts` | List subscribed podcast feeds | -| `POST` | `/api/podcasts` | Subscribe to a new feed (admin) | -| `GET` | `/api/podcasts/{id}/episodes` | List episodes with status | -| `POST` | `/api/podcasts/episodes/{id}/download` | Server-side download | -| `POST` | `/api/podcasts/episodes/{id}/complete` | Toggle completion | - -### New Files - -- `internal/model/podcast.go` — PodcastFeed, PodcastEpisode, PodcastStatus -- `internal/repository/podcast.go` — CRUD and queries -- `internal/podcast/feed.go` — RSS/Atom parser using `gofeed` -- `internal/podcast/cover.go` — Cover image downloader -- `internal/service/podcast.go` — Business logic and background checker -- `internal/api/handlers_podcast.go` — REST handlers -- `internal/service/import.go` — Shared `ImportMediaFile` helper (used by uploads + downloader) -- `web/js/podcasts.js` — Feed manager modal and episode renderer - ---- - -## Notes for Agents - -- When modifying tests, always run `go test ./... -race -cover` before committing. -- Do not introduce package-level mutable state; inject via constructors. -- All repository access goes through the `repository.Store` interface. -- Frontend modules are plain ES modules — no transpilation step. Keep JS vanilla. -- CSS changes must use `var(--*)` tokens from `theme.css`. -- If you add new env vars, update both `internal/config.go` and this document. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 6f30192..0000000 --- a/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -# syntax=docker/dockerfile:1 - -# ---- Builder stage ---- -FROM golang:1.25-alpine AS builder - -WORKDIR /build - -# Install git for go mod download if needed. -RUN apk add --no-cache git - -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . - -# Build the binary. -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o player ./cmd/player - -# ---- Runtime stage ---- -FROM alpine:3.21 - -# Install ffmpeg and ffprobe. -RUN apk add --no-cache ffmpeg - -# Create a non-root user (nobody already exists as 65534). -USER 65534:65534 - -WORKDIR /app - -# Copy the binary and static assets from the builder. -COPY --from=builder --chown=65534:65534 /build/player /app/player -COPY --from=builder --chown=65534:65534 /build/web /app/web - -# Expose the default HTTP port. -EXPOSE 8080 - -ENTRYPOINT ["/app/player"] diff --git a/Magefile.go b/Magefile.go deleted file mode 100644 index f7b79f2..0000000 --- a/Magefile.go +++ /dev/null @@ -1,76 +0,0 @@ -//go:build mage - -package main - -import ( - "fmt" - "os" - "path/filepath" - "runtime" - - "github.com/magefile/mage/mg" - "github.com/magefile/mage/sh" -) - -const binaryName = "player" - -// Default target runs Build. -func Default() error { - mg.Deps(Build) - return nil -} - -// Build compiles the application binary. -func Build() error { - return sh.RunV("go", "build", "-o", binaryName, "./cmd/player") -} - -// Test runs all tests in the project with the race detector enabled. -func Test() error { - return sh.RunV("go", "test", "-race", "-count=1", "./...") -} - -// Install builds and copies the binary to GOPATH/bin. -func Install() error { - mg.Deps(Build) - - goPath := os.Getenv("GOPATH") - if goPath == "" { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("resolving home directory: %w", err) - } - goPath = filepath.Join(home, "go") - } - - binDir := filepath.Join(goPath, "bin") - if err := os.MkdirAll(binDir, 0o755); err != nil { - return fmt.Errorf("creating %s: %w", binDir, err) - } - - src := filepath.Join(".", binaryName) - dst := filepath.Join(binDir, binaryName) - - if runtime.GOOS == "windows" { - return sh.Copy(dst, src) - } - return sh.RunV("cp", "-v", src, dst) -} - -// Clean removes build artifacts. -func Clean() error { - if err := sh.Rm(binaryName); err != nil { - return fmt.Errorf("removing %s: %w", binaryName, err) - } - return nil -} - -// DockerBuild builds the container image. -func DockerBuild() error { - return sh.RunV("docker", "build", "-t", "player:latest", ".") -} - -// DockerPush pushes the container image to the registry. -func DockerPush() error { - return sh.RunV("docker", "push", "player:latest") -} diff --git a/README.md b/README.md deleted file mode 100644 index 337e51f..0000000 --- a/README.md +++ /dev/null @@ -1,71 +0,0 @@ -player -====== - -![Player logo](logo.png) - -Player is an opinionated, self-hosted web media player designed around the KISS principle: minimal dependencies, no frontend frameworks, keyboard-first navigation. Built with Go, SQLite, and vanilla JavaScript. - -Features --------- - -### Media Playback - -- **Videos** — streaming with seek (range requests), optional remuxing, thumbnail previews -- **Images** — lightbox viewer with zoom, slideshow mode, and pan support -- **Audio / Audiobooks** — cover art display, progress tracking (resumes where you left off) -- **Podcasts** — subscribe to RSS/Atom feeds, download episodes to the server, mark listened/unlistened - -### Organization - -- **Sets** — media is grouped into sets (one subdirectory of `MEDIA_ROOT` per set) -- **Folder browsing** — navigate subfolders within a set via breadcrumbs -- **Tags** — add and remove tags on any media item; filter by tag -- **Favorites** — mark items as favorites; filter to favorites-only view -- **Notes** — attach personal notes to any media item -- **Search** — full-text search with query syntax: `min:30`, `tag:a,b`, `like:1`, `type:video`, `sort:random`, `minsize:10`, etc. -- **Shuffle** — randomize playback of the current filtered result set - -### Sharing & Access - -- **Share links** — generate time-limited share links for any media item (no login required for recipients) -- **User accounts** — multi-user with login, session cookies, and logout -- **RBAC permissions** — per-set `owner` and `viewer` roles; admins see everything implicitly -- **Admin panel** — create/delete users, manage permissions, trigger library rescans, manage trash - -### Integrations - -- **Podcast feed manager** — add feeds via URL, automatic background refresh with conditional GET -- **File upload** — upload media directly to a set from the browser -- **Library scanner** — `ffprobe`-backed metadata extraction and automatic thumbnail generation -- **PWA** — installable as a Progressive Web App (`manifest.json` + service worker) -- **Detached player** — pop out the player into its own window for multitasking - -### Theming - -- Dark and light themes with CSS custom properties -- Theme preference saved to `localStorage` -- Easy to add custom themes via `data-theme` attribute selectors - -### Keyboard-First - -Full vim-friendly keyboard navigation — disabled automatically when typing in inputs. See [Keyboard Shortcuts](docs/keyboard-shortcuts.md) for the full reference. - -### Tech Stack - -- **Backend:** Go 1.23, `net/http` stdlib, SQLite (`modernc.org/sqlite`), bcrypt -- **Media processing:** `ffmpeg` / `ffprobe` (in container) -- **Frontend:** Vanilla JS (ES modules), CSS custom properties, no build step -- **Container:** Alpine-based Docker image, runs as non-root (UID 65534) - -Documentation -------------- - -| Document | Description | -|----------|-------------| -| [Quick Start](docs/quick-start.md) | Running from source, Docker, Kubernetes, mage targets | -| [Configuration](docs/configuration.md) | Environment variables and defaults | -| [API Reference](docs/api.md) | Full REST API endpoint listing | -| [Keyboard Shortcuts](docs/keyboard-shortcuts.md) | All key bindings and search syntax | -| [Theming](docs/theming.md) | Theme system and adding custom themes | -| [Podcast Support](docs/podcasts.md) | Subscribing, episode management, feed checker | -| [Admin Guide](docs/admin.md) | Users, permissions, rescans, trash | \ No newline at end of file diff --git a/cmd/player/main.go b/cmd/player/main.go deleted file mode 100644 index 771664f..0000000 --- a/cmd/player/main.go +++ /dev/null @@ -1,269 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "log/slog" - "net/http" - "os" - "os/signal" - "runtime/debug" - "syscall" - "time" - - "codeberg.org/snonux/player/internal" - "codeberg.org/snonux/player/internal/api" - "codeberg.org/snonux/player/internal/auth" - "codeberg.org/snonux/player/internal/clock" - "codeberg.org/snonux/player/internal/probe" - "codeberg.org/snonux/player/internal/repository" - "codeberg.org/snonux/player/internal/scanner" - "codeberg.org/snonux/player/internal/service" - "codeberg.org/snonux/player/internal/thumb" -) - -// appDeps bundles all wired service-layer dependencies. -type appDeps struct { - store repository.Store - hasher auth.Hasher - sm auth.SessionManager - cfg *internal.Config - clk clock.Clock - mediaSvc service.MediaService - adminSvc service.AdminService - progressSvc service.ProgressService - authSvc service.AuthService - podcastSvc service.PodcastEpisodeService - scanner scanner.Scanner - gcWorker *service.GCWorker - logger *slog.Logger - appCtx context.Context - workersStarted chan<- struct{} -} - -// parseVersionFlag parses CLI flags and returns whether --version was requested. -func parseVersionFlag(args []string) (bool, error) { - fs := flag.NewFlagSet("player", flag.ContinueOnError) - versionFlag := fs.Bool("version", false, "print version and exit") - if err := fs.Parse(args); err != nil { - return false, err - } - return *versionFlag, nil -} - -func main() { - if err := run(os.Args[1:]); err != nil { - slog.Error("fatal", "err", err) - os.Exit(1) - } -} - -func run(args []string) error { - return runWithSignal(args, nil) -} - -// buildLogger creates a slog.Logger aligned with the named log level. -func buildLogger(logLevel string) *slog.Logger { - var level slog.Level - switch logLevel { - case "debug": - level = slog.LevelDebug - case "info": - level = slog.LevelInfo - case "warn": - level = slog.LevelWarn - case "error": - level = slog.LevelError - default: - level = slog.LevelInfo - } - return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})) -} - -func recoverBackgroundWorkerPanic(logger *slog.Logger, worker string) { - if r := recover(); r != nil && logger != nil { - logger.Error("background worker panic", "worker", worker, "panic", r, "stack", string(debug.Stack())) - } -} - -// wireDeps constructs the core service layer dependencies. -func wireDeps(cfg *internal.Config, store repository.Store, logger *slog.Logger, appCtx context.Context) *appDeps { - clk := clock.RealClock{} - hasher := auth.NewBCryptHasher(12) - sm := auth.NewSessionManager(store, clk, time.Duration(cfg.SessionTimeoutHours)*time.Hour) - - prober := probe.NewFFProber() - thumbGen := thumb.NewFFmpegGenerator() - - helper := service.NewAccessHelper(store) - browser := service.NewPodcastBrowseService(store, cfg.MediaRoot) - mediaSvc := service.NewMediaServiceWithPodcastBrowser(store, clk, cfg.MediaRoot, thumbGen, prober, browser) - - fsScanner := scanner.NewFSScannerWithLogger(store, prober, thumbGen, clk, cfg.MediaRoot, logger) - adminSvc := service.NewAdminServiceWithLogger(store, clk, hasher, fsScanner, cfg.MediaRoot, appCtx, logger) - - progressSvc := service.NewProgressService(store, clk) - authSvc := service.NewAuthService(store, clk, hasher, sm) - - podcastSvc := service.NewPodcastServiceWithLogger(store, clk, cfg.MediaRoot, helper, prober, thumbGen, &http.Client{Timeout: service.DefaultHTTPClientTimeout}, cfg.PodcastCheckMinutes, logger) - - gcWorker := service.NewGCWorker(store, clk, cfg.MediaRoot, time.Duration(cfg.GCIntervalMinutes)*time.Minute, logger) - - return &appDeps{ - store: store, - hasher: hasher, - sm: sm, - cfg: cfg, - clk: clk, - mediaSvc: mediaSvc, - adminSvc: adminSvc, - progressSvc: progressSvc, - authSvc: authSvc, - podcastSvc: podcastSvc, - scanner: fsScanner, - gcWorker: gcWorker, - logger: logger, - appCtx: appCtx, - } -} - -// startBackgroundWorkers launches background goroutines (GC, podcast feed checker). -func startBackgroundWorkers(deps *appDeps) { - deps.gcWorker.Start() - - // Start podcast feed background checker. - go func() { - ticker := time.NewTicker(time.Duration(deps.cfg.PodcastCheckMinutes) * time.Minute) - defer ticker.Stop() - for { - select { - case <-ticker.C: - func() { - defer recoverBackgroundWorkerPanic(deps.logger, "podcast checker") - if err := deps.podcastSvc.CheckFeeds(context.Background()); err != nil {