jellyflood/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift

393 lines
12 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
// 001fC^ = dark grey plain blurhash
public extension BaseItemDto {
// MARK: Images
func getSeriesBackdropImageBlurHash() -> String {
let imgURL = getSeriesBackdropImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.backdrop?[imgTag]
else {
return "001fC^"
}
return hash
}
func getSeriesPrimaryImageBlurHash() -> String {
let imgURL = getSeriesPrimaryImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.primary?[imgTag]
else {
return "001fC^"
}
return hash
}
func getPrimaryImageBlurHash() -> String {
let imgURL = getPrimaryImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.primary?[imgTag]
else {
return "001fC^"
}
return hash
}
func getBackdropImageBlurHash() -> String {
let imgURL = getBackdropImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"] else {
return "001fC^"
}
if imgURL.queryParameters?[ImageType.backdrop.rawValue] == nil {
if itemType == .episode {
return imageBlurHashes?.backdrop?.values.first ?? "001fC^"
} else {
return imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"
}
} else {
return imageBlurHashes?.primary?[imgTag] ?? "001fC^"
}
}
func getBackdropImage(maxWidth: Int) -> URL {
var imageType = ImageType.backdrop
var imageTag: String?
var imageItemId = id ?? ""
if primaryImageAspectRatio ?? 0.0 < 1.0 {
if !(backdropImageTags?.isEmpty ?? true) {
imageTag = backdropImageTags?.first
}
} else {
imageType = .primary
imageTag = imageTags?[ImageType.primary.rawValue] ?? ""
}
if imageTag == nil || imageItemId.isEmpty {
if !(parentBackdropImageTags?.isEmpty ?? true) {
imageTag = parentBackdropImageTags?.first
imageItemId = parentBackdropItemId ?? ""
}
}
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(
itemId: imageItemId,
imageType: imageType,
maxWidth: Int(x),
quality: 96,
tag: imageTag
).URLString
return URL(string: urlString)!
}
func getThumbImage(maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(
itemId: id ?? "",
imageType: .thumb,
maxWidth: Int(x),
quality: 96
).URLString
return URL(string: urlString)!
}
func getEpisodeLocator() -> String? {
if let seasonNo = parentIndexNumber, let episodeNo = indexNumber {
return L10n.seasonAndEpisode(String(seasonNo), String(episodeNo))
}
return nil
}
func getSeriesBackdropImage(maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(
itemId: parentBackdropItemId ?? "",
imageType: .backdrop,
maxWidth: Int(x),
quality: 96,
tag: parentBackdropImageTags?.first
).URLString
return URL(string: urlString)!
}
func getSeriesPrimaryImage(maxWidth: Int) -> URL {
guard let seriesId = seriesId else {
return getPrimaryImage(maxWidth: maxWidth)
}
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(
itemId: seriesId,
imageType: .primary,
maxWidth: Int(x),
quality: 96,
tag: seriesPrimaryImageTag
).URLString
return URL(string: urlString)!
}
func getSeriesThumbImage(maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(
itemId: seriesId ?? "",
imageType: .thumb,
maxWidth: Int(x),
quality: 96,
tag: seriesPrimaryImageTag
).URLString
return URL(string: urlString)!
}
func getPrimaryImage(maxWidth: Int) -> URL {
let imageType = ImageType.primary
var imageTag = imageTags?[ImageType.primary.rawValue] ?? ""
var imageItemId = id ?? ""
if imageTag.isEmpty || imageItemId.isEmpty {
imageTag = seriesPrimaryImageTag ?? ""
imageItemId = seriesId ?? ""
}
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(
itemId: imageItemId,
imageType: imageType,
maxWidth: Int(x),
quality: 96,
tag: imageTag
).URLString
return URL(string: urlString)!
}
// 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
}
// MARK: ItemType
enum ItemType: String {
case movie = "Movie"
case season = "Season"
case episode = "Episode"
case series = "Series"
case boxset = "BoxSet"
case collectionFolder = "CollectionFolder"
case folder = "Folder"
case liveTV = "LiveTV"
case unknown
var showDetails: Bool {
switch self {
case .season, .series:
return false
default:
return true
}
}
public init?(rawValue: String) {
let lowerCase = rawValue.lowercased()
switch lowerCase {
case "movie": self = .movie
case "season": self = .season
case "episode": self = .episode
case "series": self = .series
case "boxset": self = .boxset
case "collectionfolder": self = .collectionFolder
case "folder": self = .folder
case "livetv": self = .liveTV
default: self = .unknown
}
}
}
var itemType: ItemType {
guard let originalType = type, let knownType = ItemType(rawValue: originalType.rawValue) else { return .unknown }
return knownType
}
// MARK: PortraitHeaderViewURL
func portraitHeaderViewURL(maxWidth: Int) -> URL {
switch itemType {
case .movie, .season, .series, .boxset, .collectionFolder, .folder, .liveTV:
return getPrimaryImage(maxWidth: maxWidth)
case .episode:
return getSeriesPrimaryImage(maxWidth: maxWidth)
case .unknown:
return getPrimaryImage(maxWidth: maxWidth)
}
}
// 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: ", ")
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: ", ")
mediaItems.append(ItemDetail(title: L10n.subtitles, content: subtitleList))
}
}
return mediaItems
}
// 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)
}
// 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
}
}