project reorg

This commit is contained in:
2025-06-10 11:09:44 -07:00
parent 6c183aea03
commit 13b27a2a1a
14 changed files with 38 additions and 30 deletions

221
QueueCube/Backend/API.swift Normal file
View File

@@ -0,0 +1,221 @@
//
// API.swift
// QueueCube
//
// Created by James Magahern on 3/3/25.
//
import Foundation
struct MediaItem: Codable
{
let filename: String?
let title: String?
let id: Int
let current: Bool?
let playing: Bool?
let metadata: Metadata?
// MARK: - Types
struct Metadata: Codable
{
let title: String?
let description: String?
let siteName: String?
}
}
struct NowPlayingInfo: Codable
{
let playingItem: MediaItem?
let isPaused: Bool
let volume: Int
}
struct API
{
let baseURL: URL
static func fromSettings() -> Self? {
let settings = Settings.fromDefaults()
guard let baseURL = settings.serverURL.flatMap({ URL(string: $0) })
else { return nil }
return API(baseURL: baseURL)
}
init(baseURL: URL) {
self.baseURL = baseURL
}
public func fetchNowPlayingInfo() async throws -> NowPlayingInfo {
try await request()
.path("/nowplaying")
.json()
}
public func fetchPlaylist() async throws -> [MediaItem] {
try await request()
.path("/playlist")
.json()
}
public func fetchFavorites() async throws -> [MediaItem] {
try await request()
.path("/favorites")
.json()
}
public func play() async throws {
try await request()
.path("/play")
.post()
}
public func pause() async throws {
try await request()
.path("/pause")
.post()
}
public func skip(_ to: Int? = nil) async throws {
let path = if let to { "/skip/\(to)" } else { "/skip" }
try await request()
.path(path)
.post()
}
public func previous() async throws {
try await request()
.path("/previous")
.post()
}
public func add(mediaURL: String) async throws {
try await request()
.path("/playlist")
.body([ "url" : mediaURL ])
.post()
}
public func addFavorite(mediaURL: String) async throws {
try await request()
.path("/favorites")
.body([ "filename" : mediaURL ])
.post()
}
public func delete(index: Int) async throws {
try await request()
.path("/playlist/\(index)")
.method(.delete)
.execute()
}
public func setVolume(_ value: Double) async throws {
try await request()
.path("/volume")
.body([ "volume" : Int(value * 100) ])
.post()
}
public func events() async throws -> AsyncStream<StreamEvent> {
return AsyncStream { continuation in
var websocketTask: URLSessionWebSocketTask = spawnWebsocketTask(with: continuation)
Task {
while true {
try await Task.sleep(for: .seconds(5))
websocketTask.sendPing { error in
if let error {
print("Ping error: \(error). Trying to reconnect.")
continuation.yield(.error(.websocketError(error)))
websocketTask = spawnWebsocketTask(with: continuation)
} else {
continuation.yield(.event(Event(type: .receivedWebsocketPong)))
}
}
}
}
}
}
private func spawnWebsocketTask(
with continuation: AsyncStream<StreamEvent>.Continuation
) -> URLSessionWebSocketTask
{
let url = request()
.path("/events")
.websocket()
let websocketTask = URLSession.shared.webSocketTask(with: url)
websocketTask.resume()
Task {
do {
let event = { (data: Data) in
try JSONDecoder().decode(Event.self, from: data)
}
while websocketTask.state == .running {
switch try await websocketTask.receive() {
case .string(let string):
let event = try event(string.data(using: .utf8)!)
continuation.yield(.event(event))
case .data(let data):
let event = try event(data)
continuation.yield(.event(event))
default:
break
}
}
} catch {
print("Websocket Error: \(error)")
}
}
return websocketTask
}
private func request() -> RequestBuilder {
RequestBuilder(url: baseURL)
}
// MARK: - Types
enum Error: Swift.Error
{
case apiNotConfigured
case websocketError(Swift.Error)
}
enum StreamEvent {
case event(Event)
case error(API.Error)
}
struct Event: Decodable
{
let type: EventType
enum CodingKeys: String, CodingKey {
case type = "event"
}
enum EventType: String, Decodable {
case playlistUpdate = "playlist_update"
case nowPlayingUpdate = "now_playing_update"
case volumeUpdate = "volume_update"
case favoritesUpdate = "favorites_update"
case metadataUpdate = "metadata_update"
case mpdUpdate = "mpd_update"
// Private UI events
case receivedWebsocketPong
}
}
}