Update OpenWiki for source ingest
All checks were successful
CI / build-and-deploy (push) Successful in 16s

This commit is contained in:
Krishna Kumar
2026-07-02 01:20:18 -05:00
parent f8693f4be0
commit f7735431a8
4 changed files with 83 additions and 27 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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`.

View File

@@ -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