feat: report authoritative playback progress from shared backend

This commit is contained in:
2026-10-11 19:29:35 +02:00
parent a4c38bfa51
commit cc99a2a4f8
14 changed files with 1369 additions and 62 deletions
+39 -7
View File
@@ -3,11 +3,12 @@ import Foundation
enum PlayerCommand {
case help
case printDefaultConfig
case launch(URL?, newInstance: Bool, noConfig: Bool)
case launch(URL?, newInstance: Bool, noConfig: Bool, progressFile: URL?)
static let usage = """
Usage: terebi [--new-instance] [--no-config] [FILE | YOUTUBE_URL]
terebi [--new-instance] [--no-config] --url YOUTUBE_URL
terebi --new-instance --url YOUTUBE_URL --progress-file PATH
terebi [--new-instance] [--no-config] -- FILE
terebi --help
terebi --default-config
@@ -19,6 +20,8 @@ enum PlayerCommand {
--new-instance Launch a separate player, leaving existing playback untouched.
--no-config Use built-in settings instead of config.toml on a fresh launch.
--url URL Open a YouTube video URL.
--progress-file PATH Write JSONL playback facts to a new private file; requires
--new-instance. The GUI owns reporting after this CLI exits.
-- Treat the following argument as a filename, even with a leading '-'.
-h, --help Show this help without launching the player.
--default-config Print the complete built-in TOML without launching.
@@ -30,6 +33,23 @@ enum PlayerCommand {
static func parse(_ arguments: [String], directory: URL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true)) throws -> PlayerCommand {
var inputs = arguments
var progressFile: URL?
var index = 0
while index < inputs.count && inputs[index] != "--" {
if inputs[index] != "--progress-file" {
index += 1
continue
}
guard progressFile == nil else { throw ArgumentError("--progress-file may only be supplied once.") }
guard index + 1 < inputs.count, !inputs[index + 1].isEmpty,
!inputs[index + 1].contains("\0"), !inputs[index + 1].hasPrefix("--") else {
throw ArgumentError("--progress-file requires a filesystem path.")
}
let base = directory.hasDirectoryPath ? directory :
URL(fileURLWithPath: directory.path, isDirectory: true)
progressFile = URL(fileURLWithPath: inputs[index + 1], relativeTo: base).absoluteURL.standardizedFileURL
inputs.removeSubrange(index...index + 1)
}
var newInstance = false
var noConfig = false
while let option = inputs.first, option == "--new-instance" || option == "--no-config" {
@@ -42,12 +62,24 @@ enum PlayerCommand {
}
inputs.removeFirst()
}
if inputs.isEmpty { return .launch(nil, newInstance: newInstance, noConfig: noConfig) }
if inputs == ["--help"] || inputs == ["-h"] { return .help }
if inputs == ["--default-config"] { return .printDefaultConfig }
func launch(_ url: URL?) throws -> PlayerCommand {
guard progressFile == nil || newInstance else {
throw ArgumentError("--progress-file requires --new-instance.")
}
return .launch(url, newInstance: newInstance, noConfig: noConfig, progressFile: progressFile)
}
if inputs.isEmpty { return try launch(nil) }
if inputs == ["--help"] || inputs == ["-h"] {
guard progressFile == nil else { throw ArgumentError("--progress-file cannot be combined with --help.") }
return .help
}
if inputs == ["--default-config"] {
guard progressFile == nil else { throw ArgumentError("--progress-file cannot be combined with --default-config.") }
return .printDefaultConfig
}
if inputs.first == "--url" {
guard inputs.count == 2 else { throw ArgumentError("--url requires exactly one YouTube URL.") }
return .launch(try YouTubeResolver.validateURL(inputs[1]), newInstance: newInstance, noConfig: noConfig)
return try launch(YouTubeResolver.validateURL(inputs[1]))
}
let filenameOnly = inputs.first == "--"
if filenameOnly {
@@ -67,7 +99,7 @@ enum PlayerCommand {
}
file = url
} else {
return .launch(try YouTubeResolver.validateURL(input), newInstance: newInstance, noConfig: noConfig)
return try launch(YouTubeResolver.validateURL(input))
}
} else {
let base = directory.hasDirectoryPath ? directory : URL(fileURLWithPath: directory.path, isDirectory: true)
@@ -77,7 +109,7 @@ enum PlayerCommand {
FileManager.default.isReadableFile(atPath: file.path) else {
throw ArgumentError("File does not exist or is not a readable regular file.")
}
return .launch(file.standardizedFileURL, newInstance: newInstance, noConfig: noConfig)
return try launch(file.standardizedFileURL)
}
private struct ArgumentError: LocalizedError {