Add local source ingest and podcasts
This commit is contained in:
@@ -82,16 +82,35 @@ struct BookmarkCheck: Codable, Sendable {
|
||||
struct BookmarkCreate: Codable, Sendable {
|
||||
let url: String
|
||||
let title: String
|
||||
let description: String
|
||||
let tagNames: [String]
|
||||
let isArchived: Bool
|
||||
let unread: Bool
|
||||
let shared: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url, title, unread, shared
|
||||
case url, title, description, unread, shared
|
||||
case tagNames = "tag_names"
|
||||
case isArchived = "is_archived"
|
||||
}
|
||||
|
||||
init(
|
||||
url: String,
|
||||
title: String,
|
||||
description: String = "",
|
||||
tagNames: [String],
|
||||
isArchived: Bool,
|
||||
unread: Bool,
|
||||
shared: Bool
|
||||
) {
|
||||
self.url = url
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.tagNames = tagNames
|
||||
self.isArchived = isArchived
|
||||
self.unread = unread
|
||||
self.shared = shared
|
||||
}
|
||||
}
|
||||
|
||||
struct BookmarkUpdate: Codable, Sendable {
|
||||
|
||||
95
Marks/Models/IngestPayload.swift
Normal file
95
Marks/Models/IngestPayload.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
import Foundation
|
||||
|
||||
struct IngestPayload: Equatable, Sendable {
|
||||
let url: String
|
||||
let title: String?
|
||||
let notes: String
|
||||
}
|
||||
|
||||
enum IngestPayloadParser {
|
||||
static func parse(item: Any?, typeIdentifier: String? = nil) -> IngestPayload? {
|
||||
if let url = item as? URL {
|
||||
return payload(from: url.absoluteString, notes: "")
|
||||
}
|
||||
|
||||
if let attributed = item as? NSAttributedString {
|
||||
return payload(from: attributed.string, notes: attributed.string)
|
||||
}
|
||||
|
||||
if let string = item as? String {
|
||||
return payload(from: string, notes: shouldKeepNotes(for: typeIdentifier) ? string : "")
|
||||
}
|
||||
|
||||
if let data = item as? Data,
|
||||
let string = String(data: data, encoding: .utf8) ?? String(data: data, encoding: .ascii) {
|
||||
return payload(from: string, notes: shouldKeepNotes(for: typeIdentifier) ? string : "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func payload(from text: String, notes: String) -> IngestPayload? {
|
||||
guard let url = firstWebURL(in: text) else { return nil }
|
||||
return IngestPayload(url: url, title: nil, notes: cleanedNotes(notes, excluding: url))
|
||||
}
|
||||
|
||||
static func firstWebURL(in text: String) -> String? {
|
||||
if let direct = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines)),
|
||||
isBookmarkable(direct) {
|
||||
return direct.absoluteString
|
||||
}
|
||||
|
||||
let decoded = decodeHTML(text)
|
||||
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||
let range = NSRange(decoded.startIndex..<decoded.endIndex, in: decoded)
|
||||
let match = detector?.matches(in: decoded, range: range).first { result in
|
||||
guard let url = result.url else { return false }
|
||||
return isBookmarkable(url)
|
||||
}
|
||||
|
||||
guard let matchRange = match?.range,
|
||||
let swiftRange = Range(matchRange, in: decoded) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let candidate = String(decoded[swiftRange])
|
||||
.trimmingCharacters(in: CharacterSet(charactersIn: " \n\t\r<>\"'.,;:)]】」"))
|
||||
|
||||
guard let url = URL(string: candidate), isBookmarkable(url) else { return nil }
|
||||
return url.absoluteString
|
||||
}
|
||||
|
||||
private static func isBookmarkable(_ url: URL) -> Bool {
|
||||
guard let scheme = url.scheme?.lowercased(),
|
||||
scheme == "http" || scheme == "https",
|
||||
url.host?.isEmpty == false else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private static func cleanedNotes(_ notes: String, excluding url: String) -> String {
|
||||
let trimmed = decodeHTML(notes)
|
||||
.replacingOccurrences(of: url, with: "")
|
||||
.components(separatedBy: .newlines)
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: "\n")
|
||||
|
||||
return String(trimmed.prefix(1800))
|
||||
}
|
||||
|
||||
private static func decodeHTML(_ text: String) -> String {
|
||||
text
|
||||
.replacingOccurrences(of: "&", with: "&")
|
||||
.replacingOccurrences(of: "<", with: "<")
|
||||
.replacingOccurrences(of: ">", with: ">")
|
||||
.replacingOccurrences(of: """, with: "\"")
|
||||
.replacingOccurrences(of: "'", with: "'")
|
||||
}
|
||||
|
||||
private static func shouldKeepNotes(for typeIdentifier: String?) -> Bool {
|
||||
guard let typeIdentifier else { return true }
|
||||
return !typeIdentifier.lowercased().contains("url")
|
||||
}
|
||||
}
|
||||
69
Marks/Models/IngestedSource.swift
Normal file
69
Marks/Models/IngestedSource.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
import Foundation
|
||||
|
||||
enum IngestedSourceKind: String, Codable, Sendable, CaseIterable {
|
||||
case text
|
||||
case pdf
|
||||
case file
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .text: "Text"
|
||||
case .pdf: "PDF"
|
||||
case .file: "File"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IngestedSource: Identifiable, Codable, Sendable, Hashable {
|
||||
let id: UUID
|
||||
var kind: IngestedSourceKind
|
||||
var title: String
|
||||
var bodyText: String
|
||||
var originalFilename: String?
|
||||
var localFilename: String?
|
||||
var tags: [String]
|
||||
var createdAt: Date
|
||||
var modifiedAt: Date
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
kind: IngestedSourceKind,
|
||||
title: String,
|
||||
bodyText: String,
|
||||
originalFilename: String? = nil,
|
||||
localFilename: String? = nil,
|
||||
tags: [String] = [],
|
||||
createdAt: Date = Date(),
|
||||
modifiedAt: Date = Date()
|
||||
) {
|
||||
self.id = id
|
||||
self.kind = kind
|
||||
self.title = title
|
||||
self.bodyText = bodyText
|
||||
self.originalFilename = originalFilename
|
||||
self.localFilename = localFilename
|
||||
self.tags = tags
|
||||
self.createdAt = createdAt
|
||||
self.modifiedAt = modifiedAt
|
||||
}
|
||||
|
||||
var displayTitle: String {
|
||||
let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty { return trimmed }
|
||||
return originalFilename ?? kind.label
|
||||
}
|
||||
|
||||
var excerpt: String {
|
||||
bodyText
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.replacingOccurrences(of: "\n", with: " ")
|
||||
}
|
||||
|
||||
var podcastURL: String {
|
||||
"marks-source://\(id.uuidString)"
|
||||
}
|
||||
|
||||
var podcastText: String {
|
||||
String(bodyText.trimmingCharacters(in: .whitespacesAndNewlines).prefix(100_000))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user