250 lines
7.2 KiB
Swift
250 lines
7.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) 2022 Jellyfin & Jellyfin Contributors
|
|
//
|
|
|
|
import Foundation
|
|
import JellyfinAPI
|
|
import UIKit
|
|
|
|
extension BaseItemDto: Identifiable {}
|
|
|
|
extension BaseItemDto {
|
|
|
|
var episodeLocator: String? {
|
|
guard let episodeNo = indexNumber else { return nil }
|
|
return L10n.episodeNumber(episodeNo)
|
|
}
|
|
|
|
var seasonEpisodeLocator: String? {
|
|
if let seasonNo = parentIndexNumber, let episodeNo = indexNumber {
|
|
return L10n.seasonAndEpisode(String(seasonNo), String(episodeNo))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MARK: Calculations
|
|
|
|
func getItemRuntime() -> String? {
|
|
let timeHMSFormatter: DateComponentsFormatter = {
|
|
let formatter = DateComponentsFormatter()
|
|
formatter.unitsStyle = .abbreviated
|
|
formatter.allowedUnits = [.hour, .minute]
|
|
return formatter
|
|
}()
|
|
|
|
guard let runTimeTicks = runTimeTicks,
|
|
let text = timeHMSFormatter.string(from: Double(runTimeTicks / 10_000_000)) else { return nil }
|
|
|
|
return text
|
|
}
|
|
|
|
func getItemProgressString() -> String? {
|
|
if userData?.playbackPositionTicks == nil || userData?.playbackPositionTicks == 0 {
|
|
return nil
|
|
}
|
|
|
|
let remainingSecs = ((runTimeTicks ?? 0) - (userData?.playbackPositionTicks ?? 0)) / 10_000_000
|
|
let proghours = Int(remainingSecs / 3600)
|
|
let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60)
|
|
if proghours != 0 {
|
|
return "\(proghours)h \(String(progminutes).leftPad(toWidth: 2, withString: "0"))m"
|
|
} else {
|
|
return "\(String(progminutes))m"
|
|
}
|
|
}
|
|
|
|
func getLiveStartTimeString(formatter: DateFormatter) -> String {
|
|
if let startDate = self.startDate {
|
|
return formatter.string(from: startDate)
|
|
}
|
|
return " "
|
|
}
|
|
|
|
func getLiveEndTimeString(formatter: DateFormatter) -> String {
|
|
if let endDate = self.endDate {
|
|
return formatter.string(from: endDate)
|
|
}
|
|
return " "
|
|
}
|
|
|
|
func getLiveProgressPercentage() -> Double {
|
|
if let startDate = self.startDate,
|
|
let endDate = self.endDate
|
|
{
|
|
let start = startDate.timeIntervalSinceReferenceDate
|
|
let end = endDate.timeIntervalSinceReferenceDate
|
|
let now = Date().timeIntervalSinceReferenceDate
|
|
let length = end - start
|
|
let progress = now - start
|
|
return progress / length
|
|
}
|
|
return 0
|
|
}
|
|
|
|
var displayName: String {
|
|
name ?? "--"
|
|
}
|
|
|
|
// MARK: ItemDetail
|
|
|
|
struct ItemDetail {
|
|
let title: String
|
|
let content: String
|
|
}
|
|
|
|
func createInformationItems() -> [ItemDetail] {
|
|
var informationItems: [ItemDetail] = []
|
|
|
|
if let productionYear = productionYear {
|
|
informationItems.append(ItemDetail(title: L10n.released, content: "\(productionYear)"))
|
|
}
|
|
|
|
if let rating = officialRating {
|
|
informationItems.append(ItemDetail(title: L10n.rated, content: "\(rating)"))
|
|
}
|
|
|
|
if let runtime = getItemRuntime() {
|
|
informationItems.append(ItemDetail(title: L10n.runtime, content: runtime))
|
|
}
|
|
|
|
return informationItems
|
|
}
|
|
|
|
func createMediaItems() -> [ItemDetail] {
|
|
var mediaItems: [ItemDetail] = []
|
|
|
|
if let mediaStreams = mediaStreams {
|
|
let audioStreams = mediaStreams.filter { $0.type == .audio }
|
|
let subtitleStreams = mediaStreams.filter { $0.type == .subtitle }
|
|
|
|
if !audioStreams.isEmpty {
|
|
let audioList = audioStreams.compactMap { "\($0.displayTitle ?? L10n.noTitle) (\($0.codec ?? L10n.noCodec))" }
|
|
.joined(separator: "\n")
|
|
mediaItems.append(ItemDetail(title: L10n.audio, content: audioList))
|
|
}
|
|
|
|
if !subtitleStreams.isEmpty {
|
|
let subtitleList = subtitleStreams.compactMap { "\($0.displayTitle ?? L10n.noTitle) (\($0.codec ?? L10n.noCodec))" }
|
|
.joined(separator: "\n")
|
|
mediaItems.append(ItemDetail(title: L10n.subtitles, content: subtitleList))
|
|
}
|
|
}
|
|
|
|
return mediaItems
|
|
}
|
|
|
|
var subtitleStreams: [MediaStream] {
|
|
mediaStreams?.filter { $0.type == .subtitle } ?? []
|
|
}
|
|
|
|
var audioStreams: [MediaStream] {
|
|
mediaStreams?.filter { $0.type == .audio } ?? []
|
|
}
|
|
|
|
// MARK: Missing and Unaired
|
|
|
|
var missing: Bool {
|
|
locationType == .virtual
|
|
}
|
|
|
|
var unaired: Bool {
|
|
if let premierDate = premiereDate {
|
|
return premierDate > Date()
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var airDateLabel: String? {
|
|
guard let premiereDateFormatted = premiereDateFormatted else { return nil }
|
|
return L10n.airWithDate(premiereDateFormatted)
|
|
}
|
|
|
|
var premiereDateFormatted: String? {
|
|
guard let premiereDate = premiereDate else { return nil }
|
|
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.dateStyle = .medium
|
|
return dateFormatter.string(from: premiereDate)
|
|
}
|
|
|
|
var premiereDateYear: String? {
|
|
guard let premiereDate = premiereDate else { return nil }
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.dateFormat = "YYYY"
|
|
return dateFormatter.string(from: premiereDate)
|
|
}
|
|
|
|
// MARK: Chapter Images
|
|
|
|
func getChapterImage(maxWidth: Int) -> [URL] {
|
|
guard let chapters = chapters, !chapters.isEmpty else { return [] }
|
|
|
|
var chapterImageURLs: [URL] = []
|
|
|
|
for chapterIndex in 0 ..< chapters.count {
|
|
let urlString = ImageAPI.getItemImageWithRequestBuilder(
|
|
itemId: id ?? "",
|
|
imageType: .chapter,
|
|
maxWidth: maxWidth,
|
|
imageIndex: chapterIndex
|
|
).URLString
|
|
chapterImageURLs.append(URL(string: urlString)!)
|
|
}
|
|
|
|
return chapterImageURLs
|
|
}
|
|
|
|
// TODO: Don't use spoof objects as a placeholder or no results
|
|
|
|
static var placeHolder: BaseItemDto {
|
|
.init(
|
|
name: "Placeholder",
|
|
id: "1",
|
|
overview: String(repeating: "a", count: 100),
|
|
indexNumber: 20
|
|
)
|
|
}
|
|
|
|
static var noResults: BaseItemDto {
|
|
.init(name: L10n.noResults)
|
|
}
|
|
}
|
|
|
|
extension BaseItemDtoImageBlurHashes {
|
|
subscript(imageType: ImageType) -> [String: String]? {
|
|
switch imageType {
|
|
case .primary:
|
|
return primary
|
|
case .art:
|
|
return art
|
|
case .backdrop:
|
|
return backdrop
|
|
case .banner:
|
|
return banner
|
|
case .logo:
|
|
return logo
|
|
case .thumb:
|
|
return thumb
|
|
case .disc:
|
|
return disc
|
|
case .box:
|
|
return box
|
|
case .screenshot:
|
|
return screenshot
|
|
case .menu:
|
|
return menu
|
|
case .chapter:
|
|
return chapter
|
|
case .boxRear:
|
|
return boxRear
|
|
case .profile:
|
|
return profile
|
|
}
|
|
}
|
|
}
|