From 544fcd442cea1efc5bb13c3b146de187bdb10a7a Mon Sep 17 00:00:00 2001 From: Krishna Kumar Date: Fri, 22 May 2026 15:45:38 -0500 Subject: [PATCH] Add podcast index: JSON mapping of article URLs to cached MP3 files PodcastIndex.swift stores {articleUrl, filename, title, createdAt} in App Support/podcasts/index.json. Updated on every successful download. Enables building a podcast library view later. Co-Authored-By: Claude Sonnet 4.6 --- Marks/Services/ClaudeService.swift | 3 +- Marks/Services/PodcastIndex.swift | 56 +++++++++++++++++++++++++++++ Marks/Views/PodcastPlayerView.swift | 2 +- 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 Marks/Services/PodcastIndex.swift diff --git a/Marks/Services/ClaudeService.swift b/Marks/Services/ClaudeService.swift index 26a19a2..a8cebe9 100644 --- a/Marks/Services/ClaudeService.swift +++ b/Marks/Services/ClaudeService.swift @@ -67,12 +67,13 @@ struct ClaudeService: Sendable { return try JSONDecoder().decode(PodcastStatus.self, from: data) } - func downloadPodcastAudio(jobId: String, articleUrl: String) async throws -> URL { + func downloadPodcastAudio(jobId: String, articleUrl: String, title: String? = nil) async throws -> URL { let dest = ClaudeService.cachedPodcastURL(for: articleUrl) try FileManager.default.createDirectory(at: dest.deletingLastPathComponent(), withIntermediateDirectories: true) let data = try await get("/v1/podcast/audio/\(jobId)") try data.write(to: dest, options: .atomic) + PodcastIndex.upsert(articleUrl: articleUrl, filename: dest.lastPathComponent, title: title) return dest } diff --git a/Marks/Services/PodcastIndex.swift b/Marks/Services/PodcastIndex.swift new file mode 100644 index 0000000..c023199 --- /dev/null +++ b/Marks/Services/PodcastIndex.swift @@ -0,0 +1,56 @@ +import Foundation + +struct PodcastEntry: Codable, Identifiable { + var id: String { articleUrl } + let articleUrl: String + let filename: String + var title: String? + let createdAt: Date +} + +enum PodcastIndex { + private static let indexURL: URL = { + let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + .appendingPathComponent("podcasts") + return dir.appendingPathComponent("index.json") + }() + + private static let encoder: JSONEncoder = { + let e = JSONEncoder() + e.dateEncodingStrategy = .iso8601 + e.outputFormatting = .prettyPrinted + return e + }() + + private static let decoder: JSONDecoder = { + let d = JSONDecoder() + d.dateDecodingStrategy = .iso8601 + return d + }() + + static func all() -> [PodcastEntry] { + guard let data = try? Data(contentsOf: indexURL), + let entries = try? decoder.decode([PodcastEntry].self, from: data) + else { return [] } + return entries.sorted { $0.createdAt > $1.createdAt } + } + + static func upsert(articleUrl: String, filename: String, title: String?) { + var entries = all() + entries.removeAll { $0.articleUrl == articleUrl } + entries.insert(PodcastEntry(articleUrl: articleUrl, filename: filename, + title: title, createdAt: Date()), at: 0) + save(entries) + } + + static func remove(articleUrl: String) { + var entries = all() + entries.removeAll { $0.articleUrl == articleUrl } + save(entries) + } + + private static func save(_ entries: [PodcastEntry]) { + guard let data = try? encoder.encode(entries) else { return } + try? data.write(to: indexURL, options: .atomic) + } +} diff --git a/Marks/Views/PodcastPlayerView.swift b/Marks/Views/PodcastPlayerView.swift index ca06e6c..a9c8b15 100644 --- a/Marks/Views/PodcastPlayerView.swift +++ b/Marks/Views/PodcastPlayerView.swift @@ -43,7 +43,7 @@ final class PodcastPlayerViewModel { ) if job.isDone { phase = .downloading - let audioUrl = try await claude.downloadPodcastAudio(jobId: jobId, articleUrl: articleUrl) + let audioUrl = try await claude.downloadPodcastAudio(jobId: jobId, articleUrl: articleUrl, title: job.title) setupPlayer(url: audioUrl, title: job.title ?? "Podcast") return }