diff --git a/openwiki/architecture/app-architecture.md b/openwiki/architecture/app-architecture.md index d41766f..8bcb6b6 100644 --- a/openwiki/architecture/app-architecture.md +++ b/openwiki/architecture/app-architecture.md @@ -7,7 +7,7 @@ This page explains how the major Marks runtime pieces fit together. For product- `/project.yml` is the XcodeGen project definition. It defines four targets: - `Marks` — the main iOS app. Sources include `/Marks` plus `/MarksWidget/WidgetDataStore.swift` so the app can write widget data. -- `ShareExtension` — app extension with `/ShareExtension` plus selected shared model/service files from `/Marks` (`Bookmark`, `ServerConfig`, `LinkdingAPI`, `MarksAuth`, `Log`, `PodcastRequests`). +- `ShareExtension` — app extension with `/ShareExtension` plus selected shared model/service files from `/Marks` (`Bookmark`, `IngestPayload`, `ServerConfig`, `LinkdingAPI`, `MarksAuth`, `Log`, `PodcastRequests`). - `MarksWidget` — WidgetKit extension with `/MarksWidget`. - `MarksTests` — unit-test bundle depending on `Marks`. @@ -22,7 +22,8 @@ At startup: 1. `MarksApp` loads a saved `ServerConfig` or falls back to a built-in default config. 2. `MainContainer` constructs `LinkdingAPI(config:)`. 3. `MainContainer` stores `BookmarksViewModel(api:cacheKey:)` in `@State`, keeping the model stable across SwiftUI re-renders. -4. A top-level `TabView` presents Bookmarks, Tags, Podcasts, and Search. +4. `MainContainer` also creates an `IngestedSourceLibrary` for local source storage. +5. A top-level `TabView` presents Bookmarks, Tags, Sources, Podcasts, and Search. `MainContainer` also handles: @@ -70,6 +71,19 @@ Implemented operations include: `Bookmark`, `BookmarkResponse`, create/update payloads, `BookmarkCheck`, tag response models, and `SmartCollection` live in `/Marks/Models/Bookmark.swift`. +## Local source ingest + +Local source ingest is separate from linkding bookmarks. It is owned by: + +- `/Marks/Models/IngestedSource.swift` — source metadata for text, PDF, and generic file imports. +- `/Marks/Services/IngestedSourceStore.swift` — JSON metadata persistence, security-scoped file copying, PDFKit text extraction, plain-text extraction, and local search. +- `/Marks/Services/SourceSpotlightIndexer.swift` — Spotlight indexing/removal for source title, body text, tags, kind, and original filename. +- `/Marks/Views/SourcesView.swift` — Sources tab list/search, text import, file importer, QuickLook preview, delete, and podcast actions. + +`IngestedSourceStore` writes `Sources/sources.json` and imported originals under `Sources/files/` in Application Support. Text imports store only metadata/body text. File imports copy the selected file into app storage; PDF imports extract text with `PDFDocument`, while other files attempt UTF-8/ASCII extraction. + +Source podcasts use `IngestedSource.podcastURL` (`marks-source://{uuid}`) as the podcast cache/index key and send up to 100,000 characters of `bodyText` to the backend as `text`. + ## Configuration and app-group sharing `ServerConfig` contains linkding host, optional port, path, token, and HTTPS flag. Its extension stores config in two places: @@ -94,7 +108,7 @@ Security note: `/Marks/MarksApp.swift` currently includes a built-in default con `/Marks/Services/AnalyticsService.swift` sends events through the backend path used by `ClaudeService`/`MarksAuth`. -Podcast audio is downloaded to Application Support under a `podcasts` directory using a stable hash of the article URL. `PodcastIndex` records metadata in `index.json`; `PodcastLibrary` reads that index for the Podcasts tab and widget metadata. +Podcast generation accepts either a web URL or a local source pseudo-URL. `ClaudeService.generatePodcast(url:title:sourceText:sourceKind:)` always sends `url` and may also send `title`, `text`, and `source_kind`. Podcast audio is downloaded to Application Support under a `podcasts` directory using a stable hash of the article/source URL. `PodcastIndex` records metadata in `index.json`; `PodcastLibrary` reads that index for the Podcasts tab and widget metadata. ## On-device Ask and Spotlight @@ -109,13 +123,13 @@ The app has two AI paths: - `SpotlightBookmarkSearch`/`BookmarkSearchTool` search the index for relevant bookmarks. - `BookmarkEntity` also conforms to indexed App Intents concepts for system-level entity discovery. -If changing bookmark fields, keep `Bookmark`, `BookmarkEntity`, Spotlight indexing, widget payloads, and row UI in sync. +If changing bookmark fields, keep `Bookmark`, `BookmarkEntity`, Spotlight indexing, widget payloads, and row UI in sync. If changing source fields, keep `IngestedSource`, `IngestedSourceStore`, `SourceSpotlightIndexer`, Sources UI, and source podcast generation in sync. ## Share extension architecture The share extension is implemented by `/ShareExtension/ShareViewController.swift` and `/ShareExtension/ShareView.swift`. -It accepts a shared URL/text payload, creates a SwiftUI save card, loads shared `ServerConfig`, checks linkding for an existing bookmark, and lets the user edit tags, notes, read-later state, and a Create Podcast toggle. +It accepts shared URL, HTML, RTF, plain-text, and text payloads, uses `IngestPayloadParser` to find the first bookmarkable `http`/`https` URL and preserve non-URL notes, creates a SwiftUI save card, loads shared `ServerConfig`, checks linkding for an existing bookmark, and lets the user edit tags, notes, read-later state, and a Create Podcast toggle. For suggestions, it combines: @@ -123,7 +137,7 @@ For suggestions, it combines: - the user's existing tag vocabulary from `fetchTags()`, - backend AI tag suggestions through `/ShareExtension/TagSuggester.swift`. -Existing bookmarks are updated rather than duplicated. New bookmarks are created through `LinkdingAPI`. Podcast generation is not performed inside the extension; it queues a `PodcastRequests` item in the app group for the main app to process later. +Existing bookmarks are updated rather than duplicated. New bookmarks are created through `LinkdingAPI`. Podcast generation is not performed inside the extension; it queues a `PodcastRequests` item in the app group for the main app to process later. The same parser is used by in-app Add Bookmark text import. ## Widget architecture and deep links @@ -157,4 +171,4 @@ Widget links use custom URLs built by `URL.marksBookmark(_:)` and `URL.marksPodc ## Recent history context -Recent git history shows major investment in podcast UX and on-device AI: the latest merge added played/unplayed queues, background podcast generation, share-sheet audio requests, podcast library/index changes, and player improvements. A prior AI commit added on-device RAG, Spotlight indexing, and unified logging. Use those areas cautiously because they span app state, storage, widgets, share extension, and App Intents rather than a single view. +Recent git history shows major investment in ingest and podcast UX: the latest change added local text/PDF/file sources, richer URL extraction for share/add flows, source Spotlight indexing, and source-backed podcast generation. Earlier podcast and AI changes added played/unplayed queues, background podcast generation, share-sheet audio requests, podcast library/index changes, player improvements, on-device RAG, Spotlight indexing, and unified logging. Use those areas cautiously because they span app state, storage, widgets, share extension, and App Intents rather than a single view. diff --git a/openwiki/operations/development-and-testing.md b/openwiki/operations/development-and-testing.md index 107b7d5..0290569 100644 --- a/openwiki/operations/development-and-testing.md +++ b/openwiki/operations/development-and-testing.md @@ -32,7 +32,7 @@ xcodegen generate Common target-membership traps: - The main app includes `/MarksWidget/WidgetDataStore.swift` directly so it can write widget JSON. -- The share extension includes selected shared files from `/Marks/Models` and `/Marks/Services`; if the extension needs new model/service code, add it in `/project.yml`. +- The share extension includes selected shared files from `/Marks/Models` and `/Marks/Services`, including `IngestPayload.swift` for URL extraction; if the extension needs new model/service code, add it in `/project.yml`. - The app, widget, and share extension all depend on matching app-group entitlements. ## Testing @@ -46,6 +46,8 @@ Covered today: - `IntentRouter` open/search setters. - Direct no-network `perform()` tests for Search and Open intents. - App Shortcuts registration count. +- `IngestPayloadParser` URL extraction from direct URLs, plain text, HTML-ish text, and notes. +- `IngestedSourceStore` save/load behavior plus plain-text file import into a temporary root. Not currently covered by repository tests: @@ -56,6 +58,7 @@ Not currently covered by repository tests: - Widget timeline rendering/deep links. - Browser reading progress/reader mode. - Podcast generation, playback, queue, played/unplayed, and background audio behavior. +- Sources tab UI, QuickLook presentation, PDF extraction edge cases, and source-backed podcast backend behavior. Suggested test command: @@ -88,17 +91,26 @@ AI changes: Share extension changes: -- Share a new URL and an already-saved URL. +- Share a new URL, an already-saved URL, and text/HTML/RTF content that contains a URL plus notes. - Verify duplicate-aware update behavior. - Test linkding auto-tags, AI suggestion chips, tag parsing, notes, read-later, and Create Podcast. - Confirm the extension works when shared config is missing; current UI shows an "Open Marks first" style failure. +Source ingest changes: + +- Import typed text with title/tags and confirm it appears in Sources search. +- Import a text file and a PDF through the file importer; confirm originals copy into Application Support and QuickLook opens imported files. +- Confirm PDF text extraction works for text PDFs and handles image-only PDFs gracefully. +- Delete a source and confirm metadata, copied file, and Spotlight entry are removed. +- Create a podcast from a source and confirm cached playback uses the same `marks-source://` entry. + Podcast changes: - Generate from bookmark row and Browser view. - Start generation while another podcast is playing; confirm background generation does not interrupt playback. - Test queue playback, played/unplayed state, delete, sleep timer, saved speed, background audio, remote controls, now-playing metadata, relaunch restore, and widget recent podcasts. - Share a URL with Create Podcast and confirm the main app drains and starts the queued request. +- Generate from a local text/PDF source and confirm backend support for `text` and `source_kind`. Widget/deep-link changes: @@ -136,6 +148,8 @@ App-group defaults/files (`group.com.magicive.marks`): Application Support files: - `bookmarks_cache.json` or `bookmarks_{host}.json` — cached bookmarks. +- `Sources/sources.json` — local source metadata. +- `Sources/files/{uuid}.{ext}` — copied originals for imported PDFs/files. - `podcasts/{hash}.mp3` — generated podcast audio. - `podcasts/index.json` — podcast metadata and played state. @@ -148,7 +162,8 @@ Application Support files: - `MarksAuth` stores backend JWT/device state in `UserDefaults`, not Keychain. - App-group ID strings are duplicated in code and entitlements; update all occurrences together if changing it. - Share-extension podcast queue draining clears requests before generation success, so crash/retry semantics deserve care. -- Recent git history shows podcast and on-device AI features were added across many files; avoid narrow changes that ignore cross-target data flow. +- Source-backed podcast generation sends extracted text to the Marks backend; if the backend only accepts URL fetches, local source podcasts will fail even though bookmark podcasts still work. +- Recent git history shows source ingest, podcast, and on-device AI features were added across many files; avoid narrow changes that ignore cross-target data flow. ## Existing design guidance diff --git a/openwiki/quickstart.md b/openwiki/quickstart.md index 473e9a1..2eed401 100644 --- a/openwiki/quickstart.md +++ b/openwiki/quickstart.md @@ -1,19 +1,19 @@ # Marks OpenWiki Quickstart -Marks is a native SwiftUI iOS client for [linkding](https://github.com/sissbruecker/linkding), a self-hosted bookmark manager. The app targets iOS 26 and Swift 6, and includes a main app, share extension, widget extension, App Intents, AI-assisted bookmark features, and podcast generation/playback. +Marks is a native SwiftUI iOS client for [linkding](https://github.com/sissbruecker/linkding), a self-hosted bookmark manager. The app targets iOS 26 and Swift 6, and includes a main app, share extension, widget extension, App Intents, AI-assisted bookmark features, local source ingest, and podcast generation/playback. Start here when changing the repository, then follow the page links below for the area you are touching. ## What this repository contains -- **Main iOS app** in `/Marks` with SwiftUI tabs for bookmarks, tags, podcasts, and search. The entrypoint is `/Marks/MarksApp.swift`. -- **Models** in `/Marks/Models`, centered on `Bookmark`, linkding response/create/update payloads, tags, smart collections, and `ServerConfig`. -- **Services** in `/Marks/Services` for linkding networking, AI/backend calls, auth, on-device bookmark Q&A, Spotlight indexing, widget/podcast data, analytics, and logging. +- **Main iOS app** in `/Marks` with SwiftUI tabs for bookmarks, tags, sources, podcasts, and search. The entrypoint is `/Marks/MarksApp.swift`. +- **Models** in `/Marks/Models`, centered on `Bookmark`, linkding response/create/update payloads, tags, smart collections, `ServerConfig`, URL ingest payloads, and locally ingested sources. +- **Services** in `/Marks/Services` for linkding networking, AI/backend calls, auth, on-device bookmark Q&A, Spotlight indexing, source storage, widget/podcast data, analytics, and logging. - **Views** in `/Marks/Views` for the primary product surfaces. `BookmarksViewModel` is the central app state object. - **App Intents** in `/Marks/Intents` for opening, searching, adding, listing unread, and summarizing bookmarks. -- **Share extension** in `/ShareExtension` for saving shared URLs/text to linkding and queueing podcast generation. +- **Share extension** in `/ShareExtension` for extracting URLs from shared URLs, text, HTML, and RTF, saving to linkding, and queueing podcast generation. - **Widget extension** in `/MarksWidget` for recent bookmarks, random bookmark, and recent podcast widgets. -- **Tests** in `/MarksTests`, currently focused on the App Intents layer. +- **Tests** in `/MarksTests`, currently focused on App Intents plus ingest parser/source-store coverage. - **XcodeGen project spec** in `/project.yml`; `Marks.xcodeproj` is generated/checked in but `project.yml` is the project definition to inspect first. - **Design notes** in `/docs/design-wwdc26.md` for iOS 26/Liquid Glass, search, widgets, and row-design guidance. @@ -46,9 +46,11 @@ Adjust the simulator name to what is installed locally. ## Runtime architecture in one minute -At launch, `MarksApp` loads a `ServerConfig`, constructs `LinkdingAPI`, and stores a shared `BookmarksViewModel` in `MainContainer` so it survives SwiftUI re-renders (`/Marks/MarksApp.swift`, `/Marks/Views/BookmarksViewModel.swift`). The top-level `TabView` has Bookmarks, Tags, Podcasts, and Search tabs. +At launch, `MarksApp` loads a `ServerConfig`, constructs `LinkdingAPI`, and stores a shared `BookmarksViewModel` in `MainContainer` so it survives SwiftUI re-renders (`/Marks/MarksApp.swift`, `/Marks/Views/BookmarksViewModel.swift`). The top-level `TabView` has Bookmarks, Tags, Sources, Podcasts, and Search tabs. -`BookmarksViewModel` fetches paginated bookmarks from linkding through `LinkdingAPI`, restores local AI metadata, caches bookmarks to Application Support, writes recent bookmark metadata for widgets, and indexes bookmarks into Spotlight. It also owns `ClaudeService`, `PodcastPlayerViewModel`, and `PodcastGenerationManager`, so AI, podcast, search, and list state are shared across tabs. +`BookmarksViewModel` fetches paginated bookmarks from linkding through `LinkdingAPI`, restores local AI metadata, caches bookmarks to Application Support, writes recent bookmark metadata for widgets, and indexes bookmarks into Spotlight. It also owns `ClaudeService`, `PodcastPlayerViewModel`, and `PodcastGenerationManager`, so AI, podcast, search, and list state are shared across tabs. `MainContainer` separately owns `IngestedSourceLibrary` for the Sources tab. + +Local sources are stored in Application Support under `Sources/`: `sources.json` stores metadata, `Sources/files/` stores imported files, PDF text is extracted with PDFKit, and sources are indexed into Spotlight through `SourceSpotlightIndexer`. Extensions and widgets use the shared app group `group.com.magicive.marks`. `ServerConfig.save()` writes to standard defaults and the app-group defaults; `WidgetDataStore` writes app-group JSON files for widgets; `PodcastRequests` uses app-group defaults to hand podcast requests from the share extension to the main app. @@ -56,7 +58,7 @@ Extensions and widgets use the shared app group `group.com.magicive.marks`. `Ser - **linkding** is the source of truth for bookmarks and tags. `LinkdingAPI` uses token auth and calls linkding bookmark, check, tag, create, update, delete, archive, and connection-test endpoints. - **Marks backend** is used by `MarksAuth`/`ClaudeService` for AI enrichment, semantic search, smart collections, analytics events, and podcast generation/audio download. -- **Apple on-device intelligence and Spotlight** power the in-app Ask surface via `FoundationModels`, `CoreSpotlight`, and the `BookmarkSearchTool` retrieval path. +- **Apple on-device intelligence and Spotlight** power the in-app Ask surface via `FoundationModels`, `CoreSpotlight`, and the `BookmarkSearchTool` retrieval path. Spotlight is also used for locally ingested sources. - **WidgetKit, App Intents, AVFoundation, and MediaPlayer** support widgets, Siri/Shortcuts integration, podcast playback, background audio, now-playing metadata, and remote controls. Do not document or expose secret values. `/Marks/MarksApp.swift` currently contains a built-in default server configuration with a token-like value; treat it as sensitive source material and avoid copying it into docs, logs, screenshots, or tests. @@ -68,14 +70,16 @@ Do not document or expose secret values. `/Marks/MarksApp.swift` currently conta - Settings' disconnect callback currently resets to the built-in default config path rather than forcing onboarding. Confirm intended product behavior before changing account/config flows. - `BookmarksViewModel` is shared across tabs, so changes to bookmark arrays, filters, search, enrichment, and pagination can affect Bookmarks, Tags, Search, Podcasts, widgets, and Spotlight indexing. - Share-extension podcast requests are queued in shared defaults and drained by the main app on launch/foreground. Be careful with crash/retry semantics when touching that flow. -- Existing automated tests cover App Intents only. For UI, networking, sync, share extension, widgets, and podcasts, add tests where feasible and perform manual verification. +- Local source podcasts use `marks-source://{uuid}` pseudo-URLs plus extracted text sent to `ClaudeService.generatePodcast(...)`. The backend must accept `text` and `source_kind` for non-web podcast generation. +- Existing automated tests cover App Intents plus focused ingest parser/source-store behavior. For UI, networking, sync, share extension, widgets, and podcasts, add tests where feasible and perform manual verification. ## Where to start for common changes - Bookmark list, pagination, search, tags, enrichment state: `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Views/BookmarksView.swift`, `/Marks/Services/LinkdingAPI.swift`. - Linkding API changes: `/Marks/Services/LinkdingAPI.swift`, `/Marks/Models/Bookmark.swift`, `/ShareExtension/ShareView.swift`, `/Marks/Intents/IntentSupport.swift`. - AI/backend changes: `/Marks/Services/ClaudeService.swift`, `/Marks/Services/MarksAuth.swift`, `/Marks/Services/BookmarkAssistant.swift`, `/Marks/Services/BookmarkSearchTool.swift`. -- Podcast generation/playback: `/Marks/Services/PodcastGenerationManager.swift`, `/Marks/Services/PodcastIndex.swift`, `/Marks/Views/PodcastPlayerView.swift`, `/Marks/Views/BookmarksViewModel.swift`. -- Share extension: `/ShareExtension/ShareViewController.swift`, `/ShareExtension/ShareView.swift`, `/ShareExtension/TagSuggester.swift`, plus shared files declared in `/project.yml`. +- Source ingest and local documents: `/Marks/Views/SourcesView.swift`, `/Marks/Models/IngestedSource.swift`, `/Marks/Services/IngestedSourceStore.swift`, `/Marks/Services/SourceSpotlightIndexer.swift`. +- Podcast generation/playback: `/Marks/Services/PodcastGenerationManager.swift`, `/Marks/Services/PodcastIndex.swift`, `/Marks/Views/PodcastPlayerView.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Views/SourcesView.swift`. +- Share extension and text/link extraction: `/Marks/Models/IngestPayload.swift`, `/ShareExtension/ShareViewController.swift`, `/ShareExtension/ShareView.swift`, `/ShareExtension/TagSuggester.swift`, plus shared files declared in `/project.yml`. - Widgets/deep links: `/MarksWidget/WidgetDataStore.swift`, `/MarksWidget/*Widget.swift`, `/Marks/MarksApp.swift`. - App Intents/Siri/Shortcuts: `/Marks/Intents/*`, `/MarksTests/AppIntentsTests.swift`. diff --git a/openwiki/workflows/product-workflows.md b/openwiki/workflows/product-workflows.md index 6ca248e..6c6fc2f 100644 --- a/openwiki/workflows/product-workflows.md +++ b/openwiki/workflows/product-workflows.md @@ -20,7 +20,7 @@ Change guidance: Primary sources: `/Marks/Views/AddBookmarkView.swift`, `/Marks/Views/EditBookmarkView.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Models/Bookmark.swift`. -The Add view collects URL, optional title, and comma-separated tags, normalizes/validates the URL, and calls `BookmarksViewModel.addBookmark(url:title:tags:)`. Added bookmarks are inserted locally, indexed into Spotlight, and enriched in a background task. +The Add view collects URL, optional title, description/notes, and comma-separated tags, normalizes/validates the URL, and calls `BookmarksViewModel.addBookmark(url:title:description:tags:)`. It also has a paste/import text path that uses `IngestPayloadParser` to extract the first `http`/`https` URL from plain text and preserve surrounding notes as the bookmark description. Added bookmarks are inserted locally, indexed into Spotlight, and enriched in a background task. The Edit view updates URL, title, description, tags, unread, and shared state through `BookmarkUpdate`. `BookmarksViewModel.updateBookmark(_:)` preserves locally stored AI summary/tags when replacing the bookmark returned from linkding. @@ -80,9 +80,9 @@ Change guidance: ## Share extension save flow -Primary sources: `/ShareExtension/ShareViewController.swift`, `/ShareExtension/ShareView.swift`, `/ShareExtension/TagSuggester.swift`, `/Marks/Services/PodcastRequests.swift`, `/project.yml`. +Primary sources: `/Marks/Models/IngestPayload.swift`, `/ShareExtension/ShareViewController.swift`, `/ShareExtension/ShareView.swift`, `/ShareExtension/TagSuggester.swift`, `/Marks/Services/PodcastRequests.swift`, `/project.yml`. -The share extension extracts a URL/text payload and shows an interactive save card. On load it: +The share extension scans URL, HTML, RTF, plain-text, and text attachments, then `IngestPayloadParser` extracts the first bookmarkable web URL. For non-URL text payloads it also preserves cleaned surrounding text as notes. On load it: 1. reads linkding config from app-group defaults, 2. calls `LinkdingAPI.checkBookmark(url:)`, @@ -96,13 +96,35 @@ Change guidance: - The extension is short-lived. Keep long-running work in the main app. - The extension depends on shared source files listed in `/project.yml`; adding service dependencies may require target membership updates. +- `IngestPayloadParser` intentionally ignores non-HTTP(S) URLs and caps preserved notes at 1,800 characters. - `ShareExtension/Info.plist` currently uses a broad activation rule. Narrowing supported payloads is a product decision. +## Sources + +Primary sources: `/Marks/Views/SourcesView.swift`, `/Marks/Models/IngestedSource.swift`, `/Marks/Services/IngestedSourceStore.swift`, `/Marks/Services/SourceSpotlightIndexer.swift`. + +The Sources tab stores non-web material in Marks. Users can: + +- import typed/pasted text with an optional title and tags, +- import PDFs, plain text files, generic text/data files through the document picker, +- search across source title, body text, tags, and original filename, +- open imported originals through QuickLook, +- delete sources and copied files, +- create or play a podcast from extractable source text. + +`IngestedSourceLibrary.load()` reads `Sources/sources.json` from Application Support and indexes sources into Spotlight. Text imports write metadata/body text directly. File imports copy the selected security-scoped file into `Sources/files/`; PDFs extract text with PDFKit, while other files attempt UTF-8/ASCII extraction. + +Change guidance: + +- Empty or image-only PDFs may import with no extractable text; podcast actions are disabled when `podcastText` is empty. +- Source podcasts use `marks-source://{uuid}` as the cache/index URL and send extracted text plus `source_kind` to the backend. +- If changing source storage, update deletion, Spotlight removal, QuickLook file lookup, and source podcast cache behavior together. + ## Podcasts Primary sources: `/Marks/Views/PodcastPlayerView.swift`, `/Marks/Services/PodcastGenerationManager.swift`, `/Marks/Services/PodcastIndex.swift`, `/Marks/Services/PodcastLibrary.swift`, `/Marks/Services/ClaudeService.swift`, `/Marks/Services/PodcastRequests.swift`, `/Marks/Info.plist`. -Podcast generation converts an article URL into audio through the Marks backend: +Podcast generation converts an article URL, or a locally ingested source with extracted text, into audio through the Marks backend: 1. create generation job (`/v1/podcast/generate`), 2. poll status every few seconds, @@ -112,7 +134,7 @@ Podcast generation converts an article URL into audio through the Marks backend: 6. reload `PodcastLibrary`, 7. write recent podcast metadata for widgets. -`BookmarksViewModel.playOrGeneratePodcast(...)` prevents interrupting current playback. If the player is idle, it starts foreground generation/playback and callers present the player. If busy, it enqueues background generation through `PodcastGenerationManager`. +`BookmarksViewModel.playOrGeneratePodcast(...)` prevents interrupting current bookmark playback. If the player is idle, it starts foreground generation/playback and callers present the player. If busy, it enqueues background generation through `PodcastGenerationManager`. `SourcesView` follows the same foreground/background pattern for local sources, using `marks-source://{uuid}` plus `sourceText`/`sourceKind`. The player uses AVFoundation/MediaPlayer for playback, background audio, progress, saved playback speed, restored paused session, queue playback, now-playing metadata, remote controls, sleep timer, and played/unplayed state. @@ -121,6 +143,7 @@ Change guidance: - The main app plist enables background audio and the custom `marks` URL scheme. - Widget podcast metadata is in the app group, but audio files and `PodcastIndex` are in the main app's Application Support directory. - Share-extension queued podcast requests are drained by `BookmarksViewModel.processPendingPodcastRequests()` on launch/foreground. Consider retry semantics before changing drain behavior. +- Source-backed podcast generation depends on backend support for the optional `text` and `source_kind` fields in `POST /v1/podcast/generate`. ## Widgets and deep links