137 lines
7.0 KiB
Swift
137 lines
7.0 KiB
Swift
import Foundation
|
|
|
|
enum PlayerCommand {
|
|
case help
|
|
case printDefaultConfig
|
|
case launch(URL?, newInstance: Bool, noConfig: Bool, progressFile: URL?)
|
|
case launchSource(sourceFile: 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 --source-file PATH [--progress-file PATH]
|
|
terebi [--new-instance] [--no-config] -- FILE
|
|
terebi --help
|
|
terebi --default-config
|
|
|
|
Open one local file, YouTube video or resolved source and start playback,
|
|
resuming saved video progress. With no arguments, open or activate the player.
|
|
Quote paths and URLs containing spaces or shell metacharacters.
|
|
|
|
--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.
|
|
--source-file PATH Open a validated resolved HTTP(S) source in a new player.
|
|
--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.
|
|
|
|
Reuses the running player unless --new-instance is supplied before the media
|
|
arguments. The command exits once macOS accepts the launch; playback and
|
|
extraction errors appear in the player window.
|
|
"""
|
|
|
|
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" {
|
|
if option == "--new-instance" {
|
|
guard !newInstance else { throw ArgumentError("--new-instance may only be supplied once.") }
|
|
newInstance = true
|
|
} else {
|
|
guard !noConfig else { throw ArgumentError("--no-config may only be supplied once.") }
|
|
noConfig = true
|
|
}
|
|
inputs.removeFirst()
|
|
}
|
|
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 == "--source-file" {
|
|
guard newInstance else { throw ArgumentError("--source-file requires --new-instance.") }
|
|
guard inputs.count == 2, !inputs[1].isEmpty, !inputs[1].contains("\0"),
|
|
!inputs[1].hasPrefix("--") else {
|
|
throw ArgumentError("--source-file requires exactly one filesystem path.")
|
|
}
|
|
let base = directory.hasDirectoryPath ? directory :
|
|
URL(fileURLWithPath: directory.path, isDirectory: true)
|
|
let sourceFile = URL(fileURLWithPath: inputs[1], relativeTo: base).absoluteURL.standardizedFileURL
|
|
_ = try ResolvedSource.load(from: sourceFile)
|
|
return .launchSource(sourceFile: sourceFile, newInstance: newInstance,
|
|
noConfig: noConfig, progressFile: progressFile)
|
|
}
|
|
if inputs.first == "--url" {
|
|
guard inputs.count == 2 else { throw ArgumentError("--url requires exactly one YouTube URL.") }
|
|
return try launch(YouTubeResolver.validateURL(inputs[1]))
|
|
}
|
|
let filenameOnly = inputs.first == "--"
|
|
if filenameOnly {
|
|
inputs.removeFirst()
|
|
} else if inputs.first?.hasPrefix("-") == true {
|
|
throw ArgumentError("Unknown option. Use -- before filenames beginning with '-'.")
|
|
}
|
|
guard inputs.count == 1, let input = inputs.first, !input.isEmpty else {
|
|
throw ArgumentError("Expected exactly one file or YouTube URL.")
|
|
}
|
|
let file: URL
|
|
if !filenameOnly && input.contains("://") {
|
|
guard let url = URL(string: input) else { throw ArgumentError("Invalid media URL.") }
|
|
if url.isFileURL {
|
|
guard url.host == nil || url.host == "" || url.host == "localhost" else {
|
|
throw ArgumentError("File URLs must refer to this machine.")
|
|
}
|
|
file = url
|
|
} else {
|
|
return try launch(YouTubeResolver.validateURL(input))
|
|
}
|
|
} else {
|
|
let base = directory.hasDirectoryPath ? directory : URL(fileURLWithPath: directory.path, isDirectory: true)
|
|
file = URL(fileURLWithPath: input, relativeTo: base).absoluteURL
|
|
}
|
|
guard (try? file.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true,
|
|
FileManager.default.isReadableFile(atPath: file.path) else {
|
|
throw ArgumentError("File does not exist or is not a readable regular file.")
|
|
}
|
|
return try launch(file.standardizedFileURL)
|
|
}
|
|
|
|
private struct ArgumentError: LocalizedError {
|
|
let message: String
|
|
init(_ message: String) { self.message = message }
|
|
var errorDescription: String? { message }
|
|
}
|
|
}
|