project reorg
This commit is contained in:
221
QueueCube/Backend/API.swift
Normal file
221
QueueCube/Backend/API.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user