Files
terebi/Sources/CLI/PlayerCommand.swift
T

85 lines
4.1 KiB
Swift

import Foundation
enum PlayerCommand {
case help
case launch(URL?, newInstance: Bool, noConfig: Bool)
static let usage = """
Usage: terebi [--new-instance] [--no-config] [FILE | YOUTUBE_URL]
terebi [--new-instance] [--no-config] --url YOUTUBE_URL
terebi [--new-instance] [--no-config] -- FILE
terebi --help
Open one local media file or YouTube video and start playback, resuming saved
video progress when available. 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.
-- Treat the following argument as a filename, even with a leading '-'.
-h, --help Show this help without launching the player.
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 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()
}
if inputs.isEmpty { return .launch(nil, newInstance: newInstance, noConfig: noConfig) }
if inputs == ["--help"] || inputs == ["-h"] { return .help }
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)
}
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 .launch(try YouTubeResolver.validateURL(input), newInstance: newInstance, noConfig: noConfig)
}
} 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 .launch(file.standardizedFileURL, newInstance: newInstance, noConfig: noConfig)
}
private struct ArgumentError: LocalizedError {
let message: String
init(_ message: String) { self.message = message }
var errorDescription: String? { message }
}
}