From e0d3d42150080444a326a26ea46884359d423355 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Sat, 19 Sep 2026 02:02:24 +0000 Subject: [PATCH] feat: add read-aloud and media playback support Add EPUB narration, shared media controls, and native device session integration. Apply the generated migrations and update all first-party clients alongside the server. BREAKING CHANGE: introduce incompatible database and API contracts for media playback --- .env.example | 3 + README.md | 3 + client/eslint.config.ts | 11 +- client/public/assets/foliate/overlayer.d.ts | 10 + client/public/assets/foliate/text-walker.d.ts | 1 + client/public/assets/foliate/tts.d.ts | 9 + client/src/App.vue | 59 +- client/src/assets/theme/bridge.css | 1 + client/src/assets/theme/tokens.css | 16 + client/src/components/AppHeader.vue | 25 +- client/src/components/AppLayout.vue | 11 +- client/src/components/AppSidebar.vue | 338 +- client/src/components/ChipGroup.vue | 21 + client/src/components/EmptyState.vue | 18 + client/src/components/FilterChip.vue | 27 + client/src/components/FormSheet.vue | 140 + client/src/components/ListEndFooter.vue | 22 + client/src/components/ListSkeleton.vue | 16 + client/src/components/LoadErrorPanel.vue | 21 + client/src/components/SelectionActionBar.vue | 8 +- client/src/components/ViewHeader.vue | 2 +- .../components/__tests__/EmptyState.spec.ts | 27 + .../components/__tests__/FormSheet.spec.ts | 106 + .../entity-index/EntityIndexView.vue | 20 +- .../__tests__/EntityIndexView.spec.ts | 68 + client/src/components/media/MediaScrubber.vue | 237 + .../src/components/media/MediaTransport.vue | 124 + .../components/media/MediaVolumeControl.vue | 93 + .../media/__tests__/MediaScrubber.spec.ts | 201 + .../media/__tests__/MediaTransport.spec.ts | 92 + .../__tests__/MediaVolumeControl.spec.ts | 74 + client/src/components/media/types.ts | 25 + .../sidebar/SidebarEntitySection.vue | 35 +- .../components/sidebar/SidebarModeSwitch.vue | 72 + .../__tests__/SidebarEntitySection.spec.ts | 21 + .../sidebar/__tests__/SidebarZone.spec.ts | 3 + .../sidebar/__tests__/sidebar-order.spec.ts | 69 + .../src/components/sidebar/sidebar-order.ts | 30 + client/src/components/ui/Chip.vue | 53 + client/src/components/ui/ConfirmDialog.vue | 26 +- .../ui/__tests__/ConfirmDialog.spec.ts | 81 + client/src/components/ui/input/Input.vue | 24 +- client/src/components/ui/select/Select.vue | 38 + client/src/components/ui/select/index.ts | 1 + .../src/components/ui/textarea/Textarea.vue | 36 + client/src/components/ui/textarea/index.ts | 1 + .../view-header/ViewHeaderDisplayControls.vue | 6 + .../ViewHeaderMobileSearchSheet.vue | 4 +- .../__tests__/useMediaMode.spec.ts | 116 + .../__tests__/useSidebarNav.spec.ts | 38 +- .../__tests__/useSidebarPrefs.spec.ts | 6 + client/src/composables/useDraggableOrder.ts | 6 +- client/src/composables/useFilePicker.ts | 23 + .../composables/useInfiniteScrollSentinel.ts | 0 client/src/composables/useMediaMode.ts | 100 + client/src/composables/useModal.ts | 30 +- .../src/composables/usePagedEntityMembers.ts | 117 + client/src/composables/useScrollOverflow.ts | 54 + client/src/composables/useSidebarNav.ts | 39 +- client/src/composables/useSidebarPrefs.ts | 6 +- .../admin/SharedReadingInsightsPage.vue | 2 +- .../SharedReadingInsightsPage.spec.ts | 7 +- .../lib/__tests__/permission-presets.spec.ts | 15 +- .../features/admin/lib/permission-presets.ts | 12 +- client/src/features/audit/AuditLogPage.vue | 11 +- .../audit/__tests__/audit-display.spec.ts | 1 + client/src/features/audit/audit-display.ts | 2 +- client/src/features/audit/audit-resources.ts | 2 + .../composables/__tests__/useAuth.spec.ts | 16 + .../src/features/auth/composables/useAuth.ts | 14 +- .../__tests__/BookDockFileRow.spec.ts | 2 +- .../BookMetadataFetchWidget.spec.ts | 3 +- .../ReleaseFileInspectionPanel.spec.ts | 2 +- .../book/components/BookCoverCard.vue | 23 +- .../book/components/BookDownloadButton.vue | 42 +- .../features/book/components/BookListRow.vue | 35 +- .../book/components/BookQuickView.vue | 29 +- .../book/components/BookSortBuilder.vue | 5 +- .../__tests__/BookCoverCard.spec.ts | 14 +- .../components/__tests__/BookListRow.spec.ts | 17 + .../__tests__/BookQuickView.spec.ts | 11 + .../__tests__/BookTableCells.spec.ts | 8 + .../components/__tests__/DetailsTab.spec.ts | 60 + .../detail/reading-log/ReadingLogLedger.vue | 2 + .../reading-log/ReadingLogSourceSplit.vue | 2 + .../reading-log/ReadingLogTraceChart.vue | 2 + .../__tests__/ReadingLogSourceSplit.spec.ts | 8 +- .../components/detail/tabs/DetailsTab.vue | 54 +- .../detail/tabs/__tests__/DetailsTab.spec.ts | 11 + .../tabs/__tests__/DetailsTabActions.spec.ts | 11 + .../tabs/__tests__/ReadingLogTab.spec.ts | 37 +- .../components/table/BookTableFormatCell.vue | 32 +- .../__tests__/useBookDownload.spec.ts | 13 +- .../__tests__/useMetadataEditor.spec.ts | 11 + .../__tests__/useMetadataLocks.spec.ts | 11 + .../__tests__/usePersonalNote.spec.ts | 11 + .../book/composables/useBookDownload.ts | 13 +- .../metadata-refresh-feedback.spec.ts | 11 + .../book/lib/book-card-mapper.test.ts | 29 + .../src/features/book/lib/book-card-mapper.ts | 10 +- .../features/book/lib/file-capabilities.ts | 18 + .../components/AddToCollectionSheet.vue | 40 +- .../components/CollectionMembershipList.vue | 71 + .../components/CreateCollectionDialog.vue | 13 +- .../__tests__/AddToCollectionSheet.spec.ts | 2 + .../__tests__/CreateCollectionDialog.spec.ts | 113 + .../__tests__/useCollections.spec.ts | 59 + .../collection/composables/useCollections.ts | 141 +- .../collection/lib/collection-access.spec.ts | 19 +- .../collection/lib/collection-access.ts | 8 - .../collection/views/CollectionsView.vue | 4 +- .../DashboardPodcastScroller.spec.ts | 151 + .../components/DashboardPodcastScroller.vue | 171 + .../components/DashboardScroller.spec.ts | 6 +- .../components/DashboardScroller.vue | 4 +- .../components/DashboardSettingsSheet.spec.ts | 10 +- .../components/DashboardSettingsSheet.vue | 3 +- .../__tests__/CurrentlyReadingWidget.spec.ts | 14 +- .../__tests__/useDashboardConfig.spec.ts | 4 +- .../__tests__/useDashboardLabels.spec.ts | 1 + .../__tests__/useDashboardScroller.spec.ts | 4 +- .../composables/useDashboardConfig.ts | 14 +- .../composables/useDashboardLabels.ts | 1 + .../useDashboardPodcastScroller.ts | 35 + .../composables/useDashboardScroller.ts | 6 +- .../components/LibraryCreatorDetails.vue | 45 +- .../components/LibraryCreatorFolders.vue | 47 +- .../components/LibraryCreatorLocalFolders.vue | 134 + .../components/LibraryCreatorModal.vue | 66 +- .../components/LibraryCreatorSchedule.vue | 14 +- .../__tests__/LibraryCreatorFolders.spec.ts | 16 + .../__tests__/LibraryCreatorModal.spec.ts | 30 +- .../__tests__/useLibraries.spec.ts | 1 + .../__tests__/useLibraryCreator.spec.ts | 37 +- .../library/composables/useLibraries.ts | 53 +- .../composables/useLibraryCreationRedirect.ts | 4 +- .../library/composables/useLibraryCreator.ts | 44 +- .../features/library/views/LibrariesView.vue | 7 +- .../lib/notification-category-groups.ts | 20 +- .../AddShowToCollectionSheet.spec.ts | 181 + .../components/AddShowToCollectionSheet.vue | 198 + .../PodcastAcquisitionField.spec.ts | 34 + .../components/PodcastAcquisitionField.vue | 42 + .../components/PodcastAddFeedSheet.spec.ts | 83 + .../components/PodcastAddFeedSheet.vue | 175 + .../podcast/components/PodcastArtwork.vue | 42 + .../components/PodcastBookmarksPanel.spec.ts | 34 + .../components/PodcastBookmarksPanel.vue | 204 + .../components/PodcastDownloadWidget.spec.ts | 90 + .../components/PodcastDownloadWidget.vue | 249 + .../PodcastEditDetailsSheet.spec.ts | 224 + .../components/PodcastEditDetailsSheet.vue | 339 + .../PodcastEpisodeEditSheet.spec.ts | 180 + .../components/PodcastEpisodeEditSheet.vue | 283 + .../PodcastEpisodeQuickView.spec.ts | 155 + .../components/PodcastEpisodeQuickView.vue | 224 + .../PodcastEpisodeResultsPanel.spec.ts | 98 + .../components/PodcastEpisodeResultsPanel.vue | 121 + .../components/PodcastEpisodeRow.spec.ts | 232 + .../podcast/components/PodcastEpisodeRow.vue | 317 + .../components/PodcastEpisodeStatusChips.vue | 26 + .../podcast/components/PodcastFieldLabel.vue | 61 + .../components/PodcastHealthPanel.spec.ts | 61 + .../podcast/components/PodcastHealthPanel.vue | 97 + .../podcast/components/PodcastImportGroup.vue | 42 + .../components/PodcastImportSheet.spec.ts | 280 + .../podcast/components/PodcastImportSheet.vue | 327 + .../podcast/components/PodcastLiveRegion.vue | 9 + .../components/PodcastMiniPlayer.spec.ts | 268 + .../podcast/components/PodcastMiniPlayer.vue | 436 + .../components/PodcastOpmlImportSheet.spec.ts | 60 + .../components/PodcastOpmlImportSheet.vue | 72 + .../components/PodcastPlaylistEditor.spec.ts | 198 + .../components/PodcastPlaylistEditor.vue | 398 + .../components/PodcastPlaylistRuleSummary.vue | 71 + .../components/PodcastQueueList.spec.ts | 132 + .../podcast/components/PodcastQueueList.vue | 222 + .../components/PodcastQueuePanel.spec.ts | 158 + .../podcast/components/PodcastQueuePanel.vue | 310 + .../components/PodcastShortcutsDialog.vue | 57 + .../components/PodcastShowCard.spec.ts | 139 + .../podcast/components/PodcastShowCard.vue | 191 + .../components/PodcastShowManageSheet.spec.ts | 71 + .../components/PodcastShowManageSheet.vue | 159 + .../PodcastShowSettingsSheet.spec.ts | 103 + .../components/PodcastShowSettingsSheet.vue | 110 + .../components/PodcastShowsPanel.spec.ts | 65 + .../podcast/components/PodcastShowsPanel.vue | 106 + .../components/PodcastStatusChip.spec.ts | 32 + .../podcast/components/PodcastStatusChip.vue | 67 + .../podcast/components/PodcastViewHeader.vue | 287 + .../composables/createLockedMetadataForm.ts | 130 + .../composables/podcast-audio-engine.spec.ts | 182 + .../composables/podcast-audio-engine.ts | 224 + .../podcast/composables/podcast-list-lane.ts | 71 + .../composables/podcast-media-session.spec.ts | 147 + .../composables/podcast-media-session.ts | 110 + .../composables/podcast-player-actions.ts | 123 + .../podcast-player-persistence.spec.ts | 131 + .../composables/podcast-player-persistence.ts | 152 + .../composables/podcast-player-preferences.ts | 155 + .../composables/podcast-player-queue.ts | 106 + .../composables/podcast-player-state.spec.ts | 78 + .../composables/podcast-player-state.ts | 72 + .../composables/podcast-sleep-timer.spec.ts | 88 + .../composables/podcast-sleep-timer.ts | 114 + .../composables/useEpisodeQuickView.ts | 33 + .../composables/usePodcastAnnouncer.spec.ts | 49 + .../composables/usePodcastAnnouncer.ts | 30 + .../usePodcastArtworkEditor.spec.ts | 104 + .../composables/usePodcastArtworkEditor.ts | 91 + .../composables/usePodcastBulkDelete.spec.ts | 116 + .../composables/usePodcastBulkDelete.ts | 95 + .../usePodcastDownloadBatches.spec.ts | 211 + .../composables/usePodcastDownloadBatches.ts | 224 + .../composables/usePodcastEpisodeDetail.ts | 57 + .../usePodcastEpisodeListActions.spec.ts | 143 + .../usePodcastEpisodeListActions.ts | 158 + .../usePodcastEpisodeMetadataEditor.spec.ts | 187 + .../usePodcastEpisodeMetadataEditor.ts | 148 + .../composables/usePodcastEvents.spec.ts | 133 + .../podcast/composables/usePodcastEvents.ts | 115 + .../usePodcastFailingFeedCount.spec.ts | 96 + .../composables/usePodcastFailingFeedCount.ts | 50 + .../composables/usePodcastFeedAdd.spec.ts | 144 + .../podcast/composables/usePodcastFeedAdd.ts | 185 + .../composables/usePodcastFeedHealth.spec.ts | 90 + .../composables/usePodcastFeedHealth.ts | 72 + .../composables/usePodcastImport.spec.ts | 80 + .../podcast/composables/usePodcastImport.ts | 215 + .../usePodcastImportProgress.spec.ts | 96 + .../composables/usePodcastImportProgress.ts | 106 + .../usePodcastKeyboardShortcuts.spec.ts | 180 + .../usePodcastKeyboardShortcuts.ts | 90 + .../composables/usePodcastLibraryAccess.ts | 27 + .../composables/usePodcastLibraryPage.ts | 267 + .../usePodcastMetadataEditor.spec.ts | 148 + .../composables/usePodcastMetadataEditor.ts | 80 + .../usePodcastOpmlTransfer.spec.ts | 82 + .../composables/usePodcastOpmlTransfer.ts | 93 + .../composables/usePodcastPlayer.spec.ts | 810 + .../podcast/composables/usePodcastPlayer.ts | 605 + .../composables/usePodcastPlayerDisplay.ts | 84 + .../usePodcastPlaylistPreview.spec.ts | 146 + .../composables/usePodcastPlaylistPreview.ts | 105 + .../composables/usePodcastPlaylists.spec.ts | 139 + .../composables/usePodcastPlaylists.ts | 133 + .../composables/usePodcastQueue.spec.ts | 121 + .../podcast/composables/usePodcastQueue.ts | 107 + .../composables/usePodcastQueueMaintenance.ts | 98 + .../composables/usePodcastSettings.spec.ts | 139 + .../podcast/composables/usePodcastSettings.ts | 173 + .../composables/usePodcastShortcuts.ts | 53 + .../usePodcastShowManagement.spec.ts | 107 + .../composables/usePodcastShowManagement.ts | 458 + .../composables/usePodcastShowPage.spec.ts | 129 + .../podcast/composables/usePodcastShowPage.ts | 297 + .../usePodcastShowSelection.spec.ts | 74 + .../composables/usePodcastShowSelection.ts | 50 + .../podcast/composables/usePodcasts.spec.ts | 344 + .../podcast/composables/usePodcasts.ts | 337 + .../features/podcast/lib/podcast-bulk.spec.ts | 45 + .../src/features/podcast/lib/podcast-bulk.ts | 26 + .../podcast/lib/podcast-episode-api.spec.ts | 51 + .../podcast/lib/podcast-episode-api.ts | 81 + .../podcast/lib/podcast-episode-filters.ts | 28 + .../lib/podcast-episode-presentation.ts | 50 + .../podcast/lib/podcast-format.spec.ts | 71 + .../features/podcast/lib/podcast-format.ts | 33 + .../src/features/podcast/lib/podcast-locks.ts | 10 + .../podcast/lib/podcast-playback-rates.ts | 23 + .../lib/podcast-player-visibility.spec.ts | 27 + .../podcast/lib/podcast-player-visibility.ts | 9 + .../lib/podcast-playlist-rules.spec.ts | 35 + .../podcast/lib/podcast-playlist-rules.ts | 42 + .../podcast/test/audio-engine-stub.ts | 145 + .../podcast/test/episode-action-stubs.ts | 31 + client/src/features/podcast/test/fixtures.ts | 144 + client/src/features/podcast/test/stubs.ts | 400 + .../views/PodcastCollectionView.spec.ts | 257 + .../podcast/views/PodcastCollectionView.vue | 129 + .../podcast/views/PodcastLibrariesView.vue | 57 + .../podcast/views/PodcastLibraryView.spec.ts | 714 + .../podcast/views/PodcastLibraryView.vue | 825 + .../podcast/views/PodcastPlayerView.spec.ts | 259 + .../podcast/views/PodcastPlayerView.vue | 941 + .../podcast/views/PodcastScopeView.spec.ts | 417 + .../podcast/views/PodcastScopeView.vue | 218 + .../podcast/views/PodcastShowView.spec.ts | 656 + .../podcast/views/PodcastShowView.vue | 714 + client/src/features/reader/ReaderView.vue | 992 +- .../features/reader/cbz/CbzReaderView.spec.ts | 1 - .../src/features/reader/cbz/CbzReaderView.vue | 35 +- .../reader/epub/components/ReaderFooter.vue | 26 +- .../reader/epub/components/ReaderHeader.vue | 61 + .../epub/components/ReaderSearchPanel.vue | 11 +- .../reader/epub/components/ReaderSidebar.vue | 34 +- .../reader/epub/components/SelectionPopup.vue | 16 +- .../components/__tests__/ReaderFooter.spec.ts | 25 + .../components/__tests__/ReaderHeader.spec.ts | 19 + .../__tests__/ReaderSearchPanel.spec.ts | 21 + .../__tests__/ReaderSidebar.spec.ts | 23 + .../__tests__/SelectionPopup.spec.ts | 5 + .../composables/__tests__/useFoliate.spec.ts | 16 + .../__tests__/useFoliateInput.spec.ts | 107 + .../__tests__/useFoliateSelection.spec.ts | 5 +- .../__tests__/useReaderSelection.spec.ts | 2 + .../reader/epub/composables/useFoliate.ts | 91 +- .../epub/composables/useFoliateInput.ts | 92 +- .../epub/composables/useFoliateSelection.ts | 3 +- .../epub/composables/useReaderSelection.ts | 4 +- .../components/MediaOverlayMiniPlayer.vue | 102 + .../composables/useMediaOverlay.test.ts | 132 + .../composables/useMediaOverlay.ts | 169 + .../lib/media-overlay-resume.test.ts | 101 + .../media-overlay/lib/media-overlay-resume.ts | 76 + .../__tests__/useVisibility.spec.ts | 81 + .../composables/useReaderProgress.test.ts | 54 + .../shared/composables/useReaderProgress.ts | 54 +- .../shared/composables/useVisibility.ts | 98 +- .../features/settings/LibrariesSettings.vue | 18 +- .../src/features/settings/PodcastSettings.vue | 232 + .../__tests__/LibrariesSettings.spec.ts | 50 +- .../settings/__tests__/admin-tabs.spec.ts | 17 +- .../settings/components/SettingsTabs.spec.ts | 2 +- .../settings/components/SettingsTabs.vue | 75 +- .../src/features/settings/lib/admin-tabs.ts | 6 +- .../src/features/settings/lib/settings-nav.ts | 31 +- .../__tests__/library-ledger-helpers.spec.ts | 3 +- .../components/LibraryAutoFetchConfig.spec.ts | 1 + .../components/CreateSmartScopeDialog.vue | 27 +- .../components/SmartScopeEditorPanel.vue | 3 +- .../__tests__/CreateSmartScopeDialog.spec.ts | 68 +- .../__tests__/SmartScopeEditorPanel.spec.ts | 2 + .../__tests__/useSmartScopeKoboSync.spec.ts | 2 + .../__tests__/useSmartScopes.spec.ts | 50 +- .../smart-scope/composables/useSmartScopes.ts | 127 +- .../smart-scope/views/SmartScopesView.vue | 4 +- .../user/SourceDistributionChart.vue | 2 +- .../__tests__/SourceDistributionChart.spec.ts | 8 +- .../features/statistics/lib/breakdown.test.ts | 6 +- .../lib/source-bucket-colors.test.ts | 10 +- .../statistics/lib/source-bucket-colors.ts | 8 +- .../src/features/tts/TtsAdminSettings.test.ts | 223 + client/src/features/tts/TtsAdminSettings.vue | 272 + .../tts/TtsOpenAiVoiceCuration.test.ts | 287 + .../features/tts/TtsOpenAiVoiceCuration.vue | 340 + client/src/features/tts/TtsSettings.vue | 131 + client/src/features/tts/api/tts.api.ts | 222 + .../tts/components/TtsAdminProviderCard.vue | 275 + .../tts/components/TtsAdminProviderForm.vue | 106 + .../tts/components/TtsAdminVoiceChips.vue | 121 + .../features/tts/components/TtsMiniPlayer.vue | 279 + .../tts/components/TtsMiniPlayerExpanded.vue | 296 + .../tts/components/TtsReaderControls.vue | 34 + .../tts/components/TtsResumePrompt.vue | 101 + .../tts/components/TtsSleepTimerPicker.vue | 46 + .../tts/components/TtsSpeedControl.vue | 37 + .../tts/components/TtsVoicePicker.vue | 217 + .../tts/composables/useFoliateTts.test.ts | 227 + .../features/tts/composables/useFoliateTts.ts | 277 + .../tts/composables/useTtsAdminProviders.ts | 230 + .../tts/composables/useTtsKeyboard.ts | 44 + .../tts/composables/useTtsMediaSession.ts | 44 + .../tts/composables/useTtsMiniPlayerUi.ts | 36 + .../tts/composables/useTtsPlayer.test.ts | 553 + .../features/tts/composables/useTtsPlayer.ts | 445 + .../tts/composables/useTtsPosition.ts | 108 + .../tts/composables/useTtsPreferences.test.ts | 146 + .../tts/composables/useTtsPreferences.ts | 76 + .../tts/composables/useTtsReadingSession.ts | 61 + .../tts/composables/useTtsSleepTimer.test.ts | 93 + .../tts/composables/useTtsSleepTimer.ts | 41 + .../tts/composables/useTtsVoices.test.ts | 157 + .../features/tts/composables/useTtsVoices.ts | 88 + client/src/features/tts/lib/tts-state.ts | 19 + .../features/tts/lib/voice-display.test.ts | 30 + client/src/features/tts/lib/voice-display.ts | 46 + .../features/tts/lib/voice-presets.test.ts | 182 + client/src/features/tts/lib/voice-presets.ts | 78 + client/src/i18n/formatters.spec.ts | 9 +- client/src/i18n/formatters.ts | 22 + client/src/lib/__tests__/api-json.spec.ts | 74 + client/src/lib/__tests__/audio-focus.spec.ts | 47 + client/src/lib/__tests__/entity-count.spec.ts | 36 + client/src/lib/api-json.ts | 73 + client/src/lib/api.ts | 27 +- client/src/lib/async.ts | 54 + client/src/lib/audio-focus.ts | 23 + client/src/lib/entity-count.ts | 27 + client/src/lib/formatting.ts | 22 +- client/src/locales/cs.json | 99 +- client/src/locales/da.json | 34 +- client/src/locales/de.json | 490 +- client/src/locales/en.json | 1044 +- client/src/locales/es.json | 87 + client/src/locales/fi.json | 34 +- client/src/locales/fr.json | 84 + client/src/locales/it.json | 37 +- client/src/locales/nl.json | 732 +- client/src/locales/pl.json | 37 +- client/src/locales/pt.json | 47 +- client/src/locales/ru.json | 34 +- client/src/locales/sl.json | 1031 +- client/src/locales/sv.json | 34 +- client/src/locales/uk.json | 33 +- client/src/locales/zh.json | 77 + .../router/__tests__/podcast-feature.spec.ts | 21 + client/src/router/index.ts | 75 +- client/src/test-setup.ts | 18 + client/src/views/CollectionView.vue | 2 +- client/src/views/DashboardView.vue | 48 +- client/src/views/HomeView.vue | 2 +- client/src/views/SmartScopeView.vue | 2 +- .../views/__tests__/BookDetailView.spec.ts | 11 + client/tsconfig.app.json | 5 +- client/vite.config.ts | 6 +- docker-compose.dev.yml | 21 + docker-compose.yml | 25 + docs/BOOK_REQUESTS.md | 215 - docs/DEVELOPMENT.md | 25 +- docs/whats-new-authoring-guide.md | 128 - docs/whats-new-template.md | 8 - .../bookorbit.koplugin/bookorbit_api.lua | 1 + .../bookorbit_catalog_bulk_download.lua | 15 +- .../bookorbit_catalog_download.lua | 12 +- package.json | 2 + .../reading-session-source-bucket.spec.ts | 13 +- packages/types/src/achievement.ts | 8 + packages/types/src/activity.ts | 202 + packages/types/src/annotation.ts | 1 + packages/types/src/app-features.ts | 7 + packages/types/src/audit.ts | 14 + packages/types/src/auth.ts | 31 +- packages/types/src/book.ts | 26 + packages/types/src/collection.ts | 15 + packages/types/src/dashboard.ts | 66 +- packages/types/src/epub.ts | 44 + packages/types/src/index.ts | 7 + packages/types/src/library.ts | 16 + packages/types/src/media.ts | 17 + packages/types/src/notification.ts | 7 + packages/types/src/permissions.ts | 10 + packages/types/src/podcast.ts | 964 + packages/types/src/query.ts | 2 + .../src/reading-session-source-bucket.ts | 12 +- packages/types/src/reading-session.ts | 7 +- packages/types/src/search.ts | 23 + packages/types/src/shared-reading-insights.ts | 8 +- packages/types/src/sidebar.ts | 19 +- packages/types/src/smart-scope.ts | 37 +- packages/types/src/tts.ts | 120 + packages/types/src/upload.ts | 92 + packages/types/src/watch-download.ts | 43 + server/.env.example | 17 + server/package.json | 2 + server/src/app.module.ts | 24 +- server/src/common/cache/stats-cache.test.ts | 45 + server/src/common/cache/stats-cache.ts | 11 + .../constants/app-settings.constants.ts | 1 + .../common/constants/pagination.constants.ts | 8 + .../src/common/constants/upload.constants.ts | 9 + .../require-library-type.decorator.ts | 8 + .../common/filters/http-exception.filter.ts | 4 +- server/src/common/fs-walk.utils.test.ts | 113 + server/src/common/fs-walk.utils.ts | 119 + .../guards/library-access.guard.test.ts | 67 +- .../src/common/guards/library-access.guard.ts | 32 +- server/src/common/logger.config.ts | 8 +- .../pipes/optional-int-query.pipe.test.ts | 36 + .../common/pipes/optional-int-query.pipe.ts | 21 + server/src/common/podcast-artwork-storage.ts | 24 + .../common/podcast-feed-snapshot-storage.ts | 11 + server/src/common/types/request-user.ts | 1 + .../src/common/utils/bootstrap.utils.test.ts | 30 + server/src/common/utils/bootstrap.utils.ts | 14 + .../utils/fs-stability.utils.test.ts} | 2 +- .../utils/fs-stability.utils.ts} | 4 +- server/src/common/utils/fs-watch.utils.ts | 25 + server/src/common/utils/log-url.utils.test.ts | 27 + server/src/common/utils/log-url.utils.ts | 23 + server/src/config/config.test.ts | 51 +- server/src/config/config.ts | 20 + server/src/config/env.validation.test.ts | 42 + server/src/config/env.validation.ts | 33 + .../0091_add_read_aloud_and_podcasts.sql | 412 + .../0092_narration_progress_columns.sql | 4 + .../0093_add_device_auth_sessions.sql | 23 + .../src/db/migrations/meta/0091_snapshot.json | 21726 +++++++++++++++ .../src/db/migrations/meta/0092_snapshot.json | 21748 +++++++++++++++ .../src/db/migrations/meta/0093_snapshot.json | 21927 ++++++++++++++++ server/src/db/migrations/meta/_journal.json | 21 + server/src/db/schema/achievements.ts | 7 +- server/src/db/schema/auth.ts | 25 + server/src/db/schema/books.ts | 11 + server/src/db/schema/collections.ts | 27 +- server/src/db/schema/index.ts | 3 + server/src/db/schema/libraries.ts | 11 + server/src/db/schema/oidc.ts | 4 +- server/src/db/schema/podcasts.ts | 427 + server/src/db/schema/reader.ts | 51 +- server/src/db/schema/smart-scopes.ts | 24 +- server/src/db/schema/tts.ts | 104 + server/src/db/schema/upload-sessions.ts | 51 + server/src/main.ts | 3 + .../achievement/achievement-events.service.ts | 2 + .../achievement/achievement.controller.ts | 15 +- .../achievement.repository.test.ts | 107 +- .../achievement/achievement.repository.ts | 130 +- .../achievement/achievement.service.test.ts | 63 +- .../achievement/achievement.service.ts | 97 +- .../evaluators/devices.evaluator.test.ts | 19 +- .../evaluators/devices.evaluator.ts | 7 - .../achievement/seed/achievement-seed.ts | 2 +- .../annotation/annotation.service.test.ts | 4 +- .../annotation/dto/annotation-response.dto.ts | 2 + .../app-settings/app-settings.module.ts | 2 +- .../app-settings/app-settings.service.ts | 9 +- .../auth/auth-session-transport.test.ts | 150 + .../auth/auth-session.repository.test.ts | 134 + .../modules/auth/auth-session.repository.ts | 193 + .../modules/auth/auth-session.service.test.ts | 81 + .../src/modules/auth/auth-session.service.ts | 79 + .../src/modules/auth/auth.controller.test.ts | 8 +- server/src/modules/auth/auth.controller.ts | 10 +- server/src/modules/auth/auth.module.ts | 4 + server/src/modules/auth/auth.service.test.ts | 654 +- server/src/modules/auth/auth.service.ts | 402 +- .../src/modules/auth/dto/auth-client.dto.ts | 13 + server/src/modules/auth/dto/login.dto.ts | 3 +- server/src/modules/auth/dto/logout.dto.ts | 3 + .../src/modules/auth/dto/oidc-callback.dto.ts | 3 +- server/src/modules/auth/dto/refresh.dto.ts | 8 + server/src/modules/auth/jwt.strategy.test.ts | 18 +- server/src/modules/auth/jwt.strategy.ts | 4 +- .../oidc/backchannel-logout.service.test.ts | 262 +- .../auth/oidc/backchannel-logout.service.ts | 40 +- .../auth/oidc/oidc-session.repository.test.ts | 209 +- .../auth/oidc/oidc-session.repository.ts | 81 +- .../modules/auth/oidc/oidc.service.test.ts | 37 +- server/src/modules/auth/oidc/oidc.service.ts | 28 +- .../modules/auth/oidc/redirect-uri.test.ts | 62 + server/src/modules/auth/oidc/redirect-uri.ts | 39 + .../src/modules/auth/refresh-token.utils.ts | 11 + .../authors/author-enrichment.gateway.test.ts | 8 +- .../authors/author-enrichment.gateway.ts | 6 +- .../src/modules/authors/authors.repository.ts | 41 +- .../modules/authors/dto/list-authors.dto.ts | 2 +- .../book-dock/book-dock-discovery.test.ts | 4 +- .../book-dock-ingest.service.test.ts | 2 +- .../book-dock/book-dock-ingest.service.ts | 2 +- .../book-dock-watcher.service.test.ts | 5 +- .../book-dock/book-dock-watcher.service.ts | 27 +- .../book-dock/book-dock.gateway.test.ts | 8 +- .../modules/book-dock/book-dock.gateway.ts | 6 +- .../book-metadata-fetch.controller.ts | 5 + .../book-metadata-fetch.gateway.test.ts | 6 +- .../book-metadata-fetch.gateway.ts | 6 +- .../book-request/book-request.gateway.test.ts | 4 +- .../book-request/book-request.gateway.ts | 6 +- .../audiobook-ebook-progress-sync.service.ts | 441 + .../book/audioless-epub.service.test.ts | 567 + .../modules/book/audioless-epub.service.ts | 412 + .../book/book-access-hides-existence.test.ts | 51 + .../book/book-query-builder.service.test.ts | 25 +- .../book/book-query-builder.service.ts | 22 +- .../book/book-sort-builder.service.test.ts | 17 + .../modules/book/book-sort-builder.service.ts | 56 +- .../src/modules/book/book.controller.test.ts | 18 + server/src/modules/book/book.controller.ts | 88 + server/src/modules/book/book.module.test.ts | 8 +- server/src/modules/book/book.module.ts | 17 +- .../src/modules/book/book.repository.test.ts | 80 + server/src/modules/book/book.repository.ts | 326 +- server/src/modules/book/book.service.test.ts | 152 +- server/src/modules/book/book.service.ts | 302 +- .../src/modules/book/dto/book-detail.dto.ts | 4 + .../book/dto/book-dto-validation.test.ts | 15 + .../src/modules/book/dto/save-progress.dto.ts | 23 +- .../update-read-aloud-sync-settings.dto.ts | 8 + .../book/dto/upsert-audio-progress.dto.ts | 16 + .../book/reading-attempt.controller.test.ts | 18 + .../book/reading-attempt.controller.ts | 10 +- .../book/utils/assemble-book-cards.test.ts | 22 + .../modules/book/utils/assemble-book-cards.ts | 40 +- .../catalog/catalog.repository.test.ts | 1 + .../src/modules/catalog/catalog.repository.ts | 2 +- .../collection/collection.controller.ts | 36 +- .../modules/collection/collection.module.ts | 3 +- .../collection/collection.repository.test.ts | 29 +- .../collection/collection.repository.ts | 111 +- .../collection/collection.service.test.ts | 135 +- .../modules/collection/collection.service.ts | 140 +- .../collection/dto/collection-podcasts.dto.ts | 14 + .../collection/dto/create-collection.dto.ts | 9 +- .../dashboard-widget.calculations.ts | 59 + .../dashboard-widget.repository.test.ts | 72 +- .../dashboard/dashboard-widget.repository.ts | 42 +- .../dashboard-widget.service.test.ts | 40 + .../dashboard/dashboard-widget.service.ts | 6 + .../dashboard/dashboard.controller.test.ts | 10 + .../modules/dashboard/dashboard.controller.ts | 6 + .../modules/dashboard/dashboard.repository.ts | 92 + .../dashboard/dashboard.service.test.ts | 34 +- .../modules/dashboard/dashboard.service.ts | 48 +- .../dto/dashboard-scroller-batch.dto.ts | 6 +- .../dashboard/dto/scroller-type.enum.ts | 10 +- .../modules/email/dto/query-send-log.dto.ts | 12 + .../email/email-send-log.controller.test.ts | 9 +- .../email/email-send-log.controller.ts | 2 +- .../email/email-send-log.repository.test.ts | 11 + .../email/email-send-log.repository.ts | 5 +- .../email/email-send-log.service.test.ts | 7 +- .../modules/email/email-send-log.service.ts | 4 +- .../services/kepub-conversion.service.test.ts | 14 + .../kobo/services/kepub-conversion.service.ts | 8 +- .../services/kobo-download.service.test.ts | 155 +- .../kobo/services/kobo-download.service.ts | 89 +- .../kobo/services/kobo-sync.service.test.ts | 14 +- .../kobo/services/kobo-sync.service.ts | 11 +- .../koreader/dto/koreader-plugin.dto.ts | 5 + .../koreader/koreader-catalog.service.test.ts | 77 +- .../koreader/koreader-catalog.service.ts | 25 +- .../koreader/koreader-plugin.service.ts | 42 +- .../koreader/koreader-stats.service.test.ts | 3 +- .../koreader/koreader-stats.service.ts | 1 + .../modules/koreader/koreader.service.test.ts | 2 + .../src/modules/koreader/koreader.service.ts | 6 + .../modules/library/dto/create-library.dto.ts | 18 +- .../modules/library/dto/update-library.dto.ts | 12 + .../library-folder-roles.utils.test.ts | 56 + .../library/library-folder-roles.utils.ts | 56 + .../src/modules/library/library.controller.ts | 8 + .../src/modules/library/library.dto.test.ts | 17 + .../library/library.repository.test.ts | 8 +- .../src/modules/library/library.repository.ts | 161 +- .../modules/library/library.service.test.ts | 297 +- server/src/modules/library/library.service.ts | 352 +- .../metadata-preferences.controller.ts | 4 + .../metadata/extractors/audio.extractor.ts | 25 +- .../migration-progress.gateway.test.ts | 8 +- .../migration/migration-progress.gateway.ts | 6 +- .../notification/notification.repository.ts | 54 +- .../notification/notification.service.test.ts | 35 +- .../notification/notification.service.ts | 34 +- .../opds/__tests__/opds-book.service.test.ts | 16 +- server/src/modules/opds/opds-book.service.ts | 13 +- .../src/modules/path/path-policy.service.ts | 20 +- .../modules/podcast/dto/podcast.dto.test.ts | 359 + server/src/modules/podcast/dto/podcast.dto.ts | 784 + .../podcast/podcast-access.service.test.ts | 77 + .../modules/podcast/podcast-access.service.ts | 59 + .../podcast/podcast-acquisition-rules.ts | 8 + .../podcast/podcast-artwork.service.test.ts | 183 + .../podcast/podcast-artwork.service.ts | 361 + .../podcast/podcast-artwork.storage.test.ts | 92 + .../podcast-catalog.repository.test.ts | 318 + .../podcast/podcast-catalog.repository.ts | 1157 + .../podcast/podcast-catalog.service.test.ts | 452 + .../podcast/podcast-catalog.service.ts | 420 + .../podcast/podcast-directory.service.test.ts | 121 + .../podcast/podcast-directory.service.ts | 160 + .../modules/podcast/podcast-episode-query.ts | 91 + .../podcast-episode.repository.test.ts | 95 + .../podcast/podcast-episode.repository.ts | 720 + .../podcast/podcast-events.service.test.ts | 317 + .../modules/podcast/podcast-events.service.ts | 249 + .../podcast/podcast-events.stream.test.ts | 150 + .../modules/podcast/podcast-feature.test.ts | 48 + .../podcast/podcast-feed-client.service.ts | 129 + .../podcast-feed-parser.service.test.ts | 125 + .../podcast/podcast-feed-parser.service.ts | 373 + .../podcast-feed-snapshot.service.test.ts | 66 + .../podcast/podcast-feed-snapshot.service.ts | 68 + .../podcast/podcast-file-import.service.ts | 765 + .../modules/podcast/podcast-html-sanitizer.ts | 35 + .../podcast/podcast-import-files.test.ts | 81 + .../modules/podcast/podcast-import-files.ts | 121 + .../podcast/podcast-import-matcher.test.ts | 251 + .../modules/podcast/podcast-import-matcher.ts | 367 + .../podcast/podcast-import-services.test.ts | 993 + .../podcast/podcast-job.repository.test.ts | 59 + .../modules/podcast/podcast-job.repository.ts | 430 + .../podcast-local-show-import.service.ts | 626 + .../podcast-local-watcher.service.test.ts | 245 + .../podcast/podcast-local-watcher.service.ts | 199 + .../podcast-media-storage.service.test.ts | 414 + .../podcast/podcast-media-storage.service.ts | 807 + .../modules/podcast/podcast-metadata-rules.ts | 40 + .../podcast-operations.service.test.ts | 920 + .../podcast/podcast-operations.service.ts | 898 + .../podcast/podcast-opml.service.test.ts | 44 + .../modules/podcast/podcast-opml.service.ts | 79 + .../podcast/podcast-origin-guards.test.ts | 123 + .../podcast-playback.repository.test.ts | 468 + .../podcast/podcast-playback.repository.ts | 872 + .../podcast/podcast-playback.service.test.ts | 712 + .../podcast/podcast-playback.service.ts | 393 + .../podcast/podcast-range.utils.test.ts | 30 + .../modules/podcast/podcast-range.utils.ts | 29 + .../podcast/podcast-row-mappers.test.ts | 290 + .../modules/podcast/podcast-row-mappers.ts | 243 + .../modules/podcast/podcast-secret.service.ts | 57 + .../podcast/podcast-sidecar.service.test.ts | 119 + .../podcast/podcast-sidecar.service.ts | 145 + .../podcast/podcast-stream-auth.guard.test.ts | 90 + .../podcast/podcast-stream-auth.guard.ts | 41 + .../podcast-stream-ticket.service.test.ts | 93 + .../podcast/podcast-stream-ticket.service.ts | 77 + .../podcast-tag-reader.service.test.ts | 146 + .../podcast/podcast-tag-reader.service.ts | 349 + .../podcast-url-security.service.test.ts | 42 + .../podcast/podcast-url-security.service.ts | 179 + .../podcast/podcast-worker.service.test.ts | 493 + .../modules/podcast/podcast-worker.service.ts | 404 + .../podcast/podcast.controller.test.ts | 177 + .../src/modules/podcast/podcast.controller.ts | 771 + .../modules/podcast/podcast.gateway.test.ts | 120 + server/src/modules/podcast/podcast.gateway.ts | 116 + server/src/modules/podcast/podcast.module.ts | 115 + server/src/modules/podcast/podcast.types.ts | 44 + .../position-converter.service.ts | 110 +- .../epub/epub-media-overlay-capability.ts | 57 + .../modules/reader/epub/epub-media-overlay.ts | 399 + .../reader/epub/epub.controller.test.ts | 35 + .../modules/reader/epub/epub.controller.ts | 38 +- server/src/modules/reader/epub/epub.module.ts | 1 + .../modules/reader/epub/epub.service.test.ts | 104 + .../src/modules/reader/epub/epub.service.ts | 111 +- .../dto/save-reading-session.dto.test.ts | 21 + .../dto/save-reading-session.dto.ts | 15 +- .../reading-session.controller.test.ts | 20 +- .../reading-session.controller.ts | 2 +- .../reading-session.module.test.ts | 3 +- .../reading-session/reading-session.module.ts | 3 +- .../reading-session.repository.test.ts | 90 +- .../reading-session.repository.ts | 56 +- .../reading-session.service.test.ts | 31 +- .../reading-session.service.ts | 8 +- .../reading-state.controller.test.ts | 9 +- .../reading-state/reading-state.controller.ts | 9 +- .../scanner/file-event-processor.service.ts | 32 +- .../modules/scanner/file-watcher.service.ts | 25 +- server/src/modules/scanner/lib/walk.ts | 169 +- .../src/modules/scanner/scan.gateway.test.ts | 6 +- server/src/modules/scanner/scan.gateway.ts | 6 +- .../src/modules/scanner/scanner.controller.ts | 2 + .../src/modules/scanner/scanner.repository.ts | 12 +- .../modules/scanner/scanner.service.test.ts | 38 +- server/src/modules/scanner/scanner.service.ts | 46 +- .../search/dto/global-search.dto.test.ts | 23 + .../modules/search/dto/global-search.dto.ts | 23 + .../modules/search/search.controller.test.ts | 16 + .../src/modules/search/search.controller.ts | 16 + server/src/modules/search/search.module.ts | 14 + .../src/modules/search/search.service.test.ts | 82 + server/src/modules/search/search.service.ts | 145 + .../src/modules/seed/cleanup.service.test.ts | 16 +- server/src/modules/seed/cleanup.service.ts | 10 +- .../src/modules/series/dto/list-series.dto.ts | 2 +- .../src/modules/series/series.repository.ts | 50 +- .../shared-reading-insights.repository.ts | 4 +- .../smart-scope/dto/create-smart-scope.dto.ts | 24 +- .../smart-scope/dto/update-smart-scope.dto.ts | 4 +- .../smart-scope/smart-scope.controller.ts | 20 +- .../modules/smart-scope/smart-scope.module.ts | 3 +- .../smart-scope.repository.test.ts | 17 +- .../smart-scope/smart-scope.repository.ts | 38 +- .../smart-scope/smart-scope.service.test.ts | 237 +- .../smart-scope/smart-scope.service.ts | 200 +- .../utils/podcast-playlist-migration.test.ts | 102 + .../utils/podcast-playlist-migration.ts | 100 + .../podcast-scope-rules.validator.test.ts | 74 + .../utils/podcast-scope-rules.validator.ts | 101 + server/src/modules/tts/dto/synthesize.dto.ts | 38 + server/src/modules/tts/dto/tts-admin.dto.ts | 117 + .../src/modules/tts/dto/tts-position.dto.ts | 11 + .../modules/tts/dto/tts-preferences.dto.ts | 17 + .../openai-compatible.provider.test.ts | 584 + .../providers/openai-compatible.provider.ts | 429 + .../tts/providers/tts-provider.factory.ts | 30 + .../tts/providers/tts-provider.interface.ts | 21 + .../src/modules/tts/tts-admin.controller.ts | 50 + .../src/modules/tts/tts-admin.service.test.ts | 322 + server/src/modules/tts/tts-admin.service.ts | 149 + server/src/modules/tts/tts-audio-format.ts | 26 + .../modules/tts/tts-synthesis.service.test.ts | 176 + .../src/modules/tts/tts-synthesis.service.ts | 169 + .../tts/tts-text-extractor.service.test.ts | 75 + .../modules/tts/tts-text-extractor.service.ts | 184 + server/src/modules/tts/tts.controller.test.ts | 166 + server/src/modules/tts/tts.controller.ts | 102 + server/src/modules/tts/tts.module.ts | 18 + server/src/modules/tts/tts.repository.ts | 126 + server/src/modules/tts/tts.service.test.ts | 438 + server/src/modules/tts/tts.service.ts | 277 + .../upload/dto/create-upload-session.dto.ts | 53 + server/src/modules/upload/upload-errors.ts | 33 + .../upload/upload-processor.service.test.ts | 18 +- .../upload/upload-processor.service.ts | 72 +- .../upload/upload-session.controller.test.ts | 55 + .../upload/upload-session.controller.ts | 61 + .../upload/upload-session.repository.ts | 137 + .../upload/upload-session.service.test.ts | 271 + .../modules/upload/upload-session.service.ts | 464 + .../modules/upload/upload-storage.service.ts | 46 +- .../upload/upload-validator.service.test.ts | 61 +- .../upload/upload-validator.service.ts | 87 +- .../src/modules/upload/upload.controller.ts | 2 + .../src/modules/upload/upload.module.test.ts | 8 +- server/src/modules/upload/upload.module.ts | 10 +- .../src/modules/upload/upload.service.test.ts | 5 + server/src/modules/upload/upload.service.ts | 118 +- .../user-preferences.controller.ts | 24 + .../user-preferences.service.test.ts | 55 + .../user-preferences.service.ts | 78 + .../dto/activity-calendar-query.dto.ts | 3 + .../activity-completion-speed-query.dto.ts | 10 + .../dto/activity-day-query.dto.ts | 3 + .../dto/activity-pace-query.dto.ts | 15 + .../user-statistics.controller.test.ts | 21 + .../user-statistics.controller.ts | 39 + .../user-statistics.repository.ts | 356 +- .../user-statistics.service.test.ts | 343 +- .../user-statistics.service.ts | 591 +- .../dto/create-watch-download.dto.ts | 14 + ...create-watch-media-overlay-download.dto.ts | 18 + .../watch-download.controller.ts | 127 + .../watch-download/watch-download.module.ts | 29 + .../watch-download.service.test.ts | 195 + .../watch-download/watch-download.service.ts | 230 + server/src/scripts/README.md | 1 + .../migrate-podcast-playlists-to-scopes.ts | 113 + .../src/scripts/register-kokoro-provider.ts | 65 + server/src/scripts/seed-apple-source-stats.ts | 310 + .../auth-recovery-oidc-logout.e2e-spec.ts | 168 +- server/test/authorization-matrix.e2e-spec.ts | 200 +- server/test/e2e.setup.ts | 3 +- server/test/e2e/app-harness.ts | 4 +- .../authorization-matrix-harness.ts | 2 + .../route-inventory/manifest.json | 26 +- .../route-inventory/routes-02.json | 24 + .../route-inventory/routes-09.json | 782 + .../route-inventory/routes-10.json | 770 + 844 files changed, 151839 insertions(+), 3415 deletions(-) create mode 100644 client/public/assets/foliate/overlayer.d.ts create mode 100644 client/public/assets/foliate/text-walker.d.ts create mode 100644 client/public/assets/foliate/tts.d.ts create mode 100644 client/src/components/ChipGroup.vue create mode 100644 client/src/components/EmptyState.vue create mode 100644 client/src/components/FilterChip.vue create mode 100644 client/src/components/FormSheet.vue create mode 100644 client/src/components/ListEndFooter.vue create mode 100644 client/src/components/ListSkeleton.vue create mode 100644 client/src/components/LoadErrorPanel.vue create mode 100644 client/src/components/__tests__/EmptyState.spec.ts create mode 100644 client/src/components/__tests__/FormSheet.spec.ts create mode 100644 client/src/components/entity-index/__tests__/EntityIndexView.spec.ts create mode 100644 client/src/components/media/MediaScrubber.vue create mode 100644 client/src/components/media/MediaTransport.vue create mode 100644 client/src/components/media/MediaVolumeControl.vue create mode 100644 client/src/components/media/__tests__/MediaScrubber.spec.ts create mode 100644 client/src/components/media/__tests__/MediaTransport.spec.ts create mode 100644 client/src/components/media/__tests__/MediaVolumeControl.spec.ts create mode 100644 client/src/components/media/types.ts create mode 100644 client/src/components/sidebar/SidebarModeSwitch.vue create mode 100644 client/src/components/sidebar/__tests__/sidebar-order.spec.ts create mode 100644 client/src/components/sidebar/sidebar-order.ts create mode 100644 client/src/components/ui/Chip.vue create mode 100644 client/src/components/ui/__tests__/ConfirmDialog.spec.ts create mode 100644 client/src/components/ui/select/Select.vue create mode 100644 client/src/components/ui/select/index.ts create mode 100644 client/src/components/ui/textarea/Textarea.vue create mode 100644 client/src/components/ui/textarea/index.ts create mode 100644 client/src/composables/__tests__/useMediaMode.spec.ts create mode 100644 client/src/composables/useFilePicker.ts rename client/src/{features/book => }/composables/useInfiniteScrollSentinel.ts (100%) create mode 100644 client/src/composables/useMediaMode.ts create mode 100644 client/src/composables/usePagedEntityMembers.ts create mode 100644 client/src/composables/useScrollOverflow.ts create mode 100644 client/src/features/book/lib/file-capabilities.ts create mode 100644 client/src/features/collection/components/CollectionMembershipList.vue create mode 100644 client/src/features/collection/components/__tests__/CreateCollectionDialog.spec.ts create mode 100644 client/src/features/dashboard/components/DashboardPodcastScroller.spec.ts create mode 100644 client/src/features/dashboard/components/DashboardPodcastScroller.vue create mode 100644 client/src/features/dashboard/composables/useDashboardPodcastScroller.ts create mode 100644 client/src/features/library/components/LibraryCreatorLocalFolders.vue create mode 100644 client/src/features/podcast/components/AddShowToCollectionSheet.spec.ts create mode 100644 client/src/features/podcast/components/AddShowToCollectionSheet.vue create mode 100644 client/src/features/podcast/components/PodcastAcquisitionField.spec.ts create mode 100644 client/src/features/podcast/components/PodcastAcquisitionField.vue create mode 100644 client/src/features/podcast/components/PodcastAddFeedSheet.spec.ts create mode 100644 client/src/features/podcast/components/PodcastAddFeedSheet.vue create mode 100644 client/src/features/podcast/components/PodcastArtwork.vue create mode 100644 client/src/features/podcast/components/PodcastBookmarksPanel.spec.ts create mode 100644 client/src/features/podcast/components/PodcastBookmarksPanel.vue create mode 100644 client/src/features/podcast/components/PodcastDownloadWidget.spec.ts create mode 100644 client/src/features/podcast/components/PodcastDownloadWidget.vue create mode 100644 client/src/features/podcast/components/PodcastEditDetailsSheet.spec.ts create mode 100644 client/src/features/podcast/components/PodcastEditDetailsSheet.vue create mode 100644 client/src/features/podcast/components/PodcastEpisodeEditSheet.spec.ts create mode 100644 client/src/features/podcast/components/PodcastEpisodeEditSheet.vue create mode 100644 client/src/features/podcast/components/PodcastEpisodeQuickView.spec.ts create mode 100644 client/src/features/podcast/components/PodcastEpisodeQuickView.vue create mode 100644 client/src/features/podcast/components/PodcastEpisodeResultsPanel.spec.ts create mode 100644 client/src/features/podcast/components/PodcastEpisodeResultsPanel.vue create mode 100644 client/src/features/podcast/components/PodcastEpisodeRow.spec.ts create mode 100644 client/src/features/podcast/components/PodcastEpisodeRow.vue create mode 100644 client/src/features/podcast/components/PodcastEpisodeStatusChips.vue create mode 100644 client/src/features/podcast/components/PodcastFieldLabel.vue create mode 100644 client/src/features/podcast/components/PodcastHealthPanel.spec.ts create mode 100644 client/src/features/podcast/components/PodcastHealthPanel.vue create mode 100644 client/src/features/podcast/components/PodcastImportGroup.vue create mode 100644 client/src/features/podcast/components/PodcastImportSheet.spec.ts create mode 100644 client/src/features/podcast/components/PodcastImportSheet.vue create mode 100644 client/src/features/podcast/components/PodcastLiveRegion.vue create mode 100644 client/src/features/podcast/components/PodcastMiniPlayer.spec.ts create mode 100644 client/src/features/podcast/components/PodcastMiniPlayer.vue create mode 100644 client/src/features/podcast/components/PodcastOpmlImportSheet.spec.ts create mode 100644 client/src/features/podcast/components/PodcastOpmlImportSheet.vue create mode 100644 client/src/features/podcast/components/PodcastPlaylistEditor.spec.ts create mode 100644 client/src/features/podcast/components/PodcastPlaylistEditor.vue create mode 100644 client/src/features/podcast/components/PodcastPlaylistRuleSummary.vue create mode 100644 client/src/features/podcast/components/PodcastQueueList.spec.ts create mode 100644 client/src/features/podcast/components/PodcastQueueList.vue create mode 100644 client/src/features/podcast/components/PodcastQueuePanel.spec.ts create mode 100644 client/src/features/podcast/components/PodcastQueuePanel.vue create mode 100644 client/src/features/podcast/components/PodcastShortcutsDialog.vue create mode 100644 client/src/features/podcast/components/PodcastShowCard.spec.ts create mode 100644 client/src/features/podcast/components/PodcastShowCard.vue create mode 100644 client/src/features/podcast/components/PodcastShowManageSheet.spec.ts create mode 100644 client/src/features/podcast/components/PodcastShowManageSheet.vue create mode 100644 client/src/features/podcast/components/PodcastShowSettingsSheet.spec.ts create mode 100644 client/src/features/podcast/components/PodcastShowSettingsSheet.vue create mode 100644 client/src/features/podcast/components/PodcastShowsPanel.spec.ts create mode 100644 client/src/features/podcast/components/PodcastShowsPanel.vue create mode 100644 client/src/features/podcast/components/PodcastStatusChip.spec.ts create mode 100644 client/src/features/podcast/components/PodcastStatusChip.vue create mode 100644 client/src/features/podcast/components/PodcastViewHeader.vue create mode 100644 client/src/features/podcast/composables/createLockedMetadataForm.ts create mode 100644 client/src/features/podcast/composables/podcast-audio-engine.spec.ts create mode 100644 client/src/features/podcast/composables/podcast-audio-engine.ts create mode 100644 client/src/features/podcast/composables/podcast-list-lane.ts create mode 100644 client/src/features/podcast/composables/podcast-media-session.spec.ts create mode 100644 client/src/features/podcast/composables/podcast-media-session.ts create mode 100644 client/src/features/podcast/composables/podcast-player-actions.ts create mode 100644 client/src/features/podcast/composables/podcast-player-persistence.spec.ts create mode 100644 client/src/features/podcast/composables/podcast-player-persistence.ts create mode 100644 client/src/features/podcast/composables/podcast-player-preferences.ts create mode 100644 client/src/features/podcast/composables/podcast-player-queue.ts create mode 100644 client/src/features/podcast/composables/podcast-player-state.spec.ts create mode 100644 client/src/features/podcast/composables/podcast-player-state.ts create mode 100644 client/src/features/podcast/composables/podcast-sleep-timer.spec.ts create mode 100644 client/src/features/podcast/composables/podcast-sleep-timer.ts create mode 100644 client/src/features/podcast/composables/useEpisodeQuickView.ts create mode 100644 client/src/features/podcast/composables/usePodcastAnnouncer.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastAnnouncer.ts create mode 100644 client/src/features/podcast/composables/usePodcastArtworkEditor.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastArtworkEditor.ts create mode 100644 client/src/features/podcast/composables/usePodcastBulkDelete.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastBulkDelete.ts create mode 100644 client/src/features/podcast/composables/usePodcastDownloadBatches.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastDownloadBatches.ts create mode 100644 client/src/features/podcast/composables/usePodcastEpisodeDetail.ts create mode 100644 client/src/features/podcast/composables/usePodcastEpisodeListActions.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastEpisodeListActions.ts create mode 100644 client/src/features/podcast/composables/usePodcastEpisodeMetadataEditor.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastEpisodeMetadataEditor.ts create mode 100644 client/src/features/podcast/composables/usePodcastEvents.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastEvents.ts create mode 100644 client/src/features/podcast/composables/usePodcastFailingFeedCount.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastFailingFeedCount.ts create mode 100644 client/src/features/podcast/composables/usePodcastFeedAdd.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastFeedAdd.ts create mode 100644 client/src/features/podcast/composables/usePodcastFeedHealth.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastFeedHealth.ts create mode 100644 client/src/features/podcast/composables/usePodcastImport.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastImport.ts create mode 100644 client/src/features/podcast/composables/usePodcastImportProgress.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastImportProgress.ts create mode 100644 client/src/features/podcast/composables/usePodcastKeyboardShortcuts.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastKeyboardShortcuts.ts create mode 100644 client/src/features/podcast/composables/usePodcastLibraryAccess.ts create mode 100644 client/src/features/podcast/composables/usePodcastLibraryPage.ts create mode 100644 client/src/features/podcast/composables/usePodcastMetadataEditor.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastMetadataEditor.ts create mode 100644 client/src/features/podcast/composables/usePodcastOpmlTransfer.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastOpmlTransfer.ts create mode 100644 client/src/features/podcast/composables/usePodcastPlayer.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastPlayer.ts create mode 100644 client/src/features/podcast/composables/usePodcastPlayerDisplay.ts create mode 100644 client/src/features/podcast/composables/usePodcastPlaylistPreview.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastPlaylistPreview.ts create mode 100644 client/src/features/podcast/composables/usePodcastPlaylists.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastPlaylists.ts create mode 100644 client/src/features/podcast/composables/usePodcastQueue.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastQueue.ts create mode 100644 client/src/features/podcast/composables/usePodcastQueueMaintenance.ts create mode 100644 client/src/features/podcast/composables/usePodcastSettings.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastSettings.ts create mode 100644 client/src/features/podcast/composables/usePodcastShortcuts.ts create mode 100644 client/src/features/podcast/composables/usePodcastShowManagement.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastShowManagement.ts create mode 100644 client/src/features/podcast/composables/usePodcastShowPage.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastShowPage.ts create mode 100644 client/src/features/podcast/composables/usePodcastShowSelection.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcastShowSelection.ts create mode 100644 client/src/features/podcast/composables/usePodcasts.spec.ts create mode 100644 client/src/features/podcast/composables/usePodcasts.ts create mode 100644 client/src/features/podcast/lib/podcast-bulk.spec.ts create mode 100644 client/src/features/podcast/lib/podcast-bulk.ts create mode 100644 client/src/features/podcast/lib/podcast-episode-api.spec.ts create mode 100644 client/src/features/podcast/lib/podcast-episode-api.ts create mode 100644 client/src/features/podcast/lib/podcast-episode-filters.ts create mode 100644 client/src/features/podcast/lib/podcast-episode-presentation.ts create mode 100644 client/src/features/podcast/lib/podcast-format.spec.ts create mode 100644 client/src/features/podcast/lib/podcast-format.ts create mode 100644 client/src/features/podcast/lib/podcast-locks.ts create mode 100644 client/src/features/podcast/lib/podcast-playback-rates.ts create mode 100644 client/src/features/podcast/lib/podcast-player-visibility.spec.ts create mode 100644 client/src/features/podcast/lib/podcast-player-visibility.ts create mode 100644 client/src/features/podcast/lib/podcast-playlist-rules.spec.ts create mode 100644 client/src/features/podcast/lib/podcast-playlist-rules.ts create mode 100644 client/src/features/podcast/test/audio-engine-stub.ts create mode 100644 client/src/features/podcast/test/episode-action-stubs.ts create mode 100644 client/src/features/podcast/test/fixtures.ts create mode 100644 client/src/features/podcast/test/stubs.ts create mode 100644 client/src/features/podcast/views/PodcastCollectionView.spec.ts create mode 100644 client/src/features/podcast/views/PodcastCollectionView.vue create mode 100644 client/src/features/podcast/views/PodcastLibrariesView.vue create mode 100644 client/src/features/podcast/views/PodcastLibraryView.spec.ts create mode 100644 client/src/features/podcast/views/PodcastLibraryView.vue create mode 100644 client/src/features/podcast/views/PodcastPlayerView.spec.ts create mode 100644 client/src/features/podcast/views/PodcastPlayerView.vue create mode 100644 client/src/features/podcast/views/PodcastScopeView.spec.ts create mode 100644 client/src/features/podcast/views/PodcastScopeView.vue create mode 100644 client/src/features/podcast/views/PodcastShowView.spec.ts create mode 100644 client/src/features/podcast/views/PodcastShowView.vue create mode 100644 client/src/features/reader/media-overlay/components/MediaOverlayMiniPlayer.vue create mode 100644 client/src/features/reader/media-overlay/composables/useMediaOverlay.test.ts create mode 100644 client/src/features/reader/media-overlay/composables/useMediaOverlay.ts create mode 100644 client/src/features/reader/media-overlay/lib/media-overlay-resume.test.ts create mode 100644 client/src/features/reader/media-overlay/lib/media-overlay-resume.ts create mode 100644 client/src/features/reader/shared/composables/__tests__/useVisibility.spec.ts create mode 100644 client/src/features/settings/PodcastSettings.vue create mode 100644 client/src/features/tts/TtsAdminSettings.test.ts create mode 100644 client/src/features/tts/TtsAdminSettings.vue create mode 100644 client/src/features/tts/TtsOpenAiVoiceCuration.test.ts create mode 100644 client/src/features/tts/TtsOpenAiVoiceCuration.vue create mode 100644 client/src/features/tts/TtsSettings.vue create mode 100644 client/src/features/tts/api/tts.api.ts create mode 100644 client/src/features/tts/components/TtsAdminProviderCard.vue create mode 100644 client/src/features/tts/components/TtsAdminProviderForm.vue create mode 100644 client/src/features/tts/components/TtsAdminVoiceChips.vue create mode 100644 client/src/features/tts/components/TtsMiniPlayer.vue create mode 100644 client/src/features/tts/components/TtsMiniPlayerExpanded.vue create mode 100644 client/src/features/tts/components/TtsReaderControls.vue create mode 100644 client/src/features/tts/components/TtsResumePrompt.vue create mode 100644 client/src/features/tts/components/TtsSleepTimerPicker.vue create mode 100644 client/src/features/tts/components/TtsSpeedControl.vue create mode 100644 client/src/features/tts/components/TtsVoicePicker.vue create mode 100644 client/src/features/tts/composables/useFoliateTts.test.ts create mode 100644 client/src/features/tts/composables/useFoliateTts.ts create mode 100644 client/src/features/tts/composables/useTtsAdminProviders.ts create mode 100644 client/src/features/tts/composables/useTtsKeyboard.ts create mode 100644 client/src/features/tts/composables/useTtsMediaSession.ts create mode 100644 client/src/features/tts/composables/useTtsMiniPlayerUi.ts create mode 100644 client/src/features/tts/composables/useTtsPlayer.test.ts create mode 100644 client/src/features/tts/composables/useTtsPlayer.ts create mode 100644 client/src/features/tts/composables/useTtsPosition.ts create mode 100644 client/src/features/tts/composables/useTtsPreferences.test.ts create mode 100644 client/src/features/tts/composables/useTtsPreferences.ts create mode 100644 client/src/features/tts/composables/useTtsReadingSession.ts create mode 100644 client/src/features/tts/composables/useTtsSleepTimer.test.ts create mode 100644 client/src/features/tts/composables/useTtsSleepTimer.ts create mode 100644 client/src/features/tts/composables/useTtsVoices.test.ts create mode 100644 client/src/features/tts/composables/useTtsVoices.ts create mode 100644 client/src/features/tts/lib/tts-state.ts create mode 100644 client/src/features/tts/lib/voice-display.test.ts create mode 100644 client/src/features/tts/lib/voice-display.ts create mode 100644 client/src/features/tts/lib/voice-presets.test.ts create mode 100644 client/src/features/tts/lib/voice-presets.ts create mode 100644 client/src/lib/__tests__/api-json.spec.ts create mode 100644 client/src/lib/__tests__/audio-focus.spec.ts create mode 100644 client/src/lib/__tests__/entity-count.spec.ts create mode 100644 client/src/lib/api-json.ts create mode 100644 client/src/lib/async.ts create mode 100644 client/src/lib/audio-focus.ts create mode 100644 client/src/lib/entity-count.ts create mode 100644 client/src/router/__tests__/podcast-feature.spec.ts delete mode 100644 docs/BOOK_REQUESTS.md delete mode 100644 docs/whats-new-authoring-guide.md delete mode 100644 docs/whats-new-template.md create mode 100644 packages/types/src/activity.ts create mode 100644 packages/types/src/app-features.ts create mode 100644 packages/types/src/media.ts create mode 100644 packages/types/src/podcast.ts create mode 100644 packages/types/src/search.ts create mode 100644 packages/types/src/tts.ts create mode 100644 packages/types/src/watch-download.ts create mode 100644 server/src/common/constants/upload.constants.ts create mode 100644 server/src/common/decorators/require-library-type.decorator.ts create mode 100644 server/src/common/fs-walk.utils.test.ts create mode 100644 server/src/common/fs-walk.utils.ts create mode 100644 server/src/common/pipes/optional-int-query.pipe.test.ts create mode 100644 server/src/common/pipes/optional-int-query.pipe.ts create mode 100644 server/src/common/podcast-artwork-storage.ts create mode 100644 server/src/common/podcast-feed-snapshot-storage.ts rename server/src/{modules/scanner/lib/stability.test.ts => common/utils/fs-stability.utils.test.ts} (98%) rename server/src/{modules/scanner/lib/stability.ts => common/utils/fs-stability.utils.ts} (96%) create mode 100644 server/src/common/utils/fs-watch.utils.ts create mode 100644 server/src/common/utils/log-url.utils.test.ts create mode 100644 server/src/common/utils/log-url.utils.ts create mode 100644 server/src/db/migrations/0091_add_read_aloud_and_podcasts.sql create mode 100644 server/src/db/migrations/0092_narration_progress_columns.sql create mode 100644 server/src/db/migrations/0093_add_device_auth_sessions.sql create mode 100644 server/src/db/migrations/meta/0091_snapshot.json create mode 100644 server/src/db/migrations/meta/0092_snapshot.json create mode 100644 server/src/db/migrations/meta/0093_snapshot.json create mode 100644 server/src/db/schema/podcasts.ts create mode 100644 server/src/db/schema/tts.ts create mode 100644 server/src/db/schema/upload-sessions.ts create mode 100644 server/src/modules/auth/auth-session-transport.test.ts create mode 100644 server/src/modules/auth/auth-session.repository.test.ts create mode 100644 server/src/modules/auth/auth-session.repository.ts create mode 100644 server/src/modules/auth/auth-session.service.test.ts create mode 100644 server/src/modules/auth/auth-session.service.ts create mode 100644 server/src/modules/auth/dto/auth-client.dto.ts create mode 100644 server/src/modules/auth/dto/logout.dto.ts create mode 100644 server/src/modules/auth/dto/refresh.dto.ts create mode 100644 server/src/modules/auth/oidc/redirect-uri.test.ts create mode 100644 server/src/modules/auth/oidc/redirect-uri.ts create mode 100644 server/src/modules/auth/refresh-token.utils.ts create mode 100644 server/src/modules/book/audiobook-ebook-progress-sync.service.ts create mode 100644 server/src/modules/book/audioless-epub.service.test.ts create mode 100644 server/src/modules/book/audioless-epub.service.ts create mode 100644 server/src/modules/book/book-access-hides-existence.test.ts create mode 100644 server/src/modules/book/dto/update-read-aloud-sync-settings.dto.ts create mode 100644 server/src/modules/book/dto/upsert-audio-progress.dto.ts create mode 100644 server/src/modules/collection/dto/collection-podcasts.dto.ts create mode 100644 server/src/modules/library/library-folder-roles.utils.test.ts create mode 100644 server/src/modules/library/library-folder-roles.utils.ts create mode 100644 server/src/modules/podcast/dto/podcast.dto.test.ts create mode 100644 server/src/modules/podcast/dto/podcast.dto.ts create mode 100644 server/src/modules/podcast/podcast-access.service.test.ts create mode 100644 server/src/modules/podcast/podcast-access.service.ts create mode 100644 server/src/modules/podcast/podcast-acquisition-rules.ts create mode 100644 server/src/modules/podcast/podcast-artwork.service.test.ts create mode 100644 server/src/modules/podcast/podcast-artwork.service.ts create mode 100644 server/src/modules/podcast/podcast-artwork.storage.test.ts create mode 100644 server/src/modules/podcast/podcast-catalog.repository.test.ts create mode 100644 server/src/modules/podcast/podcast-catalog.repository.ts create mode 100644 server/src/modules/podcast/podcast-catalog.service.test.ts create mode 100644 server/src/modules/podcast/podcast-catalog.service.ts create mode 100644 server/src/modules/podcast/podcast-directory.service.test.ts create mode 100644 server/src/modules/podcast/podcast-directory.service.ts create mode 100644 server/src/modules/podcast/podcast-episode-query.ts create mode 100644 server/src/modules/podcast/podcast-episode.repository.test.ts create mode 100644 server/src/modules/podcast/podcast-episode.repository.ts create mode 100644 server/src/modules/podcast/podcast-events.service.test.ts create mode 100644 server/src/modules/podcast/podcast-events.service.ts create mode 100644 server/src/modules/podcast/podcast-events.stream.test.ts create mode 100644 server/src/modules/podcast/podcast-feature.test.ts create mode 100644 server/src/modules/podcast/podcast-feed-client.service.ts create mode 100644 server/src/modules/podcast/podcast-feed-parser.service.test.ts create mode 100644 server/src/modules/podcast/podcast-feed-parser.service.ts create mode 100644 server/src/modules/podcast/podcast-feed-snapshot.service.test.ts create mode 100644 server/src/modules/podcast/podcast-feed-snapshot.service.ts create mode 100644 server/src/modules/podcast/podcast-file-import.service.ts create mode 100644 server/src/modules/podcast/podcast-html-sanitizer.ts create mode 100644 server/src/modules/podcast/podcast-import-files.test.ts create mode 100644 server/src/modules/podcast/podcast-import-files.ts create mode 100644 server/src/modules/podcast/podcast-import-matcher.test.ts create mode 100644 server/src/modules/podcast/podcast-import-matcher.ts create mode 100644 server/src/modules/podcast/podcast-import-services.test.ts create mode 100644 server/src/modules/podcast/podcast-job.repository.test.ts create mode 100644 server/src/modules/podcast/podcast-job.repository.ts create mode 100644 server/src/modules/podcast/podcast-local-show-import.service.ts create mode 100644 server/src/modules/podcast/podcast-local-watcher.service.test.ts create mode 100644 server/src/modules/podcast/podcast-local-watcher.service.ts create mode 100644 server/src/modules/podcast/podcast-media-storage.service.test.ts create mode 100644 server/src/modules/podcast/podcast-media-storage.service.ts create mode 100644 server/src/modules/podcast/podcast-metadata-rules.ts create mode 100644 server/src/modules/podcast/podcast-operations.service.test.ts create mode 100644 server/src/modules/podcast/podcast-operations.service.ts create mode 100644 server/src/modules/podcast/podcast-opml.service.test.ts create mode 100644 server/src/modules/podcast/podcast-opml.service.ts create mode 100644 server/src/modules/podcast/podcast-origin-guards.test.ts create mode 100644 server/src/modules/podcast/podcast-playback.repository.test.ts create mode 100644 server/src/modules/podcast/podcast-playback.repository.ts create mode 100644 server/src/modules/podcast/podcast-playback.service.test.ts create mode 100644 server/src/modules/podcast/podcast-playback.service.ts create mode 100644 server/src/modules/podcast/podcast-range.utils.test.ts create mode 100644 server/src/modules/podcast/podcast-range.utils.ts create mode 100644 server/src/modules/podcast/podcast-row-mappers.test.ts create mode 100644 server/src/modules/podcast/podcast-row-mappers.ts create mode 100644 server/src/modules/podcast/podcast-secret.service.ts create mode 100644 server/src/modules/podcast/podcast-sidecar.service.test.ts create mode 100644 server/src/modules/podcast/podcast-sidecar.service.ts create mode 100644 server/src/modules/podcast/podcast-stream-auth.guard.test.ts create mode 100644 server/src/modules/podcast/podcast-stream-auth.guard.ts create mode 100644 server/src/modules/podcast/podcast-stream-ticket.service.test.ts create mode 100644 server/src/modules/podcast/podcast-stream-ticket.service.ts create mode 100644 server/src/modules/podcast/podcast-tag-reader.service.test.ts create mode 100644 server/src/modules/podcast/podcast-tag-reader.service.ts create mode 100644 server/src/modules/podcast/podcast-url-security.service.test.ts create mode 100644 server/src/modules/podcast/podcast-url-security.service.ts create mode 100644 server/src/modules/podcast/podcast-worker.service.test.ts create mode 100644 server/src/modules/podcast/podcast-worker.service.ts create mode 100644 server/src/modules/podcast/podcast.controller.test.ts create mode 100644 server/src/modules/podcast/podcast.controller.ts create mode 100644 server/src/modules/podcast/podcast.gateway.test.ts create mode 100644 server/src/modules/podcast/podcast.gateway.ts create mode 100644 server/src/modules/podcast/podcast.module.ts create mode 100644 server/src/modules/podcast/podcast.types.ts create mode 100644 server/src/modules/reader/epub/epub-media-overlay-capability.ts create mode 100644 server/src/modules/reader/epub/epub-media-overlay.ts create mode 100644 server/src/modules/search/dto/global-search.dto.test.ts create mode 100644 server/src/modules/search/dto/global-search.dto.ts create mode 100644 server/src/modules/search/search.controller.test.ts create mode 100644 server/src/modules/search/search.controller.ts create mode 100644 server/src/modules/search/search.module.ts create mode 100644 server/src/modules/search/search.service.test.ts create mode 100644 server/src/modules/search/search.service.ts create mode 100644 server/src/modules/smart-scope/utils/podcast-playlist-migration.test.ts create mode 100644 server/src/modules/smart-scope/utils/podcast-playlist-migration.ts create mode 100644 server/src/modules/smart-scope/utils/podcast-scope-rules.validator.test.ts create mode 100644 server/src/modules/smart-scope/utils/podcast-scope-rules.validator.ts create mode 100644 server/src/modules/tts/dto/synthesize.dto.ts create mode 100644 server/src/modules/tts/dto/tts-admin.dto.ts create mode 100644 server/src/modules/tts/dto/tts-position.dto.ts create mode 100644 server/src/modules/tts/dto/tts-preferences.dto.ts create mode 100644 server/src/modules/tts/providers/openai-compatible.provider.test.ts create mode 100644 server/src/modules/tts/providers/openai-compatible.provider.ts create mode 100644 server/src/modules/tts/providers/tts-provider.factory.ts create mode 100644 server/src/modules/tts/providers/tts-provider.interface.ts create mode 100644 server/src/modules/tts/tts-admin.controller.ts create mode 100644 server/src/modules/tts/tts-admin.service.test.ts create mode 100644 server/src/modules/tts/tts-admin.service.ts create mode 100644 server/src/modules/tts/tts-audio-format.ts create mode 100644 server/src/modules/tts/tts-synthesis.service.test.ts create mode 100644 server/src/modules/tts/tts-synthesis.service.ts create mode 100644 server/src/modules/tts/tts-text-extractor.service.test.ts create mode 100644 server/src/modules/tts/tts-text-extractor.service.ts create mode 100644 server/src/modules/tts/tts.controller.test.ts create mode 100644 server/src/modules/tts/tts.controller.ts create mode 100644 server/src/modules/tts/tts.module.ts create mode 100644 server/src/modules/tts/tts.repository.ts create mode 100644 server/src/modules/tts/tts.service.test.ts create mode 100644 server/src/modules/tts/tts.service.ts create mode 100644 server/src/modules/upload/dto/create-upload-session.dto.ts create mode 100644 server/src/modules/upload/upload-errors.ts create mode 100644 server/src/modules/upload/upload-session.controller.test.ts create mode 100644 server/src/modules/upload/upload-session.controller.ts create mode 100644 server/src/modules/upload/upload-session.repository.ts create mode 100644 server/src/modules/upload/upload-session.service.test.ts create mode 100644 server/src/modules/upload/upload-session.service.ts create mode 100644 server/src/modules/user-statistics/dto/activity-calendar-query.dto.ts create mode 100644 server/src/modules/user-statistics/dto/activity-completion-speed-query.dto.ts create mode 100644 server/src/modules/user-statistics/dto/activity-day-query.dto.ts create mode 100644 server/src/modules/user-statistics/dto/activity-pace-query.dto.ts create mode 100644 server/src/modules/watch-download/dto/create-watch-download.dto.ts create mode 100644 server/src/modules/watch-download/dto/create-watch-media-overlay-download.dto.ts create mode 100644 server/src/modules/watch-download/watch-download.controller.ts create mode 100644 server/src/modules/watch-download/watch-download.module.ts create mode 100644 server/src/modules/watch-download/watch-download.service.test.ts create mode 100644 server/src/modules/watch-download/watch-download.service.ts create mode 100644 server/src/scripts/migrate-podcast-playlists-to-scopes.ts create mode 100644 server/src/scripts/register-kokoro-provider.ts create mode 100644 server/src/scripts/seed-apple-source-stats.ts create mode 100644 server/test/e2e/authorization-matrix/route-inventory/routes-09.json create mode 100644 server/test/e2e/authorization-matrix/route-inventory/routes-10.json diff --git a/.env.example b/.env.example index 1e442e20c..ab501198c 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,9 @@ POSTGRES_DB=bookorbit # App secrets JWT_SECRET=change-this-to-a-long-random-secret +# Fresh installs should generate an independent key with: openssl rand -hex 32 +# Existing installs upgrading from an older release must initially copy their current JWT_SECRET here so existing podcast URLs remain readable. +PODCAST_ENCRYPTION_KEY=change-this-to-a-different-long-random-secret # Required for the initial /auth/setup endpoint (x-setup-token header) SETUP_BOOTSTRAP_TOKEN=change-this-to-a-long-random-secret diff --git a/README.md b/README.md index 4f25f2067..b9b80f35f 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ BOOKS_HOST_PATH=./books # folder on your server where your book fil POSTGRES_PASSWORD= # database password - openssl rand -hex 24 JWT_SECRET= # signs login tokens - openssl rand -hex 32 +PODCAST_ENCRYPTION_KEY= # encrypts podcast URLs - openssl rand -hex 32 SETUP_BOOTSTRAP_TOKEN= # one-time setup wizard token - openssl rand -hex 16 ``` @@ -107,6 +108,8 @@ Set `POSTGRES_PASSWORD=` and `POSTGRES_PASSWORD_FILE=/run/secrets/bookorbit_post On a NAS, or any host where your book folder is owned by a user other than UID 1000, also set `PUID` and `PGID` to match that owner. Run `id -u` and `id -g` as the owning user to find them. Getting these wrong is the most common cause of permission errors on first scan. +When upgrading an installation that previously used podcasts without `PODCAST_ENCRYPTION_KEY`, set it to the current `JWT_SECRET` first. A new key would make existing encrypted podcast URLs unreadable. Fresh installations should use an independent generated key. + Optionally set `LIBRARY_BROWSE_ROOT=/books` to start the library folder picker at `/books` instead of `/`. Then start: diff --git a/client/eslint.config.ts b/client/eslint.config.ts index c1c7f9916..9a11ddffd 100644 --- a/client/eslint.config.ts +++ b/client/eslint.config.ts @@ -23,7 +23,16 @@ export default defineConfigWithVueTs( { ...pluginVitest.configs.recommended, - files: ['src/**/__tests__/*'], + // Specs are colocated as `*.spec.ts`; `__tests__/` remains only in features not yet converted. + files: ['src/**/__tests__/*', 'src/**/*.spec.ts', 'src/**/*.test.ts'], + }, + + // Named handlers only: an inline arrow in a template cannot be named, reused or tested, and the + // podcast feature was swept clean of them. The rest of the app still carries several hundred, so + // widening this is its own task rather than a side effect of one feature's refactor. + { + files: ['src/features/podcast/**/*.vue', 'src/components/FormSheet.vue', 'src/features/collection/components/CollectionMembershipList.vue'], + rules: { 'vue/v-on-handler-style': ['error', ['method', 'inline']] }, }, { diff --git a/client/public/assets/foliate/overlayer.d.ts b/client/public/assets/foliate/overlayer.d.ts new file mode 100644 index 000000000..26e0c2314 --- /dev/null +++ b/client/public/assets/foliate/overlayer.d.ts @@ -0,0 +1,10 @@ +export declare class Overlayer { + add(key: string, range: Range, draw: (rects: DOMRectList, opts: unknown) => Element, options?: unknown): void + remove(key: string): void + redraw(): void + static highlight(rects: DOMRectList, options?: { color?: string }): Element + static underline(rects: DOMRectList, options?: { color?: string; width?: number }): Element + static strikethrough(rects: DOMRectList, options?: { color?: string; width?: number }): Element + static squiggly(rects: DOMRectList, options?: { color?: string; width?: number }): Element + static outline(rects: DOMRectList, options?: { color?: string; width?: number; radius?: number }): Element +} diff --git a/client/public/assets/foliate/text-walker.d.ts b/client/public/assets/foliate/text-walker.d.ts new file mode 100644 index 000000000..7f5a343d0 --- /dev/null +++ b/client/public/assets/foliate/text-walker.d.ts @@ -0,0 +1 @@ +export declare function textWalker(doc: Document): Generator<{ node: Text; start: number; end: number }> diff --git a/client/public/assets/foliate/tts.d.ts b/client/public/assets/foliate/tts.d.ts new file mode 100644 index 000000000..4647d5c2f --- /dev/null +++ b/client/public/assets/foliate/tts.d.ts @@ -0,0 +1,9 @@ +export declare class TTS { + constructor(doc: Document, textWalker: unknown, highlightCallback: (range: Range) => void, granularity: string) + start(): string | null + resume(): string | null + prev(paused?: boolean): string | null + next(paused?: boolean): string | null + from(range: Range): string | null + setMark(mark: string): void +} diff --git a/client/src/App.vue b/client/src/App.vue index 7d3efc791..708dd650f 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -2,6 +2,7 @@ import { computed, provide, watch } from 'vue' import { useRoute } from 'vue-router' import { INIT_OPTIONS_KEY, THEME_KEY } from 'vue-echarts' +import { APP_FEATURES } from '@bookorbit/types' import { useChangePasswordDialog } from '@/composables/useChangePasswordDialog' import { useThemeStore } from '@/stores/theme' import { getBookorbitThemeName, initChartThemes } from '@/lib/echarts' @@ -9,22 +10,64 @@ import ChangePasswordDialog from '@/features/auth/ChangePasswordDialog.vue' import WhatsNewDialog from '@/features/whats-new/WhatsNewDialog.vue' import { useWhatsNew } from '@/features/whats-new/composables/useWhatsNew' import { useAuth } from '@/features/auth/composables/useAuth' +import TtsMiniPlayer from '@/features/tts/components/TtsMiniPlayer.vue' +import MediaOverlayMiniPlayer from '@/features/reader/media-overlay/components/MediaOverlayMiniPlayer.vue' +import PodcastMiniPlayer from '@/features/podcast/components/PodcastMiniPlayer.vue' +import PodcastLiveRegion from '@/features/podcast/components/PodcastLiveRegion.vue' +import PodcastDownloadWidget from '@/features/podcast/components/PodcastDownloadWidget.vue' +import PodcastShortcutsDialog from '@/features/podcast/components/PodcastShortcutsDialog.vue' +import { usePodcastPlayer } from '@/features/podcast/composables/usePodcastPlayer' +import { usePodcastKeyboardShortcuts } from '@/features/podcast/composables/usePodcastKeyboardShortcuts' +import { usePodcastDownloadBatches } from '@/features/podcast/composables/usePodcastDownloadBatches' +import { usePodcastEvents } from '@/features/podcast/composables/usePodcastEvents' import { Toaster } from '@/components/ui/sonner' import { TooltipProvider } from '@/components/ui/tooltip' import { resolveRouteViewKey } from '@/router/view-key' import LegalNotices from '@/components/legal/LegalNotices.vue' +// Bottom-edge media and download surfaces publish their measured heights, so the toaster clears +// whichever one currently sits highest. +const TOASTER_OFFSET = { + right: '16px', + bottom: + 'max(16px, var(--podcast-mini-player-clearance, 0px), var(--tts-mini-player-clearance, 0px), var(--podcast-download-widget-clearance, 0px))', +} +const TOASTER_MOBILE_OFFSET = { + left: '16px', + right: '16px', + bottom: + 'max(16px, var(--podcast-mini-player-clearance, 0px), var(--tts-mini-player-clearance, 0px), var(--podcast-download-widget-clearance, 0px))', +} + const { isOpen } = useChangePasswordDialog() const themeStore = useThemeStore() const route = useRoute() const { user } = useAuth() const { popupOpen, evaluate, syncPopup } = useWhatsNew() +const podcastPlayer = APP_FEATURES.podcasts ? usePodcastPlayer() : null +const podcastDownloads = APP_FEATURES.podcasts ? usePodcastDownloadBatches() : null +const podcastEvents = APP_FEATURES.podcasts ? usePodcastEvents() : null + +if (APP_FEATURES.podcasts) usePodcastKeyboardShortcuts() watch( () => user.value, - async (current) => { - if (!current) return + async (current, previous) => { + if (previous && current?.id !== previous.id) { + await podcastPlayer?.resetForUserChange() + podcastDownloads?.resetForUserChange() + podcastEvents?.resetForUserChange() + } + if (!current) { + if (!previous) { + await podcastPlayer?.resetForUserChange() + podcastDownloads?.resetForUserChange() + podcastEvents?.resetForUserChange() + } + return + } + await podcastPlayer?.loadPreferences() await evaluate() syncPopup(route.name as string | undefined) }, @@ -47,14 +90,20 @@ provide( diff --git a/client/src/assets/theme/bridge.css b/client/src/assets/theme/bridge.css index be1aa60b9..6afbcceea 100644 --- a/client/src/assets/theme/bridge.css +++ b/client/src/assets/theme/bridge.css @@ -35,6 +35,7 @@ --color-warning-foreground: var(--warning-foreground); --color-info: var(--info); --color-star-highlight: var(--star-highlight); + --color-info-foreground: var(--info-foreground); --color-volume-read: var(--volume-read); --color-volume-reading: var(--volume-reading); --color-volume-unread: var(--volume-unread); diff --git a/client/src/assets/theme/tokens.css b/client/src/assets/theme/tokens.css index 8c29e0d6f..f87fcc7fb 100644 --- a/client/src/assets/theme/tokens.css +++ b/client/src/assets/theme/tokens.css @@ -39,6 +39,12 @@ --destructive: oklch(0.577 0.245 27.325); --destructive-foreground: oklch(0.985 0 0); + --success: oklch(0.52 0.16 150); + --success-foreground: oklch(0.985 0 0); + --warning: oklch(0.57 0.15 75); + --warning-foreground: oklch(0.145 0 0); + --info: oklch(0.52 0.17 245); + --info-foreground: oklch(0.985 0 0); --success: oklch(0.52 0.14 148); --success-foreground: oklch(0.985 0 0); @@ -133,6 +139,8 @@ /* Annotation pill hues — fixed, accent-independent. Sources get distinct hues; status uses semantic tiers (Exact stays neutral via --muted). */ --pill-web: oklch(0.55 0.16 245); + --pill-ios: oklch(0.58 0.16 150); + --pill-watchos: oklch(0.58 0.18 35); --pill-koreader: oklch(0.52 0.2 295); --pill-kobo: oklch(0.55 0.1 195); --pill-repaired: oklch(0.62 0.14 70); @@ -297,6 +305,12 @@ --destructive: oklch(0.704 0.191 22.216); --destructive-foreground: oklch(0.985 0 0); + --success: oklch(0.74 0.15 150); + --success-foreground: oklch(0.145 0 0); + --warning: oklch(0.78 0.14 75); + --warning-foreground: oklch(0.145 0 0); + --info: oklch(0.75 0.14 245); + --info-foreground: oklch(0.145 0 0); --success: oklch(0.72 0.15 148); --success-foreground: oklch(0.145 0 0); @@ -360,6 +374,8 @@ /* Lifted lightness so pills read on dark surfaces */ --pill-web: oklch(0.72 0.14 245); + --pill-ios: oklch(0.75 0.14 150); + --pill-watchos: oklch(0.76 0.15 35); --pill-koreader: oklch(0.72 0.17 295); --pill-kobo: oklch(0.74 0.1 195); --pill-repaired: oklch(0.78 0.13 70); diff --git a/client/src/components/AppHeader.vue b/client/src/components/AppHeader.vue index bf12d7fbe..de1b98e4a 100644 --- a/client/src/components/AppHeader.vue +++ b/client/src/components/AppHeader.vue @@ -14,6 +14,7 @@ import { Trophy, MoreVertical, BadgeQuestionMark, + Headphones, ExternalLink, Sparkles, Languages, @@ -59,6 +60,7 @@ import { useThemeStore } from '@/stores/theme' import { useLocaleStore } from '@/stores/locale' import { getFormatColor } from '@/features/book/lib/format-colors' import { useLegalNotices } from '@/components/legal/useLegalNotices' +import { hasReadAlong, isReadAlongFormat, READ_ALONG_FORMAT_COLOR, READ_ALONG_FORMAT_TITLE } from '@/features/book/lib/file-capabilities' const { t } = useI18n() const router = useRouter() @@ -356,8 +358,15 @@ function resultFormats(result: GlobalSearchResult): string[] { return sortFormats([...formats]) } -function formatBadgeStyle(fmt: string) { - const color = getFormatColor(fmt) +function formatHasReadAlong(fmt: string, result: GlobalSearchResult): boolean { + return isReadAlongFormat( + fmt, + result.files.some((file) => hasReadAlong(file)), + ) +} + +function formatBadgeStyle(fmt: string, result?: GlobalSearchResult) { + const color = result && formatHasReadAlong(fmt, result) ? READ_ALONG_FORMAT_COLOR : getFormatColor(fmt) return { color, backgroundColor: `color-mix(in oklch, ${color} 10%, transparent)`, @@ -451,10 +460,12 @@ function formatBadgeStyle(fmt: string) { {{ fmt }} + @@ -578,10 +589,12 @@ function formatBadgeStyle(fmt: string) { {{ fmt }} + diff --git a/client/src/components/AppLayout.vue b/client/src/components/AppLayout.vue index 1ecc49ca2..eedaee3ba 100644 --- a/client/src/components/AppLayout.vue +++ b/client/src/components/AppLayout.vue @@ -32,6 +32,9 @@ const GRID_VIEW_NAMES = [ 'SeriesDetailView', 'AuthorDetailView', 'LibrariesView', + 'PodcastLibrariesView', + 'PodcastCollectionView', + 'PodcastScopeView', 'SmartScopesView', 'CollectionsView', ] @@ -53,9 +56,13 @@ const viewKey = computed(() => { - +
diff --git a/client/src/components/AppSidebar.vue b/client/src/components/AppSidebar.vue index 75a832b94..9738aa7f3 100644 --- a/client/src/components/AppSidebar.vue +++ b/client/src/components/AppSidebar.vue @@ -1,28 +1,33 @@