mirror of
https://github.com/tappollo/WWDC.git
synced 2026-01-12 22:45:32 +08:00
438 lines
16 KiB
Swift
438 lines
16 KiB
Swift
//
|
|
// Storage.swift
|
|
// WWDC
|
|
//
|
|
// Created by Guilherme Rambo on 17/03/17.
|
|
// Copyright © 2017 Guilherme Rambo. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import RealmSwift
|
|
import RxSwift
|
|
import RxRealm
|
|
|
|
public final class Storage {
|
|
|
|
public let realmConfig: Realm.Configuration
|
|
public let realm: Realm
|
|
|
|
public init(_ configuration: Realm.Configuration) throws {
|
|
var config = configuration
|
|
config.migrationBlock = Storage.migrate(migration:oldVersion:)
|
|
|
|
self.realmConfig = config
|
|
self.realm = try Realm(configuration: config)
|
|
|
|
DistributedNotificationCenter.default().addObserver(forName: .TranscriptIndexingDidStart, object: nil, queue: OperationQueue.main) { [unowned self] _ in
|
|
#if DEBUG
|
|
NSLog("[Storage] Locking realm autoupdates until transcript indexing is finished")
|
|
#endif
|
|
|
|
self.realm.autorefresh = false
|
|
}
|
|
|
|
DistributedNotificationCenter.default().addObserver(forName: .TranscriptIndexingDidStop, object: nil, queue: OperationQueue.main) { [unowned self] _ in
|
|
#if DEBUG
|
|
NSLog("[Storage] Realm autoupdates unlocked")
|
|
#endif
|
|
|
|
self.realm.autorefresh = true
|
|
}
|
|
|
|
deleteOldEventsIfNeeded()
|
|
}
|
|
|
|
private func deleteOldEventsIfNeeded() {
|
|
guard let wwdc2012 = realm.objects(Event.self).filter("identifier == %@", "wwdc2012").first else { return }
|
|
|
|
do {
|
|
try realm.write {
|
|
realm.delete(wwdc2012.sessions)
|
|
realm.delete(wwdc2012)
|
|
}
|
|
} catch {
|
|
NSLog("Error deleting old events: \(error)")
|
|
}
|
|
}
|
|
|
|
internal static func migrate(migration: Migration, oldVersion: UInt64) {
|
|
if oldVersion < 10 {
|
|
// alpha cleanup
|
|
migration.deleteData(forType: "Event")
|
|
migration.deleteData(forType: "Track")
|
|
migration.deleteData(forType: "Room")
|
|
migration.deleteData(forType: "Favorite")
|
|
migration.deleteData(forType: "SessionProgress")
|
|
migration.deleteData(forType: "Session")
|
|
migration.deleteData(forType: "SessionInstance")
|
|
migration.deleteData(forType: "SessionAsset")
|
|
migration.deleteData(forType: "SessionAsset")
|
|
}
|
|
if oldVersion < 15 {
|
|
// download model removal
|
|
migration.deleteData(forType: "Download")
|
|
}
|
|
if oldVersion < 31 {
|
|
// remove cached images which might have generic session thumbs instead of the correct ones
|
|
migration.deleteData(forType: "ImageCacheEntity")
|
|
|
|
// delete live stream assets (some of them got duplicated during the week)
|
|
migration.enumerateObjects(ofType: "SessionAsset") { asset, _ in
|
|
guard let asset = asset else { return }
|
|
|
|
if asset["rawAssetType"] as? String == SessionAssetType.liveStreamVideo.rawValue {
|
|
migration.delete(asset)
|
|
}
|
|
}
|
|
}
|
|
if oldVersion < 32 {
|
|
migration.deleteData(forType: "Event")
|
|
migration.deleteData(forType: "Track")
|
|
migration.deleteData(forType: "ScheduleSection")
|
|
}
|
|
if oldVersion < 34 {
|
|
migration.deleteData(forType: "Transcript")
|
|
migration.deleteData(forType: "TranscriptAnnotation")
|
|
|
|
migration.enumerateObjects(ofType: "Session") { _, session in
|
|
session?["transcriptIdentifier"] = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performs a write transaction on the database using `block`, on the main queue
|
|
public func update(with block: @escaping () -> Void) {
|
|
do {
|
|
try realm.write {
|
|
block()
|
|
}
|
|
} catch {
|
|
NSLog("Error performing realm write: \(error)")
|
|
}
|
|
}
|
|
|
|
/// Performs a write transaction on the database using `block`, on the current queue
|
|
public func unmanagedUpdate(with block: @escaping (_ realm: Realm) -> Void) {
|
|
do {
|
|
let tempRealm = try Realm(configuration: self.realmConfig)
|
|
|
|
try tempRealm.write {
|
|
block(tempRealm)
|
|
}
|
|
} catch {
|
|
NSLog("Error initializing temporary realm or performing background write: \(error)")
|
|
}
|
|
}
|
|
|
|
public func unmanagedObject<T: Object>(of type: T.Type, with primaryKey: String) -> T? {
|
|
do {
|
|
let tempRealm = try Realm(configuration: self.realmConfig)
|
|
|
|
return tempRealm.object(ofType: type, forPrimaryKey: primaryKey)
|
|
} catch {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public func unmanagedObjects<T: Object>(of type: T.Type, with predicate: NSPredicate) -> Results<T>? {
|
|
do {
|
|
let tempRealm = try Realm(configuration: self.realmConfig)
|
|
|
|
return tempRealm.objects(type).filter(predicate)
|
|
} catch {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func store(contentResult: Result<ContentsResponse, APIError>, completion: @escaping () -> Void) {
|
|
self.realm.autorefresh = false
|
|
|
|
defer { self.realm.autorefresh = true }
|
|
|
|
if case let .error(error) = contentResult {
|
|
print("Error downloading sessions: \(error)")
|
|
}
|
|
|
|
guard case let .success(sessionsResponse) = contentResult else {
|
|
return
|
|
}
|
|
|
|
self.realm.beginWrite()
|
|
|
|
// Merge existing session data, preserving user-defined data
|
|
sessionsResponse.sessions.forEach { newSession in
|
|
autoreleasepool {
|
|
if let existingSession = self.realm.object(ofType: Session.self, forPrimaryKey: newSession.identifier) {
|
|
existingSession.merge(with: newSession, in: self.realm)
|
|
|
|
self.realm.add(existingSession, update: true)
|
|
} else {
|
|
self.realm.add(newSession, update: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Merge existing instance data, preserving user-defined data
|
|
sessionsResponse.instances.forEach { newInstance in
|
|
return autoreleasepool {
|
|
if let existingInstance = self.realm.object(ofType: SessionInstance.self, forPrimaryKey: newInstance.identifier) {
|
|
existingInstance.merge(with: newInstance, in: self.realm)
|
|
|
|
self.realm.add(existingInstance, update: true)
|
|
} else {
|
|
self.realm.add(newInstance, update: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save everything
|
|
self.realm.add(sessionsResponse.rooms, update: true)
|
|
self.realm.add(sessionsResponse.tracks, update: true)
|
|
self.realm.add(sessionsResponse.events, update: true)
|
|
|
|
// add instances to rooms
|
|
self.realm.objects(Room.self).forEach { room in
|
|
let instances = self.realm.objects(SessionInstance.self).filter("roomIdentifier == %@", room.identifier)
|
|
|
|
instances.forEach({ $0.roomName = room.name })
|
|
|
|
room.instances.removeAll()
|
|
room.instances.append(objectsIn: instances)
|
|
}
|
|
|
|
// add instances and sessions to events
|
|
self.realm.objects(Event.self).forEach { event in
|
|
let instances = self.realm.objects(SessionInstance.self).filter("eventIdentifier == %@", event.identifier)
|
|
let sessions = self.realm.objects(Session.self).filter("eventIdentifier == %@", event.identifier)
|
|
|
|
event.sessionInstances.removeAll()
|
|
event.sessionInstances.append(objectsIn: instances)
|
|
|
|
event.sessions.removeAll()
|
|
event.sessions.append(objectsIn: sessions)
|
|
}
|
|
|
|
// add instances and sessions to tracks
|
|
self.realm.objects(Track.self).forEach { track in
|
|
let instances = self.realm.objects(SessionInstance.self).filter("trackIdentifier == %@", track.identifier)
|
|
let sessions = self.realm.objects(Session.self).filter("trackIdentifier == %@", track.identifier)
|
|
|
|
track.instances.removeAll()
|
|
track.instances.append(objectsIn: instances)
|
|
|
|
track.sessions.removeAll()
|
|
track.sessions.append(objectsIn: sessions)
|
|
|
|
sessions.forEach({ $0.trackName = track.name })
|
|
instances.forEach { instance in
|
|
instance.trackName = track.name
|
|
instance.session?.trackName = track.name
|
|
}
|
|
}
|
|
|
|
// add live video assets to sessions
|
|
self.realm.objects(SessionAsset.self).filter("rawAssetType == %@", SessionAssetType.liveStreamVideo.rawValue).forEach { liveAsset in
|
|
if let session = self.realm.objects(Session.self).filter("year == %d AND number == %@", liveAsset.year, liveAsset.sessionId).first {
|
|
if !session.assets.contains(liveAsset) {
|
|
session.assets.append(liveAsset)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create schedule view
|
|
|
|
self.realm.objects(ScheduleSection.self).forEach({ self.realm.delete($0) })
|
|
|
|
let instances = self.realm.objects(SessionInstance.self).sorted(by: SessionInstance.standardSort)
|
|
|
|
var previousStartTime: Date? = nil
|
|
for instance in instances {
|
|
guard instance.startTime != previousStartTime else { continue }
|
|
|
|
autoreleasepool {
|
|
let instancesForSection = instances.filter({ $0.startTime == instance.startTime })
|
|
|
|
let section = ScheduleSection()
|
|
|
|
section.representedDate = instance.startTime
|
|
section.eventIdentifier = instance.eventIdentifier
|
|
section.instances.removeAll()
|
|
section.instances.append(objectsIn: instancesForSection)
|
|
section.identifier = ScheduleSection.identifierFormatter.string(from: instance.startTime)
|
|
|
|
self.realm.add(section, update: true)
|
|
|
|
previousStartTime = instance.startTime
|
|
}
|
|
}
|
|
|
|
do {
|
|
try self.realm.commitWrite()
|
|
|
|
DispatchQueue.main.async { completion() }
|
|
} catch {
|
|
NSLog("Realm error: \(error)")
|
|
}
|
|
}
|
|
|
|
internal func store(liveVideosResult: Result<[SessionAsset], APIError>) {
|
|
guard case .success(let assets) = liveVideosResult else { return }
|
|
|
|
self.realm.beginWrite()
|
|
|
|
assets.forEach { asset in
|
|
asset.identifier = asset.generateIdentifier()
|
|
|
|
if let existingAsset = self.realm.objects(SessionAsset.self).filter("identifier == %@", asset.identifier).first {
|
|
existingAsset.remoteURL = asset.remoteURL
|
|
} else {
|
|
self.realm.add(asset, update: true)
|
|
|
|
if let session = self.realm.objects(Session.self).filter("year == %d AND number == %@", asset.year, asset.sessionId).first {
|
|
if !session.assets.contains(asset) {
|
|
session.assets.append(asset)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
do {
|
|
try self.realm.commitWrite()
|
|
} catch {
|
|
NSLog("Error syncing live videos: \(error)")
|
|
}
|
|
}
|
|
|
|
public lazy var events: Observable<Results<Event>> = {
|
|
let eventsSortedByDateDescending = self.realm.objects(Event.self).sorted(byKeyPath: "startDate", ascending: false)
|
|
|
|
return Observable.collection(from: eventsSortedByDateDescending)
|
|
}()
|
|
|
|
public lazy var sessionsObservable: Observable<Results<Session>> = {
|
|
return Observable.collection(from: self.realm.objects(Session.self))
|
|
}()
|
|
|
|
public var sessions: Results<Session> {
|
|
return self.realm.objects(Session.self).filter("assets.@count > 0")
|
|
}
|
|
|
|
public func session(with identifier: String) -> Session? {
|
|
return realm.object(ofType: Session.self, forPrimaryKey: identifier)
|
|
}
|
|
|
|
public func createFavorite(for session: Session) {
|
|
do {
|
|
try realm.write {
|
|
session.favorites.append(Favorite())
|
|
}
|
|
} catch {
|
|
NSLog("Error creating favorite for session \(session)")
|
|
}
|
|
}
|
|
|
|
public var isEmpty: Bool {
|
|
return realm.objects(Event.self).count <= 0
|
|
}
|
|
|
|
public func removeFavorite(for session: Session) {
|
|
guard let favorite = session.favorites.first else { return }
|
|
|
|
do {
|
|
try realm.write {
|
|
realm.delete(favorite)
|
|
}
|
|
} catch {
|
|
NSLog("Error creating favorite for session \(session)")
|
|
}
|
|
}
|
|
|
|
public lazy var tracksObservable: Observable<Results<Track>> = {
|
|
let tracks = self.realm.objects(Track.self).sorted(byKeyPath: "order")
|
|
|
|
return Observable.collection(from: tracks)
|
|
}()
|
|
|
|
public lazy var scheduleObservable: Observable<Results<ScheduleSection>> = {
|
|
let currentEvents = self.realm.objects(Event.self).filter("isCurrent == true")
|
|
|
|
return Observable.collection(from: currentEvents).map({ $0.first?.identifier }).flatMap { (identifier: String?) -> Observable<Results<ScheduleSection>> in
|
|
let sections = self.realm.objects(ScheduleSection.self).filter("eventIdentifier == %@", identifier ?? "").sorted(byKeyPath: "representedDate")
|
|
|
|
return Observable.collection(from: sections)
|
|
}
|
|
}()
|
|
|
|
public func asset(with remoteURL: URL) -> SessionAsset? {
|
|
return realm.objects(SessionAsset.self).filter("remoteURL == %@", remoteURL.absoluteString).first
|
|
}
|
|
|
|
public func bookmark(with identifier: String) -> Bookmark? {
|
|
return realm.object(ofType: Bookmark.self, forPrimaryKey: identifier)
|
|
}
|
|
|
|
public func deleteBookmark(with identifier: String) {
|
|
_ = realm.refresh()
|
|
|
|
update {
|
|
guard let bookmark = self.bookmark(with: identifier) else {
|
|
NSLog("DELETE ERROR: Unable to find bookmark with identifier \(identifier)")
|
|
return
|
|
}
|
|
|
|
self.realm.delete(bookmark)
|
|
}
|
|
}
|
|
|
|
public func softDeleteBookmark(with identifier: String) {
|
|
_ = realm.refresh()
|
|
|
|
update {
|
|
guard let bookmark = self.bookmark(with: identifier) else {
|
|
NSLog("SOFT DELETE ERROR: Unable to find bookmark with identifier \(identifier)")
|
|
return
|
|
}
|
|
|
|
bookmark.isDeleted = true
|
|
bookmark.deletedAt = Date()
|
|
}
|
|
}
|
|
|
|
public func moveBookmark(with identifier: String, to timecode: Double) {
|
|
_ = realm.refresh()
|
|
|
|
update {
|
|
guard let bookmark = self.bookmark(with: identifier) else {
|
|
NSLog("MOVE ERROR: Unable to find bookmark with identifier \(identifier)")
|
|
return
|
|
}
|
|
|
|
bookmark.timecode = timecode
|
|
self.realm.add(bookmark, update: true)
|
|
}
|
|
}
|
|
|
|
public func updateDownloadedFlag(_ isDownloaded: Bool, forAssetWithRelativeLocalURL filePath: String) {
|
|
DispatchQueue.main.async {
|
|
guard let asset = self.realm.objects(SessionAsset.self).filter("relativeLocalURL == %@", filePath).first else { return }
|
|
|
|
self.update {
|
|
asset.session.first?.isDownloaded = isDownloaded
|
|
}
|
|
}
|
|
}
|
|
|
|
public var allEvents: [Event] {
|
|
return realm.objects(Event.self).sorted(byKeyPath: "startDate", ascending: false).toArray()
|
|
}
|
|
|
|
public var allFocuses: [Focus] {
|
|
return realm.objects(Focus.self).sorted(byKeyPath: "name").toArray()
|
|
}
|
|
|
|
public var allTracks: [Track] {
|
|
return realm.objects(Track.self).sorted(byKeyPath: "order").toArray()
|
|
}
|
|
|
|
}
|