225 lines
6.2 KiB
Swift
225 lines
6.2 KiB
Swift
//
|
|
// Swiftfin is subject to the terms of the Mozilla Public
|
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
|
//
|
|
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
|
//
|
|
|
|
import Combine
|
|
import Foundation
|
|
import Get
|
|
import JellyfinAPI
|
|
import OrderedCollections
|
|
|
|
class IdentifyItemViewModel: ViewModel, Stateful, Eventful {
|
|
|
|
// MARK: - Events
|
|
|
|
enum Event: Equatable {
|
|
case updated
|
|
case cancelled
|
|
case error(JellyfinAPIError)
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
enum Action: Equatable {
|
|
case cancel
|
|
case search(name: String? = nil, originalTitle: String? = nil, year: Int? = nil)
|
|
case update(RemoteSearchResult)
|
|
}
|
|
|
|
// MARK: - State
|
|
|
|
enum State: Hashable {
|
|
case content
|
|
case searching
|
|
case updating
|
|
}
|
|
|
|
@Published
|
|
var item: BaseItemDto
|
|
@Published
|
|
var searchResults: [RemoteSearchResult] = []
|
|
@Published
|
|
var state: State = .content
|
|
|
|
private var updateTask: AnyCancellable?
|
|
private var searchTask: AnyCancellable?
|
|
|
|
private let eventSubject = PassthroughSubject<Event, Never>()
|
|
|
|
var events: AnyPublisher<Event, Never> {
|
|
eventSubject
|
|
.receive(on: RunLoop.main)
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
// MARK: - Initializer
|
|
|
|
init(item: BaseItemDto) {
|
|
self.item = item
|
|
super.init()
|
|
}
|
|
|
|
// MARK: - Respond to Actions
|
|
|
|
func respond(to action: Action) -> State {
|
|
switch action {
|
|
|
|
case .cancel:
|
|
updateTask?.cancel()
|
|
searchTask?.cancel()
|
|
|
|
return .content
|
|
|
|
case let .search(name, originalTitle, year):
|
|
searchTask?.cancel()
|
|
|
|
searchTask = Task {
|
|
do {
|
|
let newResults = try await self.searchItem(
|
|
name: name,
|
|
originalTitle: originalTitle,
|
|
year: year
|
|
)
|
|
|
|
await MainActor.run {
|
|
self.searchResults = newResults
|
|
self.state = .content
|
|
}
|
|
} catch {
|
|
let apiError = JellyfinAPIError(error.localizedDescription)
|
|
await MainActor.run {
|
|
self.state = .content
|
|
self.eventSubject.send(.error(apiError))
|
|
}
|
|
}
|
|
}.asAnyCancellable()
|
|
return .searching
|
|
|
|
case let .update(searchResult):
|
|
updateTask?.cancel()
|
|
|
|
updateTask = Task {
|
|
do {
|
|
try await updateItem(searchResult)
|
|
|
|
await MainActor.run {
|
|
self.state = .content
|
|
self.eventSubject.send(.updated)
|
|
}
|
|
} catch {
|
|
let apiError = JellyfinAPIError(error.localizedDescription)
|
|
await MainActor.run {
|
|
self.state = .content
|
|
self.eventSubject.send(.error(apiError))
|
|
}
|
|
}
|
|
}.asAnyCancellable()
|
|
|
|
return .updating
|
|
}
|
|
}
|
|
|
|
// MARK: - Return Matching Elements (To Be Overridden)
|
|
|
|
private func searchItem(
|
|
name: String?,
|
|
originalTitle: String?,
|
|
year: Int?
|
|
) async throws -> [RemoteSearchResult] {
|
|
|
|
guard let itemID = item.id, let itemType = item.type else {
|
|
return []
|
|
}
|
|
|
|
switch itemType {
|
|
case .boxSet:
|
|
let parameters = BoxSetInfoRemoteSearchQuery(
|
|
itemID: itemID,
|
|
searchInfo: BoxSetInfo(
|
|
name: name,
|
|
originalTitle: originalTitle,
|
|
year: year
|
|
)
|
|
)
|
|
let request = Paths.getBoxSetRemoteSearchResults(parameters)
|
|
let response = try await userSession.client.send(request)
|
|
|
|
return response.value
|
|
|
|
case .movie:
|
|
let parameters = MovieInfoRemoteSearchQuery(
|
|
itemID: itemID,
|
|
searchInfo: MovieInfo(
|
|
name: name,
|
|
originalTitle: originalTitle,
|
|
year: year
|
|
)
|
|
)
|
|
let request = Paths.getMovieRemoteSearchResults(parameters)
|
|
let response = try await userSession.client.send(request)
|
|
|
|
return response.value
|
|
|
|
case .person:
|
|
let parameters = PersonLookupInfoRemoteSearchQuery(
|
|
itemID: itemID,
|
|
searchInfo: PersonLookupInfo(
|
|
name: name,
|
|
originalTitle: originalTitle,
|
|
year: year
|
|
)
|
|
)
|
|
let request = Paths.getPersonRemoteSearchResults(parameters)
|
|
let response = try await userSession.client.send(request)
|
|
|
|
return response.value
|
|
|
|
case .series:
|
|
let parameters = SeriesInfoRemoteSearchQuery(
|
|
itemID: itemID,
|
|
searchInfo: SeriesInfo(
|
|
name: name,
|
|
originalTitle: originalTitle,
|
|
year: year
|
|
)
|
|
)
|
|
let request = Paths.getSeriesRemoteSearchResults(parameters)
|
|
let response = try await userSession.client.send(request)
|
|
|
|
return response.value
|
|
|
|
default:
|
|
return []
|
|
}
|
|
}
|
|
|
|
// MARK: - Save Updated Item to Server
|
|
|
|
private func updateItem(_ match: RemoteSearchResult) async throws {
|
|
guard let itemID = item.id else { return }
|
|
|
|
let request = Paths.applySearchCriteria(itemID: itemID, match)
|
|
_ = try await userSession.client.send(request)
|
|
|
|
try await refreshItem()
|
|
}
|
|
|
|
// MARK: - Refresh Item
|
|
|
|
private func refreshItem() async throws {
|
|
guard let itemID = item.id else { return }
|
|
|
|
let request = Paths.getItem(userID: userSession.user.id, itemID: itemID)
|
|
let response = try await userSession.client.send(request)
|
|
|
|
await MainActor.run {
|
|
self.item = response.value
|
|
Notifications[.itemShouldRefreshMetadata].post(itemID)
|
|
}
|
|
}
|
|
}
|