// // 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) 2023 Jellyfin & Jellyfin Contributors // import Factory import Foundation import JellyfinAPI struct LiveTVChannelProgram: Hashable { let id = UUID() let channel: BaseItemDto let currentProgram: BaseItemDto? let programs: [BaseItemDto] } final class LiveTVChannelsViewModel: ViewModel { @Published var channels: [BaseItemDto] = [] @Published var channelPrograms: [LiveTVChannelProgram] = [] // @Published // var channelPrograms = [LiveTVChannelProgram]() { // didSet { // rows = [] // let rowChannels = channelPrograms.chunked(into: 4) // for (index, rowChans) in rowChannels.enumerated() { // rows.append(LiveTVChannelRow(section: index, items: rowChans.map { LiveTVChannelRowCell(item: $0) })) // } // } // } // @Published // var rows = [LiveTVChannelRow]() private var programs = [BaseItemDto]() private var channelProgramsList = [BaseItemDto: [BaseItemDto]]() private var timer: Timer? var timeFormatter: DateFormatter { let df = DateFormatter() df.dateFormat = "h:mm" return df } override init() { super.init() getChannels() startScheduleCheckTimer() } deinit { stopScheduleCheckTimer() } private func getGuideInfo() { Task { let request = Paths.getGuideInfo guard let _ = try? await userSession.client.send(request) else { return } await MainActor.run { self.getChannels() } } } func getChannels() { Task { let parameters = Paths.GetLiveTvChannelsParameters( userID: userSession.user.id, startIndex: 0, limit: 100, enableImageTypes: [.primary], fields: ItemFields.minimumCases, enableUserData: false, enableFavoriteSorting: true ) let request = Paths.getLiveTvChannels(parameters: parameters) guard let response = try? await userSession.client.send(request) else { return } await MainActor.run { self.channels = response.value.items ?? [] self.getPrograms() } } } private func getPrograms() { guard !channels.isEmpty else { logger.debug("Cannot get programs, channels list empty.") return } let channelIds = channels.compactMap(\.id) let minEndDate = Date.now.addComponentsToDate(hours: -1) let maxStartDate = minEndDate.addComponentsToDate(hours: 6) Task { let parameters = Paths.GetLiveTvProgramsParameters( channelIDs: channelIds, userID: userSession.user.id, maxStartDate: maxStartDate, minEndDate: minEndDate, sortBy: ["StartDate"] ) let request = Paths.getLiveTvPrograms(parameters: parameters) do { let response = try await userSession.client.send(request) await MainActor.run { self.programs = response.value.items ?? [] self.channelPrograms = self.processChannelPrograms() } } catch { print(error.localizedDescription) } } } private func processChannelPrograms() -> [LiveTVChannelProgram] { var channelPrograms = [LiveTVChannelProgram]() let now = Date() for channel in self.channels { let prgs = self.programs.filter { item in item.channelID == channel.id } DispatchQueue.main.async { self.channelProgramsList[channel] = prgs } var currentPrg: BaseItemDto? for prg in prgs { if let startDate = prg.startDate, let endDate = prg.endDate, now.timeIntervalSinceReferenceDate > startDate.timeIntervalSinceReferenceDate && now.timeIntervalSinceReferenceDate < endDate.timeIntervalSinceReferenceDate { currentPrg = prg } } channelPrograms.append(LiveTVChannelProgram(channel: channel, currentProgram: currentPrg, programs: prgs)) } return channelPrograms } func startScheduleCheckTimer() { let date = Date() let calendar = Calendar.current var components = calendar.dateComponents([.era, .year, .month, .day, .hour, .minute], from: date) // Run on 10th min of every hour guard let minute = components.minute else { return } components.second = 0 components.minute = minute + (10 - (minute % 10)) guard let nextMinute = calendar.date(from: components) else { return } if let existingTimer = timer { existingTimer.invalidate() } timer = Timer(fire: nextMinute, interval: 60 * 10, repeats: true) { [weak self] _ in guard let self = self else { return } self.logger.debug("LiveTVChannels schedule check...") DispatchQueue.global(qos: .background).async { let newChanPrgs = self.processChannelPrograms() DispatchQueue.main.async { self.channelPrograms = newChanPrgs } } } if let timer = timer { RunLoop.main.add(timer, forMode: .default) } } func stopScheduleCheckTimer() { timer?.invalidate() } } extension Array { func chunked(into size: Int) -> [[Element]] { stride(from: 0, to: count, by: size).map { Array(self[$0 ..< Swift.min($0 + size, count)]) } } } extension Date { func addComponentsToDate(seconds sec: Int? = nil, minutes min: Int? = nil, hours hrs: Int? = nil, days d: Int? = nil) -> Date { var dc = DateComponents() if let sec = sec { dc.second = sec } if let min = min { dc.minute = min } if let hrs = hrs { dc.hour = hrs } if let d = d { dc.day = d } return Calendar.current.date(byAdding: dc, to: self)! } func midnightUTCDate() -> Date { var dc: DateComponents = Calendar.current.dateComponents([.year, .month, .day], from: self) dc.hour = 0 dc.minute = 0 dc.second = 0 dc.nanosecond = 0 dc.timeZone = TimeZone(secondsFromGMT: 0) return Calendar.current.date(from: dc)! } }