jellyflood/Shared/ViewModels/ProgramsViewModel.swift

174 lines
4.9 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) 2025 Jellyfin & Jellyfin Contributors
//
import Combine
import Foundation
import JellyfinAPI
final class ProgramsViewModel: ViewModel, Stateful {
enum ProgramSection: CaseIterable {
case kids
case movies
case news
case recommended
case series
case sports
}
// MARK: Action
enum Action: Equatable {
case error(JellyfinAPIError)
case refresh
}
// MARK: State
enum State: Hashable {
case content
case error(JellyfinAPIError)
case initial
case refreshing
}
@Published
private(set) var kids: [BaseItemDto] = []
@Published
private(set) var movies: [BaseItemDto] = []
@Published
private(set) var news: [BaseItemDto] = []
@Published
private(set) var recommended: [BaseItemDto] = []
@Published
private(set) var series: [BaseItemDto] = []
@Published
private(set) var sports: [BaseItemDto] = []
@Published
var state: State = .initial
private var currentRefreshTask: AnyCancellable?
var hasNoResults: Bool {
[
kids,
movies,
news,
recommended,
series,
sports,
].allSatisfy(\.isEmpty)
}
func respond(to action: Action) -> State {
switch action {
case let .error(error):
return .error(error)
case .refresh:
currentRefreshTask?.cancel()
currentRefreshTask = Task { [weak self] in
guard let self else { return }
do {
let sections = try await getItemSections()
guard !Task.isCancelled else { return }
await MainActor.run {
self.kids = sections[.kids] ?? []
self.movies = sections[.movies] ?? []
self.news = sections[.news] ?? []
self.recommended = sections[.recommended] ?? []
self.series = sections[.series] ?? []
self.sports = sections[.sports] ?? []
self.state = .content
}
} catch {
guard !Task.isCancelled else { return }
await MainActor.run {
self.send(.error(.init(error.localizedDescription)))
}
}
}
.asAnyCancellable()
return .refreshing
}
}
private func getItemSections() async throws -> [ProgramSection: [BaseItemDto]] {
try await withThrowingTaskGroup(
of: (ProgramSection, [BaseItemDto]).self,
returning: [ProgramSection: [BaseItemDto]].self
) { group in
// sections
for section in ProgramSection.allCases {
group.addTask {
let items = try await self.getPrograms(for: section)
return (section, items)
}
}
// recommended
group.addTask {
let items = try await self.getRecommendedPrograms()
return (ProgramSection.recommended, items)
}
var programs: [ProgramSection: [BaseItemDto]] = [:]
while let items = try await group.next() {
programs[items.0] = items.1
}
return programs
}
}
private func getRecommendedPrograms() async throws -> [BaseItemDto] {
var parameters = Paths.GetRecommendedProgramsParameters()
parameters.fields = .MinimumFields
.appending(.channelInfo)
parameters.isAiring = true
parameters.limit = 20
parameters.userID = userSession.user.id
let request = Paths.getRecommendedPrograms(parameters: parameters)
let response = try await userSession.client.send(request)
return response.value.items ?? []
}
private func getPrograms(for section: ProgramSection) async throws -> [BaseItemDto] {
var parameters = Paths.GetLiveTvProgramsParameters()
parameters.fields = .MinimumFields
.appending(.channelInfo)
parameters.hasAired = false
parameters.limit = 20
parameters.userID = userSession.user.id
parameters.isKids = section == .kids
parameters.isMovie = section == .movies
parameters.isNews = section == .news
parameters.isSeries = section == .series
parameters.isSports = section == .sports
let request = Paths.getLiveTvPrograms(parameters: parameters)
let response = try await userSession.client.send(request)
return response.value.items ?? []
}
}