feat: receive ordered stream playback queues
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import Foundation
|
||||
import Darwin
|
||||
|
||||
struct StreamQueue: Sendable {
|
||||
struct Entry: Sendable {
|
||||
let url: URL
|
||||
let identity: String
|
||||
let progressFile: URL
|
||||
let label: String
|
||||
}
|
||||
let entries: [Entry]
|
||||
|
||||
private struct Document: Decodable {
|
||||
struct Item: Decodable { let url: String; let progress_file: String }
|
||||
let version: Int
|
||||
let items: [Item]
|
||||
}
|
||||
private struct InvalidQueue: LocalizedError {
|
||||
var errorDescription: String? { "Invalid stream queue: expected version 1, 1–4096 unique supported videos and new absolute report files in private owned directories." }
|
||||
}
|
||||
|
||||
static func load(from file: URL) throws -> StreamQueue {
|
||||
do {
|
||||
guard file.isFileURL, file.path.hasPrefix("/") else { throw InvalidQueue() }
|
||||
let descriptor = Darwin.open(file.path, O_RDONLY | O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC)
|
||||
guard descriptor >= 0 else { throw InvalidQueue() }
|
||||
let handle = FileHandle(fileDescriptor: descriptor, closeOnDealloc: true)
|
||||
defer { try? handle.close() }
|
||||
var info = stat()
|
||||
guard fstat(descriptor, &info) == 0, (info.st_mode & S_IFMT) == S_IFREG else { throw InvalidQueue() }
|
||||
let data = try handle.read(upToCount: 8 * 1024 * 1024 + 1) ?? Data()
|
||||
guard data.count <= 8 * 1024 * 1024 else { throw InvalidQueue() }
|
||||
let document = try JSONDecoder().decode(Document.self, from: data)
|
||||
guard document.version == 1, (1...4096).contains(document.items.count) else { throw InvalidQueue() }
|
||||
var identities = Set<String>()
|
||||
var reports = Set<String>()
|
||||
let entries = try document.items.map { item -> Entry in
|
||||
let stream = try StreamURL.parse(item.url)
|
||||
let url: URL
|
||||
let identity: String
|
||||
let label: String
|
||||
switch stream {
|
||||
case .youtube(let canonical):
|
||||
url = canonical
|
||||
identity = canonical.absoluteString
|
||||
label = "YouTube · " + (URLComponents(url: canonical, resolvingAgainstBaseURL: false)?.queryItems?.first?.value ?? "Video")
|
||||
case .miruro(let mediaID, let episode):
|
||||
url = URL(string: item.url)!
|
||||
identity = "miruro:\(mediaID):\(episode)"
|
||||
label = "\(url.lastPathComponent.replacingOccurrences(of: "-", with: " ")) · Episode \(episode)"
|
||||
}
|
||||
guard identities.insert(identity).inserted,
|
||||
item.progress_file.hasPrefix("/"), !item.progress_file.contains("\0") else { throw InvalidQueue() }
|
||||
let report = URL(fileURLWithPath: item.progress_file).standardizedFileURL
|
||||
let parent = report.deletingLastPathComponent()
|
||||
var directoryInfo = stat()
|
||||
guard lstat(parent.path, &directoryInfo) == 0,
|
||||
(directoryInfo.st_mode & S_IFMT) == S_IFDIR,
|
||||
directoryInfo.st_uid == geteuid(), (directoryInfo.st_mode & 0o777) == 0o700,
|
||||
parent.resolvingSymlinksInPath().path == parent.path,
|
||||
reports.insert(report.path).inserted else { throw InvalidQueue() }
|
||||
var existing = stat()
|
||||
guard lstat(report.path, &existing) != 0, errno == ENOENT else { throw InvalidQueue() }
|
||||
return Entry(url: url, identity: identity, progressFile: report, label: label)
|
||||
}
|
||||
return StreamQueue(entries: entries)
|
||||
} catch { throw InvalidQueue() }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user