Add local source ingest and podcasts
This commit is contained in:
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user