Compare commits
6
Commits
5f3b0937e2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b2598f666 | ||
|
|
8b6d4fe751 | ||
|
|
c65262e3ea | ||
|
|
95af1afef5 | ||
|
|
d124749242 | ||
|
|
8ce36e3e26 |
@@ -7,23 +7,31 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Client {
|
/// Every `Request` must pass through this `Client`.
|
||||||
|
/// Manages merging `Request` and default headers as well as creating the `URLRequest`.
|
||||||
|
/// Provides its own `JSONDecoder` and `JSONEncoder` with a PascalCase `CodingKey` strategy.
|
||||||
|
public struct Client {
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
private let server: String
|
||||||
private let session: URLSession
|
private let session: URLSession
|
||||||
private let defaultHeaders: [String:String]
|
private let defaultHeaders: [String:String]
|
||||||
private let server: String
|
|
||||||
private let decoder: JSONDecoder = .init()
|
private let decoder: JSONDecoder = .init()
|
||||||
private let encoder: JSONEncoder = .init()
|
private let encoder: JSONEncoder = .init()
|
||||||
|
|
||||||
// MARK: - Functions
|
// MARK: - Functions
|
||||||
init(server: String, session: URLSession = .shared, headers: [String:String] = [:]) {
|
public init(server: String, session: URLSession = .shared, headers: [String:String] = [:]) {
|
||||||
self.server = server
|
self.server = server
|
||||||
self.session = session
|
self.session = session
|
||||||
self.defaultHeaders = headers
|
self.defaultHeaders = headers
|
||||||
|
|
||||||
|
decoder.keyDecodingStrategy = .convertFromPascalCase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When all a client needs is the HTTP status code of an endpoint, this function should be used.
|
||||||
|
/// - Parameter request: Any Emby or Jellyfin `Request` conforming to the `Request` protocol.
|
||||||
|
/// - Throws:`RequestError.invalidResponse` if the response could not be coerced into a `HTTPURLResponse`.
|
||||||
|
/// - Returns: The HTTP status code.
|
||||||
func statusRequest(_ request: any Request) async throws -> Int {
|
func statusRequest(_ request: any Request) async throws -> Int {
|
||||||
do {
|
|
||||||
let urlRequest = try createRequestObject(for: request)
|
let urlRequest = try createRequestObject(for: request)
|
||||||
let (_, response) = try await session.data(for: urlRequest)
|
let (_, response) = try await session.data(for: urlRequest)
|
||||||
guard let response = response as? HTTPURLResponse else {
|
guard let response = response as? HTTPURLResponse else {
|
||||||
@@ -31,25 +39,27 @@ struct Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return response.statusCode
|
return response.statusCode
|
||||||
} catch {
|
|
||||||
throw RequestError.unknown(message: error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends a request to an Emby or Jellyfin server, and builds a model object from that response.
|
||||||
|
/// - Parameter request: Any Emby or Jellyfin `Request` conforming to the `Request` protocol.
|
||||||
|
/// - Throws:`RequestError.invalidResponse` if the response could not be coerced into a `HTTPURLResponse`.
|
||||||
|
/// - Returns: A `Codable` model object whose type is infered from the receiving variable.
|
||||||
func dataRequest<T>(_ request: any Request) async throws -> T? where T: Codable {
|
func dataRequest<T>(_ request: any Request) async throws -> T? where T: Codable {
|
||||||
do {
|
|
||||||
let urlRequest = try createRequestObject(for: request)
|
let urlRequest = try createRequestObject(for: request)
|
||||||
let (data, response) = try await session.data(for: urlRequest)
|
let (data, response) = try await session.data(for: urlRequest)
|
||||||
guard let response = response as? HTTPURLResponse else {
|
guard response is HTTPURLResponse else {
|
||||||
throw RequestError.invalidResponse
|
throw RequestError.invalidResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
return try decoder.decode(T.self, from: data)
|
return try decoder.decode(T.self, from: data)
|
||||||
} catch {
|
|
||||||
throw RequestError.unknown(message: error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verifies and creates a `URLRequest` from information provided by the `Request`.
|
||||||
|
/// Merges the `Request` headers, if any, with the default headers.
|
||||||
|
/// - Parameter request: Any Emby or Jellyfin `Request` conforming to the `Request` protocol.
|
||||||
|
/// - Throws: `RequestError.badURL` if unable to create a `URL` from the string.
|
||||||
|
/// - Returns: A fully formed `URLRequest` ready to send.
|
||||||
private func createRequestObject(for request: Request) throws -> URLRequest {
|
private func createRequestObject(for request: Request) throws -> URLRequest {
|
||||||
guard let url = URL(string: "\(server)\(request.endpoint)") else {
|
guard let url = URL(string: "\(server)\(request.endpoint)") else {
|
||||||
throw RequestError.badURL
|
throw RequestError.badURL
|
||||||
@@ -60,7 +70,7 @@ struct Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var urlRequest = URLRequest(url: url)
|
var urlRequest = URLRequest(url: url)
|
||||||
urlRequest.httpMethod = request.method
|
urlRequest.httpMethod = request.method.rawValue
|
||||||
urlRequest.allHTTPHeaderFields = headers
|
urlRequest.allHTTPHeaderFields = headers
|
||||||
return urlRequest
|
return urlRequest
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// RequestMethod.swift
|
||||||
|
// JellyKit
|
||||||
|
//
|
||||||
|
// Created by Alex Loren on 10/19/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
/// What method should be used when sending a `Request` to Emby or Jellyfin.
|
||||||
|
/// Usage of this enum prevents any chance of misspelling the method.
|
||||||
|
public enum RequestMethod: String {
|
||||||
|
case get = "GET"
|
||||||
|
case head = "HEAD"
|
||||||
|
case post = "POST"
|
||||||
|
case delete = "DELETE"
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// PascalCaseKey+KeyDecodingStrategy.swift
|
||||||
|
// JellyKit
|
||||||
|
//
|
||||||
|
// Created by Alex Loren on 10/19/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension JSONDecoder.KeyDecodingStrategy {
|
||||||
|
// MARK: - Computed Properties
|
||||||
|
|
||||||
|
/// A key decoding strategy that converts pascal-case keys to camel-case keys.
|
||||||
|
static var convertFromPascalCase: Self {
|
||||||
|
.custom { codingPath in
|
||||||
|
let key = codingPath.last!.stringValue
|
||||||
|
let converted = key.prefix(1).lowercased() + key.dropFirst()
|
||||||
|
return PascalCaseKey(stringValue: converted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simply a reusable `CodingKey` for converting PascalCase to camelCase.
|
||||||
|
private struct PascalCaseKey: CodingKey {
|
||||||
|
// MARK: - Properties
|
||||||
|
let stringValue: String
|
||||||
|
let intValue: Int?
|
||||||
|
|
||||||
|
// MARK: - Functions
|
||||||
|
init(stringValue: String) {
|
||||||
|
self.stringValue = stringValue
|
||||||
|
intValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(intValue: Int) {
|
||||||
|
stringValue = String(intValue)
|
||||||
|
self.intValue = intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,18 +5,24 @@
|
|||||||
// Created by Alex Loren on 10/13/26.
|
// Created by Alex Loren on 10/13/26.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
/// Contains the information of any `User` that is reported back by Emby or Jellyfin.
|
||||||
|
/// This model does not currently contain all data reported by the server.
|
||||||
|
/// Properties will become available as needed, not prior.
|
||||||
struct User: Codable {
|
struct User: Codable {
|
||||||
var id: String
|
// MARK: - Properties
|
||||||
var serverId: String
|
|
||||||
var name: String
|
|
||||||
var hasPassword: Bool
|
|
||||||
var primaryImageTag: String
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
/// A server generated unique Id for the `User`.
|
||||||
case id = "Id"
|
var id: String
|
||||||
case serverId = "ServerId"
|
|
||||||
case name = "Name"
|
/// The `User` belongs to the server with this Id.
|
||||||
case hasPassword = "HasPassword"
|
var serverId: String
|
||||||
case primaryImageTag = "PrimaryImageTag"
|
|
||||||
}
|
/// A `User` or server administer given name
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
/// If `true` the `User` must be authenticated before making requests on their behalf.
|
||||||
|
var hasPassword: Bool
|
||||||
|
|
||||||
|
/// If not `nil` a request can be made to retreive the profile image for the `User`.
|
||||||
|
var primaryImageTag: String?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,18 @@
|
|||||||
// Created by Alex Loren on 10/13/26.
|
// Created by Alex Loren on 10/13/26.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
/// The base for any `Request` made by JellyKit
|
||||||
|
|
||||||
protocol Request {
|
protocol Request {
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
|
/// This must begin with a forward slash.
|
||||||
|
/// Points to the resource on the Emby or Jellyfin server.
|
||||||
var endpoint: String { get set }
|
var endpoint: String { get set }
|
||||||
var method: String { get set }
|
|
||||||
|
/// What should this request be sent as?
|
||||||
|
var method: RequestMethod { get set }
|
||||||
|
|
||||||
|
/// Any additional headers that are specific to *this* request.
|
||||||
|
/// Headers needed for every reqeust should be set at `Client` creation.
|
||||||
var headers: [String:String]? { get set }
|
var headers: [String:String]? { get set }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// PingRequest.swift
|
||||||
|
// JellyKit
|
||||||
|
//
|
||||||
|
// Created by Alex Loren on 10/19/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Sends a ping to the Emby or Jellyfin server.
|
||||||
|
/// Should be used exclusively with the `Client.statusRequest` function.
|
||||||
|
/// The server will respond with a code of 200 and a body string when successful.
|
||||||
|
struct PingRequest: Request {
|
||||||
|
// MARK: - Properties
|
||||||
|
var endpoint: String = "/System/Ping"
|
||||||
|
var method: RequestMethod = .get
|
||||||
|
var headers: [String : String]? = nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user