379 lines
11 KiB
Swift
379 lines
11 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
|
|
}
|
|
}
|