// // JellyfinClient+UserState.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 /// Watched state and favourites. extension JellyfinClient { /// Marks an item watched. /// /// Marking a season or series watched marks every episode beneath it, which is why this returns /// the server's recomputed state rather than assuming the local one. /// /// - Parameters: /// - itemID: The item to mark. Must not be empty. /// - userID: Whose state to change; `nil` means the signed-in account. /// - datePlayed: When it was watched; `nil` means now. /// - Returns: The item's updated playback state. /// - Throws: ``JellyfinClientError/invalidArgument(name:)`` if `itemID` is empty, or /// ``JellyfinClientError/notFound`` if no such item exists. package func markPlayed(itemID: String, userID: String?, datePlayed: Date?) async throws -> JellyfinUserData { guard !itemID.isEmpty else { throw JellyfinClientError.invalidArgument(name: "itemID") } let input = Operations.MarkPlayedItem.Input( path: .init(itemId: itemID), query: .init(userId: userID, datePlayed: datePlayed) ) switch try await current.markPlayedItem(input) { case .ok(let response): return JellyfinUserData(response.body.payload) case .notFound: throw JellyfinClientError.notFound case .serviceUnavailable(let response): throw JellyfinClientError.serviceUnavailable( retryAfterSeconds: response.headers.retryAfter ) case .unauthorized: throw JellyfinClientError.unauthorized case .forbidden: throw JellyfinClientError.forbidden case .undocumented(let statusCode, _): throw JellyfinClientError.unexpectedStatus( operation: Operations.MarkPlayedItem.id, statusCode: statusCode ) } } /// Marks an item unwatched and clears its resume position. /// /// - Parameters: /// - itemID: The item to mark. Must not be empty. /// - userID: Whose state to change; `nil` means the signed-in account. /// - Returns: The item's updated playback state. /// - Throws: ``JellyfinClientError/invalidArgument(name:)`` if `itemID` is empty, or /// ``JellyfinClientError/notFound`` if no such item exists. package func markUnplayed(itemID: String, userID: String?) async throws -> JellyfinUserData { guard !itemID.isEmpty else { throw JellyfinClientError.invalidArgument(name: "itemID") } let input = Operations.MarkUnplayedItem.Input( path: .init(itemId: itemID), query: .init(userId: userID) ) switch try await current.markUnplayedItem(input) { case .ok(let response): return JellyfinUserData(response.body.payload) case .notFound: throw JellyfinClientError.notFound case .serviceUnavailable(let response): throw JellyfinClientError.serviceUnavailable( retryAfterSeconds: response.headers.retryAfter ) case .unauthorized: throw JellyfinClientError.unauthorized case .forbidden: throw JellyfinClientError.forbidden case .undocumented(let statusCode, _): throw JellyfinClientError.unexpectedStatus( operation: Operations.MarkUnplayedItem.id, statusCode: statusCode ) } } /// Adds an item to the user's favourites. /// /// - Parameters: /// - itemID: The item to favourite. Must not be empty. /// - userID: Whose favourites to change; `nil` means the signed-in account. /// - Returns: The item's updated playback state. /// - Throws: ``JellyfinClientError/invalidArgument(name:)`` if `itemID` is empty, or /// ``JellyfinClientError/unauthorized`` if the token is missing or rejected. package func markFavorite(itemID: String, userID: String?) async throws -> JellyfinUserData { guard !itemID.isEmpty else { throw JellyfinClientError.invalidArgument(name: "itemID") } let input = Operations.MarkFavoriteItem.Input( path: .init(itemId: itemID), query: .init(userId: userID) ) switch try await current.markFavoriteItem(input) { case .ok(let response): return JellyfinUserData(response.body.payload) case .serviceUnavailable(let response): throw JellyfinClientError.serviceUnavailable( retryAfterSeconds: response.headers.retryAfter ) case .unauthorized: throw JellyfinClientError.unauthorized case .forbidden: throw JellyfinClientError.forbidden case .undocumented(let statusCode, _): throw JellyfinClientError.unexpectedStatus( operation: Operations.MarkFavoriteItem.id, statusCode: statusCode ) } } /// Removes an item from the user's favourites. /// /// - Parameters: /// - itemID: The item to unfavourite. Must not be empty. /// - userID: Whose favourites to change; `nil` means the signed-in account. /// - Returns: The item's updated playback state. /// - Throws: ``JellyfinClientError/invalidArgument(name:)`` if `itemID` is empty, or /// ``JellyfinClientError/unauthorized`` if the token is missing or rejected. package func unmarkFavorite(itemID: String, userID: String?) async throws -> JellyfinUserData { guard !itemID.isEmpty else { throw JellyfinClientError.invalidArgument(name: "itemID") } let input = Operations.UnmarkFavoriteItem.Input( path: .init(itemId: itemID), query: .init(userId: userID) ) switch try await current.unmarkFavoriteItem(input) { case .ok(let response): return JellyfinUserData(response.body.payload) case .serviceUnavailable(let response): throw JellyfinClientError.serviceUnavailable( retryAfterSeconds: response.headers.retryAfter ) case .unauthorized: throw JellyfinClientError.unauthorized case .forbidden: throw JellyfinClientError.forbidden case .undocumented(let statusCode, _): throw JellyfinClientError.unexpectedStatus( operation: Operations.UnmarkFavoriteItem.id, statusCode: statusCode ) } } }