diff --git a/.gitignore b/.gitignore index 853316f..d8df9c4 100644 --- a/.gitignore +++ b/.gitignore @@ -90,4 +90,6 @@ fastlane/test_output iOSInjectionProject/ # Rust -PlayerCore/target/** \ No newline at end of file +PlayerCore/target/** +Spottie/PlayerCore/libspottie_player_core.* + diff --git a/Spottie/PlayerCore/PlayerCore.swift b/Spottie/PlayerCore/PlayerCore.swift index f0a34c9..c2f56fe 100644 --- a/Spottie/PlayerCore/PlayerCore.swift +++ b/Spottie/PlayerCore/PlayerCore.swift @@ -6,26 +6,129 @@ // import Foundation +import Combine class PlayerCore { - func run() { + class CaptureBag { + let jsonDecoder = JSONDecoder() + let currentState$: CurrentValueSubject + init(stateSubject: CurrentValueSubject) { + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + jsonDecoder.dateDecodingStrategy = .secondsSince1970 + currentState$ = stateSubject + } + } + let captureBag: CaptureBag + let curlClient: CurlClient + + private var state$: CurrentValueSubject = CurrentValueSubject(nil) + var statePublisher: AnyPublisher { + state$.eraseToAnyPublisher() + } + + private var cancellables = [AnyCancellable]() + + init() { + let socketPath = PlayerCore.getSocketPath() + curlClient = CurlClient(socketPath: socketPath) + captureBag = CaptureBag(stateSubject: state$) + run(socketPath: socketPath) + } + + static func getSocketPath() -> String { + let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "YW4H592L4M.ljk.spottie")! + return container.appendingPathComponent("player_core.sock").path + } + + func run(socketPath: String) { + // https://oleb.net/blog/2015/06/c-callbacks-in-swift/ + let b = Unmanaged.passUnretained(captureBag); let t = Thread.init { - /* - librespot_init(f.toOpaque()) { user_data, ptr, len in - if let pointer = ptr { - let data = Data(bytes: pointer, count: Int(len)) - do { - let ff: Foo = Unmanaged.fromOpaque(user_data!).takeUnretainedValue(); - let json = try ff.rawDecode(data) - print(json) - } catch { - print("json error: \(error.localizedDescription)") - } + librespot_init(socketPath, b.toOpaque()) { user_data, ptr, len in + guard let pointer = ptr else { return } + let data = Data(bytes: pointer, count: Int(len)) + let bag: CaptureBag = Unmanaged.fromOpaque(user_data!).takeUnretainedValue(); + if let stateUpdate = try? bag.jsonDecoder.decode(PlayerCoreState.self, from: data) { + bag.currentState$.send(stateUpdate) + } + if let debugObj = try? JSONSerialization.jsonObject(with: data, options: []) { + print(debugObj) } } - */ } - t.name = "AnvilRust" + t.name = "ljk.spottie.PlayerCore" t.start() } + + private func waitForPlayerCoreToBeReady() async { + return await withCheckedContinuation { continuation in + statePublisher.compactMap { $0 }.first().sink { _ in + continuation.resume() + }.store(in: &cancellables) + } + } + + func token() async -> Result { + await waitForPlayerCoreToBeReady() + + var req = URLRequest(url: URL(string: "http://localhost/token")!) + req.httpMethod = "POST" + let result = await curlClient.run(req) + + switch result { + case .success(let received): + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let tokenObj = try? decoder.decode(TokenObject.self, from: received.data!) + return Result.success(tokenObj!) + case .failure(let err): + return Result.failure(err) + } + } + + func play(_ trackIds: [String]) async { + let jsonBody = try! JSONSerialization.data(withJSONObject: ["track_ids": trackIds], options: []) + + var req = URLRequest(url: URL(string: "http://localhost/queue/play")!) + req.httpMethod = "POST" + req.setValue("application/json", forHTTPHeaderField: "Content-Type") + req.httpBody = jsonBody + + let result = await curlClient.run(req) + switch result { + case .failure(let err): + print("Failure: \(err)") + case .success(let response): + print("Success! \(response)") + } + } + + func togglePlayback() async { + var req = URLRequest(url: URL(string: "http://localhost/queue/toggle")!) + req.httpMethod = "POST" + let _ = await curlClient.run(req) + } + + func next() async { + var req = URLRequest(url: URL(string: "http://localhost/queue/next")!) + req.httpMethod = "POST" + let _ = await curlClient.run(req) + } + + func previous() async { + var req = URLRequest(url: URL(string: "http://localhost/queue/previous")!) + req.httpMethod = "POST" + let _ = await curlClient.run(req) + } + + func seek(_ positionMs: Int) async { + let jsonBody = try! JSONSerialization.data(withJSONObject: ["position_ms": positionMs], options: []) + + var req = URLRequest(url: URL(string: "http://localhost/player/seek")!) + req.httpMethod = "POST" + req.setValue("application/json", forHTTPHeaderField: "Content-Type") + req.httpBody = jsonBody + + let _ = await curlClient.run(req) + } } diff --git a/Spottie/PlayerCore/PlayerCoreState.swift b/Spottie/PlayerCore/PlayerCoreState.swift new file mode 100644 index 0000000..69355ee --- /dev/null +++ b/Spottie/PlayerCore/PlayerCoreState.swift @@ -0,0 +1,28 @@ +// +// PlayerCoreState.swift +// PlayerCoreState +// +// Created by Lee Jun Kit on 24/8/21. +// + +import Foundation + +struct PlayerCoreState: Decodable { + struct Player: Decodable { + enum PlayState: String, Decodable { + case playing, paused, stopped + } + + let state: PlayState + let since: Date? + let elapsed: Double? + let trackId: String? + } + + struct Queue: Decodable { + let currentIndex: Int? + } + + let player: Player + let queue: Queue +} diff --git a/Spottie/Spottie-Bridging-Header.h b/Spottie/Spottie-Bridging-Header.h index ce8ce5e..9f9368f 100644 --- a/Spottie/Spottie-Bridging-Header.h +++ b/Spottie/Spottie-Bridging-Header.h @@ -2,5 +2,5 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // -#include "player_core.h" +#include "libspottie_player_core.h" #include "spottie_curl.h"