// // JellyfinClient+Images.swift // // Copyright 2026 Brendan Szymanski // // 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 . // // 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 } }