144 lines
5.8 KiB
Swift
144 lines
5.8 KiB
Swift
//
|
|
// JellyfinClient+Images.swift
|
|
//
|
|
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
|
|
import Foundation
|
|
import LuminateAPI
|
|
import LuminateCore
|
|
import OpenAPIRuntime
|
|
|
|
/// Artwork downloads.
|
|
extension JellyfinClient {
|
|
/// Downloads one artwork image, fully buffered.
|
|
///
|
|
/// The server renders artwork on demand, so ask for the size that will actually be drawn rather
|
|
/// than the original. Passing ``JellyfinImageRequest/tag`` lets caches key the response so a
|
|
/// replaced poster is never served stale.
|
|
///
|
|
/// The body is collected with a 25 MiB ceiling. No poster, backdrop, or logo comes close; a
|
|
/// larger body raises the OpenAPI runtime's own oversize error rather than a
|
|
/// ``JellyfinClientError``, because that is a server misconfiguration and not a Jellyfin status.
|
|
///
|
|
/// - Parameters:
|
|
/// - itemID: The item that owns the artwork. Must not be empty.
|
|
/// - type: Which artwork slot to fetch.
|
|
/// - index: Which image within the slot; `nil` means the first.
|
|
/// - request: The rendition to ask the server for.
|
|
/// - Returns: The encoded image bytes.
|
|
/// - Throws: ``JellyfinClientError/invalidArgument(name:)`` if `itemID` is empty or the current
|
|
/// specification does not define `type`, or ``JellyfinClientError/notFound`` if the item has
|
|
/// no image in that slot.
|
|
package func image(
|
|
itemID: String,
|
|
type: JellyfinImageType,
|
|
index: Int32?,
|
|
request: JellyfinImageRequest
|
|
) async throws -> Data {
|
|
guard !itemID.isEmpty else {
|
|
throw JellyfinClientError.invalidArgument(name: "itemID")
|
|
}
|
|
guard let imageType = Components.Schemas.ImageType(rawValue: type.rawValue) else {
|
|
throw JellyfinClientError.invalidArgument(name: "type")
|
|
}
|
|
guard let index else {
|
|
return try await firstImage(itemID: itemID, imageType: imageType, request: request)
|
|
}
|
|
return try await indexedImage(itemID: itemID, imageType: imageType, index: index, request: request)
|
|
}
|
|
|
|
/// Downloads the first image in a slot.
|
|
///
|
|
/// - Parameters:
|
|
/// - itemID: The item that owns the artwork.
|
|
/// - imageType: The generated artwork slot.
|
|
/// - request: The rendition to ask the server for.
|
|
/// - Returns: The encoded image bytes.
|
|
/// - Throws: ``JellyfinClientError/notFound`` if the item has no image in that slot.
|
|
private func firstImage(
|
|
itemID: String,
|
|
imageType: Components.Schemas.ImageType,
|
|
request: JellyfinImageRequest
|
|
) async throws -> Data {
|
|
let input = Operations.GetItemImage.Input(
|
|
path: .init(itemId: itemID, imageType: .init(value1: imageType)),
|
|
query: .init(request)
|
|
)
|
|
switch try await current.getItemImage(input) {
|
|
case .ok(let response):
|
|
switch response.body {
|
|
case .image_Ast_(let body):
|
|
return try await Data(collecting: body, upTo: Self.maximumImageBytes)
|
|
}
|
|
case .notFound:
|
|
throw JellyfinClientError.notFound
|
|
case .serviceUnavailable(let response):
|
|
throw JellyfinClientError.serviceUnavailable(
|
|
retryAfterSeconds: response.headers.retryAfter
|
|
)
|
|
case .undocumented(let statusCode, _):
|
|
throw JellyfinClientError.unexpectedStatus(
|
|
operation: Operations.GetItemImage.id,
|
|
statusCode: statusCode
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Downloads a specific image within a slot.
|
|
///
|
|
/// - Parameters:
|
|
/// - itemID: The item that owns the artwork.
|
|
/// - imageType: The generated artwork slot.
|
|
/// - index: Which image within the slot.
|
|
/// - request: The rendition to ask the server for.
|
|
/// - Returns: The encoded image bytes.
|
|
/// - Throws: ``JellyfinClientError/notFound`` if the item has no image at that index.
|
|
private func indexedImage(
|
|
itemID: String,
|
|
imageType: Components.Schemas.ImageType,
|
|
index: Int32,
|
|
request: JellyfinImageRequest
|
|
) async throws -> Data {
|
|
let input = Operations.GetItemImageByIndex.Input(
|
|
path: .init(itemId: itemID, imageType: .init(value1: imageType), imageIndex: index),
|
|
query: .init(request)
|
|
)
|
|
switch try await current.getItemImageByIndex(input) {
|
|
case .ok(let response):
|
|
switch response.body {
|
|
case .image_Ast_(let body):
|
|
return try await Data(collecting: body, upTo: Self.maximumImageBytes)
|
|
}
|
|
case .notFound:
|
|
throw JellyfinClientError.notFound
|
|
case .serviceUnavailable(let response):
|
|
throw JellyfinClientError.serviceUnavailable(
|
|
retryAfterSeconds: response.headers.retryAfter
|
|
)
|
|
case .undocumented(let statusCode, _):
|
|
throw JellyfinClientError.unexpectedStatus(
|
|
operation: Operations.GetItemImageByIndex.id,
|
|
statusCode: statusCode
|
|
)
|
|
}
|
|
}
|
|
|
|
/// The largest artwork response Luminate will buffer, in bytes.
|
|
private static var maximumImageBytes: Int { 25 * 1024 * 1024 }
|
|
}
|