// // JellyfinDomainMapping.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 // Translates generated Jellyfin DTOs into Luminate's domain models, and domain queries back into // generated query parameters. // // This file is the only place the two vocabularies meet, so regenerating the OpenAPI client can // only break here -- never in a feature target. // // Enum translation is by raw value and is deliberately lossy: a server enum value Luminate does not // model becomes `nil`, or is dropped from a collection, rather than throwing. A Jellyfin release // that adds a new item kind or image type must not break browsing. // MARK: - Enum bridging extension Array where Element == JellyfinItemField { /// The generated field list, dropping any field the current specification does not define. var generated: [Components.Schemas.ItemFields] { compactMap { Components.Schemas.ItemFields(rawValue: $0.rawValue) } } } extension Array where Element == JellyfinMediaKind { /// The generated item-kind list, dropping any kind the current specification does not define. var generated: [Components.Schemas.BaseItemKind] { compactMap { Components.Schemas.BaseItemKind(rawValue: $0.rawValue) } } } extension Array where Element == JellyfinImageType { /// The generated image-type list, dropping any type the current specification does not define. var generated: [Components.Schemas.ImageType] { compactMap { Components.Schemas.ImageType(rawValue: $0.rawValue) } } } extension Array where Element == JellyfinSortField { /// The generated sort-field list, dropping any field the current specification does not define. var generated: [Components.Schemas.ItemSortBy] { compactMap { Components.Schemas.ItemSortBy(rawValue: $0.rawValue) } } } extension Array where Element == JellyfinSortOrder { /// The generated sort-order list. var generated: [Components.Schemas.SortOrder] { compactMap { Components.Schemas.SortOrder(rawValue: $0.rawValue) } } } // MARK: - DTO to domain extension JellyfinUser { /// Maps a generated user DTO, keeping only the fields Luminate's login and profile flows use. /// /// - Parameter dto: The generated user. init(_ dto: Components.Schemas.UserDto) { self.init( id: dto.id, name: dto.name, serverID: dto.serverId, primaryImageTag: dto.primaryImageTag, hasPassword: dto.hasPassword, hasConfiguredPassword: dto.hasConfiguredPassword, hasConfiguredEasyPassword: dto.hasConfiguredEasyPassword, lastLoginDate: dto.lastLoginDate, primaryImageAspectRatio: dto.primaryImageAspectRatio ) } } extension JellyfinServerInfo { /// Maps the pre-auth server information. /// /// - Parameter dto: The generated public system information. init(_ dto: Components.Schemas.PublicSystemInfo) { self.init( id: dto.id, serverName: dto.serverName, version: dto.version, productName: dto.productName, operatingSystem: dto.operatingSystem, localAddress: dto.localAddress, startupWizardCompleted: dto.startupWizardCompleted ) } /// Maps the authenticated server information, keeping the fields the public response also has. /// /// - Parameter dto: The generated system information. init(_ dto: Components.Schemas.SystemInfo) { self.init( id: dto.id, serverName: dto.serverName, version: dto.version, productName: dto.productName, operatingSystem: dto.operatingSystem, localAddress: dto.localAddress, startupWizardCompleted: dto.startupWizardCompleted ) } } extension JellyfinUserData { /// Maps a generated per-user item state. /// /// - Parameter dto: The generated user item data. init(_ dto: Components.Schemas.UserItemDataDto) { self.init( itemID: dto.itemId, key: dto.key, rating: dto.rating, playedPercentage: dto.playedPercentage, unplayedItemCount: dto.unplayedItemCount, playbackPositionTicks: dto.playbackPositionTicks, playCount: dto.playCount, isFavorite: dto.isFavorite, likes: dto.likes, lastPlayedDate: dto.lastPlayedDate, played: dto.played ) } } extension JellyfinMediaItem { /// Maps a generated item DTO. /// /// Fields the originating query did not request stay `nil`. Item kinds, collection types, and /// image types the current Luminate build does not model are dropped rather than raised as /// errors, so an unfamiliar server value degrades one row instead of failing the whole screen. /// /// - Parameter dto: The generated item. init(_ dto: Components.Schemas.BaseItemDto) { self.init( id: dto.id, name: dto.name, originalTitle: dto.originalTitle, serverID: dto.serverId, kind: dto._type.flatMap { JellyfinMediaKind(rawValue: $0.value1.rawValue) }, collectionType: dto.collectionType.flatMap { JellyfinCollectionType(rawValue: $0.value1.rawValue) }, overview: dto.overview, taglines: dto.taglines, genres: dto.genres, officialRating: dto.officialRating, communityRating: dto.communityRating, criticRating: dto.criticRating, productionYear: dto.productionYear, premiereDate: dto.premiereDate, dateCreated: dto.dateCreated, runTimeTicks: dto.runTimeTicks, indexNumber: dto.indexNumber, parentIndexNumber: dto.parentIndexNumber, parentID: dto.parentId, seriesName: dto.seriesName, seriesID: dto.seriesId, seasonID: dto.seasonId, seasonName: dto.seasonName, childCount: dto.childCount, width: dto.width, height: dto.height, primaryImageAspectRatio: dto.primaryImageAspectRatio, imageTags: Self.imageTags(from: dto.imageTags), backdropImageTags: dto.backdropImageTags ?? [], userData: dto.userData.map { JellyfinUserData($0.value1) } ) } /// Rekeys the server's string-keyed image tag map by ``JellyfinImageType``. /// /// - Parameter payload: The generated image tag container, if the item had one. /// - Returns: The tags Luminate can name, with unrecognised keys dropped. private static func imageTags( from payload: Components.Schemas.BaseItemDto.ImageTagsPayload? ) -> [JellyfinImageType: String] { guard let payload else { return [:] } var tags: [JellyfinImageType: String] = [:] tags.reserveCapacity(payload.additionalProperties.count) for (key, value) in payload.additionalProperties { guard let type = JellyfinImageType(rawValue: key) else { continue } tags[type] = value } return tags } } extension JellyfinLibrary { /// Maps a generated item DTO that represents a library folder. /// /// Libraries come back as ordinary items, but a library row needs far less than a media row, so /// this mapper is deliberately narrower than ``JellyfinMediaItem/init(_:)``. /// /// - Parameter dto: The generated library folder. init(_ dto: Components.Schemas.BaseItemDto) { self.init( id: dto.id, name: dto.name, collectionType: dto.collectionType.flatMap { JellyfinCollectionType(rawValue: $0.value1.rawValue) }, overview: dto.overview, primaryImageTag: dto.imageTags?.additionalProperties[JellyfinImageType.primary.rawValue], backdropImageTags: dto.backdropImageTags ?? [], childCount: dto.childCount ) } } extension JellyfinMediaPage { /// Maps a generated paged query result. /// /// - Parameter dto: The generated query result; a `nil` item list becomes an empty page. init(_ dto: Components.Schemas.BaseItemDtoQueryResult) { self.init( items: (dto.items ?? []).map(JellyfinMediaItem.init), totalRecordCount: dto.totalRecordCount, startIndex: dto.startIndex ) } } extension JellyfinSearchHint { /// Maps a generated search hint. /// /// - Parameter dto: The generated hint. init(_ dto: Components.Schemas.SearchHint) { self.init( itemID: dto.itemId, id: dto.id, name: dto.name, matchedTerm: dto.matchedTerm, kind: dto._type.flatMap { JellyfinMediaKind(rawValue: $0.value1.rawValue) }, productionYear: dto.productionYear, indexNumber: dto.indexNumber, parentIndexNumber: dto.parentIndexNumber, primaryImageTag: dto.primaryImageTag, thumbImageTag: dto.thumbImageTag, backdropImageTag: dto.backdropImageTag, isFolder: dto.isFolder, runTimeTicks: dto.runTimeTicks, series: dto.series ) } } extension JellyfinQuickConnectState { /// Maps a generated Quick Connect result. /// /// - Parameter dto: The generated pairing state. init(_ dto: Components.Schemas.QuickConnectResult) { self.init( authenticated: dto.authenticated, secret: dto.secret, code: dto.code, deviceID: dto.deviceId, deviceName: dto.deviceName, appName: dto.appName, appVersion: dto.appVersion, dateAdded: dto.dateAdded ) } } // MARK: - Domain to query extension Operations.GetItems.Input.Query { /// Builds the generated item query from Luminate's domain query. /// /// Only the parameters ``JellyfinMediaQuery`` models are set; every other generated parameter /// is left `nil` so the server applies its own default. /// /// - Parameter query: The domain query. init(_ query: JellyfinMediaQuery) { self.init( userId: query.userID, startIndex: query.startIndex, limit: query.limit, recursive: query.recursive, searchTerm: query.searchTerm, sortOrder: query.sortOrder?.generated, parentId: query.parentID, fields: query.fields?.generated, excludeItemTypes: query.excludeItemKinds?.generated, includeItemTypes: query.includeItemKinds?.generated, isFavorite: query.isFavorite, sortBy: query.sortBy?.generated, isPlayed: query.isPlayed, genres: query.genres, officialRatings: query.officialRatings, years: query.years, enableUserData: query.enableUserData, imageTypeLimit: query.imageTypeLimit, enableImageTypes: query.enableImageTypes?.generated, ids: query.ids, nameStartsWith: query.nameStartsWith, enableImages: query.enableImages ) } } extension Operations.GetItemImage.Input.Query { /// Builds the generated image query from Luminate's domain request. /// /// - Parameter request: The requested rendition. init(_ request: JellyfinImageRequest) { self.init( maxWidth: request.maxWidth, maxHeight: request.maxHeight, width: request.width, height: request.height, quality: request.quality, fillWidth: request.fillWidth, fillHeight: request.fillHeight, tag: request.tag, format: request.format .flatMap { Components.Schemas.ImageFormat(rawValue: $0.rawValue) } .map { FormatPayload(value1: $0) } ) } } extension Operations.GetItemImageByIndex.Input.Query { /// Builds the generated indexed-image query from Luminate's domain request. /// /// - Parameter request: The requested rendition. init(_ request: JellyfinImageRequest) { self.init( maxWidth: request.maxWidth, maxHeight: request.maxHeight, width: request.width, height: request.height, quality: request.quality, fillWidth: request.fillWidth, fillHeight: request.fillHeight, tag: request.tag, format: request.format .flatMap { Components.Schemas.ImageFormat(rawValue: $0.rawValue) } .map { FormatPayload(value1: $0) } ) } }