Aerial/Source/Models/ManifestLoader.swift
//
// ManifestLoader.swift
// Aerial
// WARNING : This is the old deprecated stuff
//
// Created by John Coates on 10/28/15.
// Copyright © 2015 John Coates. All rights reserved.
//
import Foundation
import ScreenSaver
import GameplayKit
import AVFoundation
typealias ManifestLoadCallback = ([AerialVideo]) -> Void
// swiftlint:disable:next type_body_length
class ManifestLoader {
static let instance: ManifestLoader = ManifestLoader()
var callbacks = [ManifestLoadCallback]()
var loadedManifest = [AerialVideo]()
var processedVideos = [AerialVideo]()
var lastPluckedFromPlaylist: AerialVideo?
var customVideoFolders: CustomVideoFolders?
var manifestTvOS10: Data?
var manifestTvOS11: Data?
var manifestTvOS12: Data?
var manifestTvOS13: Data?
// Playlist management
var playlistIsRestricted = false
var playlistRestrictedTo = ""
var playlist = [AerialVideo]()
// Those videos will be ignored
let blacklist = ["b10-1.mov", // Dupe of b1-1 (Hawaii, day)
"b10-2.mov", // Dupe of b2-3 (New York, night)
"b10-4.mov", // Dupe of b2-4 (San Francisco, night)
"b9-1.mov", // Dupe of b2-2 (Hawaii, day)
"b9-2.mov", // Dupe of b3-1 (London, night)
"comp_LA_A005_C009_v05_t9_6M.mov", // Low quality version of Los Angeles day 687B36CB-BA5D-4434-BA99-2F2B8B6EC163
"comp_LA_A009_C009_t9_6M_tag0.mov"
] // Low quality version of Los Angeles night 89B1643B-06DD-4DEC-B1B0-774493B0F7B7
// This is used for videos where URLs should be merged with different ID
// This is used to dedupe old versions of videos
// old : new
let dupePairs = [
"A2BE2E4A-AD4B-428A-9C41-BDAE1E78E816": "12318CCB-3F78-43B7-A854-EFDCCE5312CD", // California to Vegas (v7 -> v8)
"6A74D52E-2447-4B84-AE45-0DEF2836C3CC": "7825C73A-658F-48EE-B14C-EC56673094AC", // China
"6C3D54AE-0871-498A-81D0-56ED24E5FE9F": "009BA758-7060-4479-8EE8-FB9B40C8FB97", // Korean and Japan night
"b5-1": "044AD56C-A107-41B2-90CC-E60CCACFBCF5", // Great Wall 3
"b2-1": "22162A9B-DB90-4517-867C-C676BC3E8E95", // Great wall 2
"b6-1": "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF", // Great wall 1
"BAF76353-3475-4855-B7E1-CE96CC9BC3A7": "9680B8EB-CE2A-4395-AF41-402801F4D6A6", // Approaching Burj Khalifa (night)
"B3BDC635-756D-4B82-B01A-A2620D1DBF10": "9680B8EB-CE2A-4395-AF41-402801F4D6A6", // Approaching Burj Khalifa (night)
"15F9B681-9EA8-4DD1-AD26-F111BC5CF64B": "E991AC0C-F272-44D8-88F3-05F44EDFE3AE", // Marina 1
"49790B7C-7D8C-466C-A09E-83E38B6BE87A": "E991AC0C-F272-44D8-88F3-05F44EDFE3AE", // Marina 1
"802866E6-4AAF-4A69-96EA-C582651391F1": "3FFA2A97-7D28-49EA-AA39-5BC9051B2745", // Marina 2
"D34A7B19-EC33-4300-B4ED-0C8BC494C035": "3FFA2A97-7D28-49EA-AA39-5BC9051B2745", // Marina 2
"02EA5DBE-3A67-4DFA-8528-12901DFD6CC1": "00BA71CD-2C54-415A-A68A-8358E677D750", // Downtown
"AC9C09DD-1D97-4013-A09F-B0F5259E64C3": "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9", // Sheikh Zayed Road (day)
"DFA399FA-620A-4517-94D6-BF78BF8C5E5A": "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9", // Sheikh Zayed Road (day)
"D388F00A-5A32-4431-A95C-38BF7FF7268D": "B8F204CE-6024-49AB-85F9-7CA2F6DCD226", // Nuusuaq Peninsula
"E4ED0B22-EB81-4D4F-A29E-7E1EA6B6D980": "B8F204CE-6024-49AB-85F9-7CA2F6DCD226", // Nuusuaq Peninsula
"30047FDA-3AE3-4E74-9575-3520AD77865B": "2F52E34C-39D4-4AB1-9025-8F7141FAA720", // Ilulissat Icefjord day
"7D4710EB-5BA4-42E6-AA60-68D77F67D9B9": "EE01F02D-1413-436C-AB05-410F224A5B7B", // Ilulissat Icefjord Night
"b8-1": "82BD33C9-B6D2-47E7-9C42-AA3B7758921A", // Pu'u O 'Umi Night
"b4-1": "258A6797-CC13-4C3A-AB35-4F25CA3BF474", // Pu'u O 'Umi day
"b1-1": "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7", // Waimanu Valley
"b7-1": "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943", // Laupāhoehoe Nui
"b6-2": "3D729CFC-9000-48D3-A052-C5BD5B7A6842", // Kohala coastline
"30313BC1-BF20-45EB-A7B1-5A6FFDBD2488": "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A", // Hong Kong Victoria Harbour night
"2A57BB93-1825-484C-9609-FF8580CAE77B": "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A", // Hong Kong Victoria Harbour night
"102C19D1-9D9F-48EC-B492-074C985C4D9F": "FE8E1F9D-59BA-4207-B626-28E34D810D0A", // Hong Kong Victoria Harbour 1
"786E674C-BB22-4AA9-9BD3-114D2020EC4D": "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA", // Hong Kong Victoria Harbour 2
"560E09E8-E89D-4ADB-8EEA-4754415383D4": "C8559883-6F3E-4AF2-8960-903710CD47B7", // Hong Kong Victoria Peak
"6E2FC8AC-832D-46CF-B306-BB2A05030C17": "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8", // Liwa oasis 1
"88025454-6D58-48E8-A2DB-924988FAD7AC": "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8", // Liwa oasis 1
"b6-3": "58754319-8709-4AB0-8674-B34F04E7FFE2", // River Thames
"b1-2": "F604AF56-EA77-4960-AEF7-82533CC1A8B3", // River Thames near sunset
"b3-1": "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97", // River Times at Dusk
"b5-2": "A5AAFF5D-8887-42BB-8AFD-867EF557ED85", // Buckingham Palace
"BEED64EC-2DB7-47E1-A67E-59C101E73C04": "CE279831-1CA7-4A83-A97B-FF1E20234396", // LAX
"829E69BA-BB53-4841-A138-4DF0C2A74236": "CE279831-1CA7-4A83-A97B-FF1E20234396", // LAX
"60CD8E2E-35CD-4192-A5A4-D5E10BFE158B": "92E48DE9-13A1-4172-B560-29B4668A87EE", // Santa Monica Beach
"B730433D-1B3B-4B99-9500-A286BF7A9940": "92E48DE9-13A1-4172-B560-29B4668A87EE", // Santa Monica Beach
"30A2A488-E708-42E7-9A90-B749A407AE1C": "35693AEA-F8C4-4A80-B77D-C94B20A68956", // Harbor Freeway
"A284F0BF-E690-4C13-92E2-4672D93E8DE5": "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9", // Downtown
"b3-2": "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8", // Upper East side
"b4-2": "640DFB00-FBB9-45DA-9444-9F663859F4BC", // Lower Manhattan (night)
"b2-3": "44166C39-8566-4ECA-BD16-43159429B52F", // Seventh Avenue
"b7-2": "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89", // Central Park
"b10-3": "EE533FBD-90AE-419A-AD13-D7A60E2015D6", // Marin Headlands in Fog
"b1-4": "3E94AE98-EAF2-4B09-96E3-452F46BC114E", // Bay bridge night
"b9-3": "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51", // Bay and Golden Bridge
"b7-3": "29BDF297-EB43-403A-8719-A78DA11A2948", // Fisherman's Wharf
"b3-3": "85CE77BF-3413-4A7B-9B0F-732E96229A73" // Embarcadero, Market Street
]
// Extra info to be merged for a given ID, as of right now only one known video
let mergeInfo = [
"2F11E857-4F77-4476-8033-4A1E4610AFCC":
["url-1080-SDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_2K_SDR_HEVC.mov",
"url-1080-HDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_2K_HDR_HEVC.mov",
"url-4K-SDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_SDR_HEVC.mov",
"url-4K-HDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_HDR_HEVC.mov" ] // Dubai night 2
]
// Extra POI
let mergePOI = [
"b6-1": "C001_C005_", // China day 4
"b2-1": "C004_C003_", // China day 5
"b5-1": "C003_C003_", // China day 6
"7D4710EB-5BA4-42E6-AA60-68D77F67D9B9": "GL_G010_C006_", // Greenland night 1
"b7-1": "H007_C003", // Hawaii day 1
"b1-1": "H005_C012_", // Hawaii day 2
"b2-2": "H010_C006_", // Hawaii day 3
"b4-1": "H004_C007_", // Hawaii day 4
"b6-2": "H012_C009_", // Hawaii night 1
"b8-1": "H004_C009_", // Hawaii night 2
"6E2FC8AC-832D-46CF-B306-BB2A05030C17": "LW_L001_C006_", // Liwa day 1 LW_L001_C006_0
"b6-3": "L010_C006_", // London day 1
"b5-2": "L007_C007_", // London day 2
"b1-2": "L012_C002_", // London night 1
"b3-1": "L004_C011_", // London night 2
"A284F0BF-E690-4C13-92E2-4672D93E8DE5": "LA_A011_C003_", // Los Angeles night 3
"b7-2": "N008_C009_", // New York day 1
"b1-3": "N006_C003_", // New York day 2
"b3-2": "N003_C006_", // New York day 3
"b2-3": "N013_C004_", // New York night 1
"b4-2": "N008_C003_", // New York night 2
"b8-2": "A008_C007_", // San Francisco day 1
// "b10-3": , // San Francisco day 2
"b9-3": "A006_C003_", // San Francisco day 3
// "b8-3":"", San Francisco day 4 (no extra poi ?)
"b3-3": "A012_C014_", // San Francisco day 5
// maybe A013_C004 ?
"b4-3": "A013_C005_", // San Francisco day 6
"b6-4": "A004_C012_", // San Francisco night 1
"b7-3": "A007_C017_", // San Francisco night 2
"b5-3": "A015_C014_", // San Francisco night 3
"b1-4": "A015_C018_", // San Francisco night 4
"b2-4": "A018_C014_" // San Francisco night 5
]
// MARK: - Playlist generation
func generatePlaylist(isRestricted: Bool, restrictedTo: String) {
// Start fresh
playlist = [AerialVideo]()
playlistIsRestricted = isRestricted
playlistRestrictedTo = restrictedTo
// Start with a shuffled list, we may have synchronized seed shuffle
var shuffled: [AerialVideo]
/*if preferences.synchronizedMode {
if #available(OSX 10.11, *) {
let date = Date()
let calendar = NSCalendar.current
let minutes = calendar.component(.minute, from: date)
debugLog("seed : \(minutes)")
var generator = SeededGenerator(seed: UInt64(minutes))
shuffled = loadedManifest.shuffled(using: &generator)
} else {
// Fallback on earlier versions
shuffled = loadedManifest.shuffled()
}
} else {
shuffled = loadedManifest.shuffled()
}*/
// Somehow code above doesn't work anymore, force disabling it for everyone for now
shuffled = loadedManifest.shuffled()
for video in shuffled {
// We exclude videos not in rotation
/*let inRotation = preferences.videoIsInRotation(videoID: video.id)
if !inRotation {
continue
}*/
// Do we restrict video types by day/night ?
if isRestricted {
if video.timeOfDay != restrictedTo {
continue
}
}
// Are we in full manual mode ?? This replace the old never stream setting
if !video.isAvailableOffline && !PrefsCache.enableManagement {
continue
}
// Is the video cached, and if not, are we full ?
if !video.isAvailableOffline && Cache.isFull() {
continue
}
// If the video isn't cached, can we network ?
if !video.isAvailableOffline && !Cache.canNetwork() {
continue
}
// All good ? Add to playlist
playlist.append(video)
}
// On regenerating a new playlist, we try to avoid repeating the last thing we played!
while playlist.count > 1 && lastPluckedFromPlaylist == playlist.first {
playlist.shuffle()
}
}
func randomVideo(excluding: [AerialVideo]) -> (AerialVideo?, Bool) {
var shouldLoop = false
let timeManagement = TimeManagement.sharedInstance
let (shouldRestrictByDayNight, restrictTo) = timeManagement.shouldRestrictPlaybackToDayNightVideo()
// We may need to regenerate a playlist!
if playlist.isEmpty || restrictTo != playlistRestrictedTo || shouldRestrictByDayNight != playlistIsRestricted {
generatePlaylist(isRestricted: shouldRestrictByDayNight, restrictedTo: restrictTo)
if playlist.count == 1 {
debugLog("playlist only has one element, looping!")
shouldLoop = true
}
}
// If not pluck one from current playlist and return that
if !playlist.isEmpty {
lastPluckedFromPlaylist = playlist.removeFirst()
return (lastPluckedFromPlaylist, shouldLoop)
} else {
// If we don't have any playlist, something's got awfully wrong so deal with that!
return (findBestEffortVideo(), shouldLoop)
}
}
// Find a backup plan when conditions are not met
func findBestEffortVideo() -> AerialVideo? {
// So this is embarassing. This can happen if :
// - No video checked
// - No video for current conditions (only day video checked, and looking for night)
// - We don't want to stream but don't have any video
// - We may not have the manifests
// At this point we're doing a best effort :
// - Did we play something previously ? If so play that back (will loop)
// - return a random one from the manifest that is cached
// - return a random video that is not cached (slight betrayal of the Never stream videos)
warnLog("Empty playlist, not good !")
if lastPluckedFromPlaylist != nil {
warnLog("Repeating last played video, after condition change not met !")
return lastPluckedFromPlaylist!
} else {
// Start with a shuffled list
let shuffled = loadedManifest.shuffled()
if shuffled.isEmpty {
// This is super bad, no manifest at all
errorLog("No manifest, nothing to play !")
return nil
}
/*for video in shuffled {
// We exclude videos not in rotation
let inRotation = preferences.videoIsInRotation(videoID: video.id)
// If we find anything cached and in rotation, we send that back
if video.isAvailableOffline && inRotation {
warnLog("returning random cached in rotation video after condition change not met !")
return video
}
}*/
// Nothing ? Sorry but you'll get a non cached file
warnLog("returning random video after condition change not met !")
return shuffled.first!
}
}
// MARK: - Lifecycle
init() {
debugLog("Manifest init")
// 2.0 remove everything here
/*
// tmp
loadCustomVideos()
// We try to load our video manifests in 3 steps :
// - reload from local variables (unused for now, maybe with previews+screensaver
// in some weird edge case on some systems)
// - reprocess the saved files in cache directory (full offline mode)
// - download the manifests from servers
//
// Starting with 1.4.6, we also may now periodically recheck for changed files!
debugLog("isManifestCached 10 \(isManifestCached(manifest: .tvOS10))")
debugLog("isManifestCached 11 \(isManifestCached(manifest: .tvOS11))")
debugLog("isManifestCached 12 \(isManifestCached(manifest: .tvOS12))")
debugLog("isManifestCached 13 \(isManifestCached(manifest: .tvOS13))")
checkIfShouldRedownloadFiles()
if areManifestsFilesLoaded() {
debugLog("Files were already loaded in memory")
loadManifestsFromLoadedFiles()
} else {
debugLog("Files were not already loaded in memory")
// Manifests are not in our preferences plist, are they cached on disk ?
if areManifestsCached() {
debugLog("Manifests are cached on disk, loading")
loadCachedManifests()
} else {
// Ok then, we fetch them...
debugLog("Fetching missing manifests online")
let dateFormatter = DateFormatter()
let current = Date()
dateFormatter.dateFormat = "yyyy-MM-dd"
preferences.lastVideoCheck = dateFormatter.string(from: current)
let downloadManager = DownloadManager()
var urls: [URL] = []
// For tvOS12-13, json is now in a tar file
if !isManifestCached(manifest: .tvOS13) || !isManifestCached(manifest: .tvOS13Strings) {
urls.append(URL(string: "https://sylvan.apple.com/Aerials/resources-13.tar")!)
}
if !isManifestCached(manifest: .tvOS12) {
urls.append(URL(string: "https://sylvan.apple.com/Aerials/resources.tar")!)
}
if !isManifestCached(manifest: .tvOS11) {
urls.append(URL(string: "https://sylvan.apple.com/Aerials/2x/entries.json")!)
}
if !isManifestCached(manifest: .tvOS10) {
urls.append(URL(string: "http://a1.phobos.apple.com/us/r1000/000/Features/atv/AutumnResources/videos/entries.json")!)
}
let completion = BlockOperation {
debugLog("Fetching manifests all done")
// We can now load from the newly cached files
self.loadCachedManifests()
}
for url in urls {
let operation = downloadManager.queueDownload(url, folder: "")
completion.addDependency(operation)
}
OperationQueue.main.addOperation(completion)
}
}*/
}
// MARK: - This will refetch the manifests online
func reloadFiles() {
moveOldManifests()
// Ok then, we fetch them...
debugLog("Fetching missing manifests online")
let dateFormatter = DateFormatter()
let current = Date()
dateFormatter.dateFormat = "yyyy-MM-dd"
PrefsVideos.lastVideoCheck = dateFormatter.string(from: current)
let downloadManager = DownloadManager()
var urls: [URL] = []
// For tvOS12, json is now in a tar file
if !isManifestCached(manifest: .tvOS13) {
urls.append(URL(string: "https://sylvan.apple.com/Aerials/resources-13.tar")!)
}
if !isManifestCached(manifest: .tvOS12) {
urls.append(URL(string: "https://sylvan.apple.com/Aerials/resources.tar")!)
}
if !isManifestCached(manifest: .tvOS11) {
urls.append(URL(string: "https://sylvan.apple.com/Aerials/2x/entries.json")!)
}
if !isManifestCached(manifest: .tvOS10) {
urls.append(URL(string: "http://a1.phobos.apple.com/us/r1000/000/Features/atv/AutumnResources/videos/entries.json")!)
}
// Setup and start async fetching
let completion = BlockOperation {
debugLog("Fetching manifests all done")
// We can now load from the newly cached files
self.loadCachedManifests()
}
for url in urls {
let operation = downloadManager.queueDownload(url, folder: "")
completion.addDependency(operation)
}
OperationQueue.main.addOperation(completion)
}
func addCallback(_ callback:@escaping ManifestLoadCallback) {
if !loadedManifest.isEmpty {
callback(loadedManifest)
} else {
callbacks.append(callback)
}
}
// MARK: - Custom videos
func loadCustomVideos() {
do {
if let cacheDirectory = VideoCache.appSupportDirectory {
// customvideos.json
var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String)
cacheFileUrl.appendPathComponent("customvideos.json")
if FileManager.default.fileExists(atPath: cacheFileUrl.path) {
debugLog("loading custom file : \(cacheFileUrl)")
let ndata = try Data(contentsOf: cacheFileUrl)
customVideoFolders = try CustomVideoFolders(data: ndata)
} else {
debugLog("No customvideos.json at : \(cacheFileUrl.path)")
}
}
} catch {
debugLog("Error loading customvideos.json : \(error)")
}
}
func saveCustomVideos() {
if let cvf = customVideoFolders, let cacheDirectory = VideoCache.appSupportDirectory {
var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String)
cacheFileUrl.appendPathComponent("customvideos.json")
do {
if let encodedData = try? cvf.jsonData() {
try encodedData.write(to: cacheFileUrl)
debugLog("customvideos.json saved successfully!")
loadedManifest.removeAll() // we remove our previously loaded manifest, it's invalid
}
} catch let error as NSError {
errorLog("customvideos.json could not be saved: \(error.localizedDescription)")
}
}
}
// This is where we merge with the processed list
func mergeCustomVideos() {
/*
if let cvf = customVideoFolders {
for folder in cvf.folders {
for asset in folder.assets {
let avResolution = getResolution(asset: AVAsset(url: URL(fileURLWithPath: asset.url)))
var url1080p = ""
var url4K = ""
if avResolution.height > 1080 {
url4K = URL(fileURLWithPath: asset.url).absoluteString
} else {
url1080p = URL(fileURLWithPath: asset.url).absoluteString
}
let urls: [VideoFormat: String] = [.v1080pH264: url1080p,
.v1080pHEVC: url1080p,
.v1080pHDR: url1080p,
.v4KHEVC: url4K,
.v4KHDR: url4K, ]
let video = AerialVideo(id: asset.id,
name: folder.label,
secondaryName: asset.accessibilityLabel,
type: "video",
timeOfDay: asset.time,
scene: "landscape",
urls: urls,
source: nil,
poi: [:],
communityPoi: asset.pointsOfInterest)
processedVideos.append(video)
}
}
}
*/
}
func getResolution(asset: AVAsset) -> CGSize {
guard let track = asset.tracks(withMediaType: AVMediaType.video).first else { return CGSize.zero }
let size = track.naturalSize.applying(track.preferredTransform)
return CGSize(width: abs(size.width), height: abs(size.height))
}
// MARK: - Periodically check for new videos
func checkIfShouldRedownloadFiles() {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.locale = Locale.init(identifier: "en_GB")
let dateObj = dateFormatter.date(from: PrefsVideos.lastVideoCheck)
debugLog(PrefsVideos.lastVideoCheck)
var dayCheck: Int
switch PrefsVideos.refreshPeriodicity {
case .weekly:
dayCheck = 7
case .monthly:
dayCheck = 30
case .never:
dayCheck = 9999
}
let cacheDirectory = VideoCache.appSupportDirectory!
var cacheResourcesString = cacheDirectory
cacheResourcesString.append(contentsOf: "/backups")
let cacheUrl = URL(fileURLWithPath: cacheResourcesString)
if #available(OSX 10.11, *) {
if !cacheUrl.hasDirectoryPath {
// If there's no backup directory, we force the first check
moveOldManifests()
return
}
} else {
// Fallback on earlier versions
}
//debugLog("Interval : \(String(describing: dateObj?.timeIntervalSinceNow))")
if Int((dateObj?.timeIntervalSinceNow)!) < -dayCheck * 86400 {
// We need to redownload then
debugLog("Checking for new videos")
moveOldManifests()
} else {
debugLog("No need to check for new videos")
}
}
// We only backup the current tvos and TVStringsBundle (tvOS13)
// Previous versions don't change
func moveOldManifests() {
debugLog("move")
let cacheDirectory = VideoCache.appSupportDirectory!
var cacheResourcesString = cacheDirectory
// Generate the backup path
let dateFormatter = DateFormatter()
let current = Date()
dateFormatter.dateFormat = "yyyy-MM-dd"
let today = dateFormatter.string(from: current)
cacheResourcesString.append(contentsOf: "/backups/"+today)
// The previous files we want to move
let previous = URL(fileURLWithPath: cacheDirectory.appending("/tvos13.json"))
let previousBnd = URL(fileURLWithPath: cacheDirectory.appending("/TVIdleScreenStrings13.bundle"))
if FileManager.default.fileExists(atPath: cacheDirectory.appending("/tvos13.json")) || FileManager.default.fileExists(atPath: cacheDirectory.appending("/TVIdleScreenStrings13.bundle")) {
let new = URL(fileURLWithPath: cacheResourcesString.appending("/tvos13.json"))
let newBnd = URL(fileURLWithPath: cacheResourcesString.appending("/TVIdleScreenStrings13.bundle"))
let cacheUrl = URL(fileURLWithPath: cacheResourcesString)
if #available(OSX 10.11, *) {
if !cacheUrl.hasDirectoryPath {
do {
debugLog("creating dir \(cacheResourcesString)")
try FileManager.default.createDirectory(atPath: cacheResourcesString, withIntermediateDirectories: true, attributes: nil)
debugLog("moving tvos13.json and TVIdleScreenStrings13.bundle")
try FileManager.default.moveItem(at: previous, to: new)
try FileManager.default.moveItem(at: previousBnd, to: newBnd)
} catch {
errorLog("\(error.localizedDescription)")
}
}
}
}
}
// MARK: - Manifests
// Check if the Manifests have been loaded in this class already
func areManifestsFilesLoaded() -> Bool {
if manifestTvOS13 != nil && manifestTvOS12 != nil && manifestTvOS11 != nil && manifestTvOS10 != nil {
debugLog("Manifests files were loaded in class")
return true
} else {
debugLog("Manifests files were not loaded in class")
return false
}
}
// Check if the Manifests are saved in our cache directory
func areManifestsCached() -> Bool {
return isManifestCached(manifest: .tvOS10) && isManifestCached(manifest: .tvOS11) && isManifestCached(manifest: .tvOS12) && isManifestCached(manifest: .tvOS13) && isManifestCached(manifest: .tvOS13Strings)
}
// Check if a Manifest is saved in our cache directory
func isManifestCached(manifest: Manifests) -> Bool {
if let cacheDirectory = VideoCache.appSupportDirectory {
let fileManager = FileManager.default
var cacheResourcesString = cacheDirectory
cacheResourcesString.append(contentsOf: "/" + manifest.rawValue)
if !fileManager.fileExists(atPath: cacheResourcesString) {
return false
}
} else {
return false
}
return true
}
// Load the JSON Data cached on disk
func loadCachedManifests() {
if let cacheDirectory = VideoCache.appSupportDirectory {
// tvOS13
var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String)
cacheFileUrl.appendPathComponent("tvos13.json")
do {
let ndata = try Data(contentsOf: cacheFileUrl)
manifestTvOS13 = ndata
} catch {
errorLog("Can't load tvos13.json from cached directory")
}
// tvOS12
cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String)
cacheFileUrl.appendPathComponent("tvos12.json")
do {
let ndata = try Data(contentsOf: cacheFileUrl)
manifestTvOS12 = ndata
} catch {
errorLog("Can't load tvos12.json from cached directory")
}
// tvOS11
cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String)
cacheFileUrl.appendPathComponent("tvos11.json")
do {
let ndata = try Data(contentsOf: cacheFileUrl)
manifestTvOS11 = ndata
} catch {
errorLog("Can't load tvos11.json from cached directory")
}
// tvOS10
cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String)
cacheFileUrl.appendPathComponent("tvos10.json")
do {
let ndata = try Data(contentsOf: cacheFileUrl)
manifestTvOS10 = ndata
} catch {
errorLog("Can't load tvos10.json from cached directory")
}
if manifestTvOS10 != nil || manifestTvOS11 != nil || manifestTvOS12 != nil || manifestTvOS13 != nil {
loadManifestsFromLoadedFiles()
} else {
// No internet, no anything, nothing to do
errorLog("No video to load, no internet connexion ?")
}
}
}
// Load Manifests from the saved preferences
func loadManifestsFromLoadedFiles() {
// Reset our array
processedVideos = []
if manifestTvOS13 != nil {
// We start with the more recent one, it has more information (poi, etc)
readJSONFromData(manifestTvOS13!, manifest: .tvOS13)
} else {
warnLog("tvOS13 manifest is absent")
}
if manifestTvOS12 != nil {
// We start with the more recent one, it has more information (poi, etc)
readJSONFromData(manifestTvOS12!, manifest: .tvOS12)
// We also need to add the missing videos
let bundlePath = Bundle(for: ManifestLoader.self).path(forResource: "missingvideos", ofType: "json")!
do {
let data = try Data(contentsOf: URL(fileURLWithPath: bundlePath), options: .mappedIfSafe)
readJSONFromData(data, manifest: .tvOS12)
} catch {
errorLog("missingvideos.json was not found in the bundle")
}
} else {
warnLog("tvOS12 manifest is absent")
}
if manifestTvOS11 != nil {
// This one has a couple videos not in the tvOS12 JSON. No H264 for these !
readJSONFromData(manifestTvOS11!, manifest: .tvOS11)
} else {
warnLog("tvOS11 manifest is absent")
}
if manifestTvOS10 != nil {
// The original manifest is in another format
readOldJSONFromData(manifestTvOS10!, manifest: .tvOS10)
} else {
warnLog("tvOS10 manifest is absent")
}
if customVideoFolders != nil {
mergeCustomVideos()
}
// We sort videos by secondary names, so they can display sorted in our view later
processedVideos = processedVideos.sorted { $0.secondaryName < $1.secondaryName }
self.loadedManifest = processedVideos
debugLog("Total videos processed : \(processedVideos.count) callbacks : \(callbacks.count)")
// callbacks
for callback in self.callbacks {
callback(self.loadedManifest)
}
self.callbacks.removeAll()
}
// MARK: - JSON
func readJSONFromData(_ data: Data, manifest: Manifests) {
/*
do {
let poiStringProvider = PoiStringProvider.sharedInstance
let options = JSONSerialization.ReadingOptions.allowFragments
let batches = try JSONSerialization.jsonObject(with: data, options: options)
guard let batch = batches as? NSDictionary else {
errorLog("Encountered unexpected content type for batch, please report !")
return
}
let assets = batch["assets"] as! [NSDictionary]
for item in assets {
let id = item["id"] as! String
let url1080pH264 = item["url-1080-H264"] as? String
let url1080pHEVC = item["url-1080-SDR"] as? String
let url1080pHDR = item["url-1080-HDR"] as? String
let url4KHEVC = item["url-4K-SDR"] as? String
let url4KHDR = item["url-4K-HDR"] as? String
let name = item["accessibilityLabel"] as! String
let urls: [VideoFormat: String] = [.v1080pH264: url1080pH264 ?? "",
.v1080pHEVC: url1080pHEVC ?? "",
.v1080pHDR: url1080pHDR ?? "",
.v4KHEVC: url4KHEVC ?? "",
.v4KHDR: url4KHDR ?? "", ]
var secondaryName = ""
// We may have a secondary name
if let mergename = poiStringProvider.getCommunityName(id: id) {
secondaryName = mergename
}
let timeOfDay = "day" // TODO, this is hardcoded as it's no longer available in the modern JSONs
let type = "video"
var poi: [String: String]?
if let mergeId = mergePOI[id] {
poi = poiStringProvider.fetchExtraPoiForId(id: mergeId)
} else {
poi = item["pointsOfInterest"] as? [String: String]
}
let communityPoi = poiStringProvider.getCommunityPoi(id: id)
let (isDupe, foundDupe) = findDuplicate(id: id, url1080pH264: url1080pH264 ?? "")
if isDupe {
//foundDupe!.sources.append(manifest)
} else {
let video = AerialVideo(id: id, // Must have
name: name, // Must have
secondaryName: secondaryName, // Optional
type: type, // Not sure the point of this one ?
timeOfDay: timeOfDay,
scene: "landscape",
urls: urls,
source: nil,
poi: poi ?? [:],
communityPoi: communityPoi)
processedVideos.append(video)
}
}
} catch {
errorLog("Error retrieving content listing (new)")
return
}*/
}
func readOldJSONFromData(_ data: Data, manifest: Manifests) {
/*
do {
let poiStringProvider = PoiStringProvider.sharedInstance
let options = JSONSerialization.ReadingOptions.allowFragments
let batches = try JSONSerialization.jsonObject(with: data,
options: options) as! [NSDictionary]
for batch: NSDictionary in batches {
let assets = batch["assets"] as! [NSDictionary]
for item in assets {
let url = item["url"] as! String
let name = item["accessibilityLabel"] as! String
let timeOfDay = item["timeOfDay"] as! String
let id = item["id"] as! String
let type = item["type"] as! String
if type != "video" {
continue
}
// We may have a secondary name
var secondaryName = ""
if let mergename = poiStringProvider.getCommunityName(id: id) {
secondaryName = mergename
}
// We may have POIs to merge
var poi: [String: String]?
if let mergeId = mergePOI[id] {
let poiStringProvider = PoiStringProvider.sharedInstance
poi = poiStringProvider.fetchExtraPoiForId(id: mergeId)
}
let communityPoi = poiStringProvider.getCommunityPoi(id: id)
// We may have dupes...
let (isDupe, foundDupe) = findDuplicate(id: id, url1080pH264: url)
if isDupe {
if foundDupe != nil {
//foundDupe!.sources.append(manifest)
if foundDupe?.urls[.v1080pH264] == "" {
foundDupe?.urls[.v1080pH264] = url
}
}
} else {
var url1080pHEVC = ""
var url1080pHDR = ""
var url4KHEVC = ""
var url4KHDR = ""
// Check if we have some HEVC urls to merge
if let val = mergeInfo[id] {
url1080pHEVC = val["url-1080-SDR"]!
url1080pHDR = val["url-1080-HDR"]!
url4KHEVC = val["url-4K-SDR"]!
url4KHDR = val["url-4K-HDR"]!
}
let urls: [VideoFormat: String] = [.v1080pH264: url,
.v1080pHEVC: url1080pHEVC,
.v1080pHDR: url1080pHDR,
.v4KHEVC: url4KHEVC,
.v4KHDR: url4KHDR, ]
// Now we can finally add...
let video = AerialVideo(id: id, // Must have
name: name, // Must have
secondaryName: secondaryName,
type: type, // Not sure the point of this one ?
timeOfDay: timeOfDay,
scene: "landscape",
urls: urls,
source: Source(),
poi: poi ?? [:],
communityPoi: communityPoi)
processedVideos.append(video)
}
}
}
} catch {
errorLog("Error retrieving content listing (old)")
return
}*/
}
// Look for a previously processed similar video
//
// tvOS11 and 12 JSON are using the same ID (and tvOS12 JSON always has better data,
// so no need for a fancy merge)
//
// tvOS10 however JSON DOES NOT use the same ID, so we need to dupecheck on the h264
// (only available format there) filename (they actually have different URLs !)
func findDuplicate(id: String, url1080pH264: String) -> (Bool, AerialVideo?) {
// We blacklist some duplicates
if url1080pH264 != "" {
if blacklist.contains((URL(string: url1080pH264)?.lastPathComponent)!) {
return (true, nil)
}
}
// We also have a Dictionary of duplicates that need source merging
for (pid, replace) in dupePairs where id == pid {
for vid in processedVideos where vid.id == replace {
// Found dupe pair
return (true, vid)
}
}
for video in processedVideos {
if id == video.id {
return (true, video)
} else if url1080pH264 != "" && video.urls[.v1080pH264] != "" {
if URL(string: url1080pH264)?.lastPathComponent == URL(string: video.urls[.v1080pH264]!)?.lastPathComponent {
return (true, video)
}
}
}
return (false, nil)
}
// MARK: - Old video management
// Try to estimate how many old (unlinked) files we have
func getOldFilesEstimation() -> (String, Int) {
// loadedManifests contains the full deduplicated list of videos
debugLog("Looking for outdated files")
if loadedManifest.isEmpty {
warnLog("We have no videos in the manifest")
return ("Can't estimate duplicates", 0)
}
guard let cacheDirectory = VideoCache.appSupportDirectory else {
warnLog("No cache directory")
return ("Can't estimate duplicates", 0)
}
var foundOldFiles = 0
let cacheDirectoryUrl = URL(fileURLWithPath: cacheDirectory as String)
let fileManager = FileManager.default
do {
let directoryContent = try fileManager.contentsOfDirectory(at: cacheDirectoryUrl, includingPropertiesForKeys: nil)
let videoFileURLs = directoryContent.filter { $0.pathExtension == "mov" }
// We check all formats we may have
for fileURL in videoFileURLs {
var found = false
for video in loadedManifest {
for format in VideoFormat.allCases where video.urls[format] != "" {
if fileURL.lastPathComponent == URL(string: video.urls[format]!)?.lastPathComponent {
found = true
break
}
}
}
if !found {
debugLog("\(fileURL.lastPathComponent) NOT FOUND in manifest")
foundOldFiles += 1
}
}
} catch {
errorLog("Error while enumerating files \(cacheDirectoryUrl.path): \(error.localizedDescription)")
}
if foundOldFiles == 0 {
debugLog("No old files found")
return ("No old files found", 0)
}
debugLog("\(foundOldFiles) old files found")
return ("\(foundOldFiles) old files found", foundOldFiles)
}
/*
func moveOldVideos() {
debugLog("move old videos")
let cacheDirectory = VideoCache.appSupportDirectory!
var cacheResourcesString = cacheDirectory
let dateFormatter = DateFormatter()
let current = Date()
dateFormatter.dateFormat = "yyyy-MM-dd"
let today = dateFormatter.string(from: current)
cacheResourcesString.append(contentsOf: "/oldvideos/"+today)
let cacheUrl = URL(fileURLWithPath: cacheResourcesString)
if #available(OSX 10.11, *) {
if !cacheUrl.hasDirectoryPath {
do {
try FileManager.default.createDirectory(atPath: cacheResourcesString, withIntermediateDirectories: true, attributes: nil)
debugLog("creating dir \(cacheResourcesString)")
} catch {
errorLog("\(error.localizedDescription)")
}
}
}
if loadedManifest.isEmpty {
warnLog("We have no videos in the manifest")
return
}
let cacheDirectoryUrl = URL(fileURLWithPath: cacheDirectory as String)
let fileManager = FileManager.default
do {
let directoryContent = try fileManager.contentsOfDirectory(at: cacheDirectoryUrl, includingPropertiesForKeys: nil)
let videoFileURLs = directoryContent.filter { $0.pathExtension == "mov" }
// We check the 5 fields
for fileURL in videoFileURLs {
var found = false
for video in loadedManifest {
for format in VideoFormat.allCases where video.urls[format] != "" {
if fileURL.lastPathComponent == URL(string: video.urls[format]!)?.lastPathComponent {
found = true
break
}
}
}
if !found {
do {
debugLog("moving \(fileURL.lastPathComponent)")
let new = URL(fileURLWithPath: cacheResourcesString.appending("/\(fileURL.lastPathComponent)"))
try FileManager.default.moveItem(at: fileURL, to: new)
} catch {
errorLog("\(error.localizedDescription)")
}
}
}
} catch {
errorLog("Error while enumerating files \(cacheDirectoryUrl.path): \(error.localizedDescription)")
}
}
func trashOldVideos() {
debugLog("trash old videos")
let cacheDirectory = VideoCache.appSupportDirectory!
var cacheResourcesString = cacheDirectory
let dateFormatter = DateFormatter()
let current = Date()
dateFormatter.dateFormat = "yyyy-MM-dd"
let today = dateFormatter.string(from: current)
cacheResourcesString.append(contentsOf: "/oldvideos/"+today)
let cacheUrl = URL(fileURLWithPath: cacheResourcesString)
if #available(OSX 10.11, *) {
if !cacheUrl.hasDirectoryPath {
do {
try FileManager.default.createDirectory(atPath: cacheResourcesString, withIntermediateDirectories: true, attributes: nil)
debugLog("creating dir \(cacheResourcesString)")
} catch {
errorLog("\(error.localizedDescription)")
}
}
}
if loadedManifest.isEmpty {
warnLog("We have no videos in the manifest")
return
}
let cacheDirectoryUrl = URL(fileURLWithPath: cacheDirectory as String)
let fileManager = FileManager.default
do {
let directoryContent = try fileManager.contentsOfDirectory(at: cacheDirectoryUrl, includingPropertiesForKeys: nil)
let videoFileURLs = directoryContent.filter { $0.pathExtension == "mov" }
// We check the 5 fields
for fileURL in videoFileURLs {
var found = false
for video in loadedManifest {
for format in VideoFormat.allCases where video.urls[format] != "" {
if fileURL.lastPathComponent == URL(string: video.urls[format]!)?.lastPathComponent {
found = true
break
}
}
}
if !found {
debugLog("trashing \(fileURL.lastPathComponent)")
NSWorkspace.shared.recycle([fileURL]) { trashedFiles, error in
for file in [fileURL] where trashedFiles[file] == nil {
errorLog("\(file.relativePath) could not be moved to trash \(error!.localizedDescription)")
}
}
}
}
} catch {
errorLog("Error while enumerating files \(cacheDirectoryUrl.path): \(error.localizedDescription)")
}
}*/
} // swiftlint:disable:this file_length