Files
terebi/Sources/CLI/TerebiCLI.swift
T

106 lines
5.1 KiB
Swift

import AppKit
import Darwin
import TerebiConfiguration
@main
@MainActor
enum TerebiCLI {
static func main() async {
let command: PlayerCommand
do {
command = try PlayerCommand.parse(Array(CommandLine.arguments.dropFirst()))
} catch {
fail(error.localizedDescription, code: 64)
}
switch command {
case .help:
print(PlayerCommand.usage)
case .printDefaultConfig:
print(PlayerConfiguration.defaultTOML, terminator: "")
case .checkSource(let mediaID, let episode):
print(await SourceCheck.run(mediaID: mediaID, episode: episode).json)
case .launch(let url, let newInstance, let noConfig, let progressFile):
await launch(url: url, sourceFile: nil, newInstance: newInstance, noConfig: noConfig, progressFile: progressFile)
case .launchSource(let sourceFile, let newInstance, let noConfig, let progressFile):
await launch(url: nil, sourceFile: sourceFile, newInstance: newInstance, noConfig: noConfig, progressFile: progressFile)
case .launchQueue(let queueFile, let newInstance, let noConfig):
await launch(url: nil, sourceFile: nil, newInstance: newInstance, noConfig: noConfig,
progressFile: nil, queueFile: queueFile)
case .enqueue(let queueFile, let noConfig):
await launch(url: StreamQueue.enqueueURL(for: queueFile), sourceFile: nil,
newInstance: false, noConfig: noConfig, progressFile: nil)
}
}
private static func launch(url: URL?, sourceFile: URL?, newInstance: Bool, noConfig: Bool,
progressFile: URL?, queueFile: URL? = nil) async {
if let progressFile {
var existing = stat()
guard lstat(progressFile.path, &existing) != 0 else {
fail("Progress file already exists: \(progressFile.path). Supply a nonexistent destination.", code: 73)
}
guard errno == ENOENT else {
fail("Could not inspect progress file \(progressFile.path): \(String(cString: strerror(errno))).", code: 73)
}
}
let configURL = PlayerConfiguration.fileURL(environment: ProcessInfo.processInfo.environment,
homeDirectory: FileManager.default.homeDirectoryForCurrentUser)
if !noConfig {
do {
_ = try PlayerConfiguration.load(from: configURL)
} catch {
fail(error.localizedDescription, code: 78)
}
}
var size: UInt32 = 0
_ = _NSGetExecutablePath(nil, &size)
var path = [CChar](repeating: 0, count: Int(size))
guard _NSGetExecutablePath(&path, &size) == 0 else {
fail("Could not locate the CLI executable.", code: 69)
}
let executable = path.withUnsafeBufferPointer {
URL(fileURLWithPath: String(cString: $0.baseAddress!)).resolvingSymlinksInPath()
}
let macOS = executable.deletingLastPathComponent()
let contents = macOS.deletingLastPathComponent()
let app = contents.deletingLastPathComponent()
guard macOS.lastPathComponent == "MacOS", contents.lastPathComponent == "Contents",
app.pathExtension == "app" else {
fail("The CLI must be installed inside terebi.app. Rebuild and reinstall the bundled app.", code: 69)
}
if let error = FFmpegRuntime.error(for: app) {
fail(error, code: 69)
}
let configuration = NSWorkspace.OpenConfiguration()
configuration.createsNewApplicationInstance = newInstance
// LaunchServices does not inherit the invoking shell's XDG environment.
configuration.arguments = noConfig ? ["--no-config"] : ["--config-file", configURL.absoluteString]
if let progressFile {
configuration.arguments += ["--progress-file", progressFile.path]
}
if let queueFile {
configuration.arguments += ["--queue-file=" + queueFile.path]
}
if let sourceFile {
configuration.arguments += ["--source-file", sourceFile.path]
}
if newInstance, let url {
configuration.arguments += ["--open-media", url.absoluteString]
}
do {
// Launch an application, not a document, so SwiftUI creates its singleton window.
_ = try await NSWorkspace.shared.openApplication(at: app, configuration: configuration)
if !newInstance, let url {
_ = try await NSWorkspace.shared.open([url], withApplicationAt: app, configuration: configuration)
}
} catch {
fail("Could not launch the player: \(error.localizedDescription)", code: 69)
}
}
private static func fail(_ message: String, code: Int32) -> Never {
FileHandle.standardError.write(Data("terebi: \(message)\n".utf8))
exit(code)
}
}