mirror of
https://github.com/tappollo/WWDC.git
synced 2026-04-29 20:35:24 +08:00
Basic download manager
This commit is contained in:
@@ -11,6 +11,7 @@ import RealmSwift
|
||||
|
||||
public enum DownloadStatus: String {
|
||||
case none
|
||||
case deleted
|
||||
case downloading
|
||||
case paused
|
||||
case failed
|
||||
|
||||
@@ -24,7 +24,18 @@ public final class Storage {
|
||||
self.realm = try Realm(configuration: configuration)
|
||||
}
|
||||
|
||||
func store(sessionsResult: Result<SessionsResponse, APIError>, scheduleResult: Result<ScheduleResponse, APIError>) {
|
||||
/// Performs a write transaction on the database using `block`
|
||||
public func update(with block: @escaping () -> Void) {
|
||||
do {
|
||||
try realm.write {
|
||||
block()
|
||||
}
|
||||
} catch {
|
||||
NSLog("Error performing realm write: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func store(sessionsResult: Result<SessionsResponse, APIError>, scheduleResult: Result<ScheduleResponse, APIError>, completion: @escaping () -> Void) {
|
||||
backgroundQueue.async {
|
||||
if self.backgroundRealm == nil {
|
||||
do {
|
||||
@@ -97,6 +108,8 @@ public final class Storage {
|
||||
|
||||
do {
|
||||
try self.backgroundRealm.commitWrite()
|
||||
|
||||
DispatchQueue.main.async { completion() }
|
||||
} catch {
|
||||
NSLog("Realm error: \(error)")
|
||||
}
|
||||
@@ -109,10 +122,14 @@ public final class Storage {
|
||||
return Observable.collection(from: eventsSortedByDateDescending)
|
||||
}()
|
||||
|
||||
public lazy var sessions: Observable<Results<Session>> = {
|
||||
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)
|
||||
}
|
||||
@@ -139,4 +156,29 @@ public final class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
public lazy var activeDownloads: Observable<Results<Download>> = {
|
||||
let results = self.realm.objects(Download.self).filter("rawStatus != %@ AND rawStatus != %@", "none", "deleted")
|
||||
|
||||
return Observable.collection(from: results)
|
||||
}()
|
||||
|
||||
public func createDownload(for asset: SessionAsset) {
|
||||
// prevent multiple download instances per session asset
|
||||
guard asset.downloads.filter({ $0.status != .deleted && $0.status != .none }).count == 0 else { return }
|
||||
|
||||
do {
|
||||
try realm.write {
|
||||
let download = Download()
|
||||
download.status = .paused
|
||||
asset.downloads.append(download)
|
||||
}
|
||||
} catch {
|
||||
NSLog("Error creating download for asset \(asset): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public func asset(with remoteURL: URL) -> SessionAsset? {
|
||||
return realm.objects(SessionAsset.self).filter("remoteURL == %@", remoteURL.absoluteString).first
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
import Foundation
|
||||
import RxSwift
|
||||
|
||||
extension Notification.Name {
|
||||
public static let SyncEngineDidSyncSessionsAndSchedule = Notification.Name("SyncEngineDidSyncSessionsAndSchedule")
|
||||
}
|
||||
|
||||
public final class SyncEngine {
|
||||
|
||||
public let storage: Storage
|
||||
@@ -19,12 +23,14 @@ public final class SyncEngine {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
public func syncSessionsAndSchedule(completion: @escaping (APIError?) -> Void) {
|
||||
public func syncSessionsAndSchedule() {
|
||||
client.fetchSessions { [weak self] sessionsResult in
|
||||
DispatchQueue.main.async {
|
||||
self?.client.fetchSchedule { scheduleResult in
|
||||
DispatchQueue.main.async {
|
||||
self?.storage.store(sessionsResult: sessionsResult, scheduleResult: scheduleResult)
|
||||
self?.storage.store(sessionsResult: sessionsResult, scheduleResult: scheduleResult) {
|
||||
NotificationCenter.default.post(name: .SyncEngineDidSyncSessionsAndSchedule, object: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
DDF32EBB1EBE65DD0028E39D /* AppCoordinator+SessionActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF32EBA1EBE65DD0028E39D /* AppCoordinator+SessionActions.swift */; };
|
||||
DDF32EBF1EBE68EE0028E39D /* NSTableView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF32EBE1EBE68EE0028E39D /* NSTableView+Rx.swift */; };
|
||||
DDFA10BD1EBEA584001DCF66 /* Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFA10BC1EBEA584001DCF66 /* Download.swift */; };
|
||||
DDFA10BF1EBEAAAD001DCF66 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFA10BE1EBEAAAD001DCF66 /* DownloadManager.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -326,6 +327,7 @@
|
||||
DDF32EBA1EBE65DD0028E39D /* AppCoordinator+SessionActions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppCoordinator+SessionActions.swift"; sourceTree = "<group>"; };
|
||||
DDF32EBE1EBE68EE0028E39D /* NSTableView+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTableView+Rx.swift"; sourceTree = "<group>"; };
|
||||
DDFA10BC1EBEA584001DCF66 /* Download.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Download.swift; sourceTree = "<group>"; };
|
||||
DDFA10BE1EBEAAAD001DCF66 /* DownloadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -423,6 +425,7 @@
|
||||
DDF32EB81EBE65B50028E39D /* AppCoordinator+Shelf.swift */,
|
||||
DDF32EB61EBE65930028E39D /* AppCoordinator+UserActivity.swift */,
|
||||
DDF32EBA1EBE65DD0028E39D /* AppCoordinator+SessionActions.swift */,
|
||||
DDFA10BE1EBEAAAD001DCF66 /* DownloadManager.swift */,
|
||||
);
|
||||
name = Bootstrap;
|
||||
sourceTree = "<group>";
|
||||
@@ -993,6 +996,7 @@
|
||||
DD7F38761EABFB20002D8C00 /* NSColor+Hex.swift in Sources */,
|
||||
DDB28F931EAD48D70077703F /* UserActivityRepresentable.swift in Sources */,
|
||||
DD7F38801EAC15B4002D8C00 /* RxNil.swift in Sources */,
|
||||
DDFA10BF1EBEAAAD001DCF66 /* DownloadManager.swift in Sources */,
|
||||
DD7F38881EAC2275002D8C00 /* PathUtil.swift in Sources */,
|
||||
DD7F386A1EABE996002D8C00 /* SessionTableCellView.swift in Sources */,
|
||||
DD36A4B21E478C6A00B2EA88 /* VideosSplitViewController.swift in Sources */,
|
||||
|
||||
@@ -25,7 +25,11 @@ extension AppCoordinator: SessionActionsViewControllerDelegate {
|
||||
}
|
||||
|
||||
func sessionActionsDidSelectDownload(_ sender: NSView?) {
|
||||
guard let viewModel = selectedSessionValue else { return }
|
||||
|
||||
guard let videoAsset = viewModel.session.assets.filter({ $0.assetType == .hdVideo }).first else { return }
|
||||
|
||||
downloadManager.download(videoAsset)
|
||||
}
|
||||
|
||||
func sessionActionsDidSelectShare(_ sender: NSView?) {
|
||||
|
||||
@@ -18,6 +18,7 @@ final class AppCoordinator {
|
||||
|
||||
var storage: Storage
|
||||
var syncEngine: SyncEngine
|
||||
var downloadManager: DownloadManager
|
||||
|
||||
var windowController: MainWindowController
|
||||
var tabController: MainTabController
|
||||
@@ -40,6 +41,8 @@ final class AppCoordinator {
|
||||
self.storage = try Storage(realmConfig)
|
||||
|
||||
self.syncEngine = SyncEngine(storage: storage, client: client)
|
||||
|
||||
self.downloadManager = DownloadManager(storage)
|
||||
} catch {
|
||||
fatalError("Realm initialization error: \(error)")
|
||||
}
|
||||
@@ -69,7 +72,6 @@ final class AppCoordinator {
|
||||
}
|
||||
|
||||
private func setupBindings() {
|
||||
storage.sessions.bind(to: videosController.listViewController.sessions).addDisposableTo(self.disposeBag)
|
||||
selectedSession.bind(to: videosController.detailViewController.viewModel).addDisposableTo(self.disposeBag)
|
||||
|
||||
selectedSession.subscribe(onNext: updateCurrentActivity).addDisposableTo(self.disposeBag)
|
||||
@@ -82,19 +84,22 @@ final class AppCoordinator {
|
||||
detail.summaryController.actionsViewController.delegate = self
|
||||
}
|
||||
|
||||
private func updateListsAfterSync() {
|
||||
self.videosController.listViewController.sessions = storage.sessions
|
||||
}
|
||||
|
||||
@IBAction func refresh(_ sender: Any?) {
|
||||
syncEngine.syncSessionsAndSchedule { error in
|
||||
if let error = error {
|
||||
// TODO: better error handling
|
||||
print("Error while syncing sessions and schedule: \(error)")
|
||||
}
|
||||
}
|
||||
syncEngine.syncSessionsAndSchedule()
|
||||
}
|
||||
|
||||
func startup() {
|
||||
windowController.contentViewController = tabController
|
||||
windowController.showWindow(self)
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .SyncEngineDidSyncSessionsAndSchedule, object: nil, queue: OperationQueue.main) { _ in
|
||||
self.updateListsAfterSync()
|
||||
}
|
||||
|
||||
refresh(nil)
|
||||
}
|
||||
|
||||
|
||||
95
WWDC/DownloadManager.swift
Normal file
95
WWDC/DownloadManager.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// DownloadManager.swift
|
||||
// WWDC
|
||||
//
|
||||
// Created by Guilherme Rambo on 06/05/17.
|
||||
// Copyright © 2017 Guilherme Rambo. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import RxSwift
|
||||
import ConfCore
|
||||
|
||||
final class DownloadManager: NSObject {
|
||||
|
||||
fileprivate let storage: Storage
|
||||
|
||||
fileprivate var tasks: [String: URLSessionDownloadTask] = [:]
|
||||
|
||||
fileprivate lazy var downloadSession: URLSession = {
|
||||
let s = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue.main)
|
||||
|
||||
return s
|
||||
}()
|
||||
|
||||
init(_ storage: Storage) {
|
||||
self.storage = storage
|
||||
}
|
||||
|
||||
func download(_ asset: SessionAsset) {
|
||||
guard let url = URL(string: asset.remoteURL) else { return }
|
||||
|
||||
storage.createDownload(for: asset)
|
||||
|
||||
let task = downloadSession.downloadTask(with: url)
|
||||
|
||||
self.tasks[asset.remoteURL] = task
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DownloadManager: URLSessionDelegate, URLSessionDownloadDelegate, URLSessionTaskDelegate {
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
guard let url = downloadTask.originalRequest?.url else { return }
|
||||
|
||||
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
|
||||
|
||||
let download = storage.asset(with: url)?.downloads.first
|
||||
storage.update {
|
||||
download?.status = .downloading
|
||||
download?.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
guard let url = downloadTask.originalRequest?.url else { return }
|
||||
guard let asset = storage.asset(with: url) else { return }
|
||||
guard let dir = NSSearchPathForDirectoriesInDomains(.moviesDirectory, .userDomainMask, true).first else { return }
|
||||
|
||||
let finalPath = dir + "/" + asset.relativeLocalURL
|
||||
|
||||
let download = asset.downloads.first
|
||||
|
||||
do {
|
||||
let finalURL = URL(fileURLWithPath: finalPath)
|
||||
let finalDirURL = finalURL.deletingLastPathComponent()
|
||||
|
||||
try FileManager.default.createDirectory(at: finalDirURL, withIntermediateDirectories: true, attributes: nil)
|
||||
try FileManager.default.moveItem(at: location, to: finalURL)
|
||||
|
||||
storage.update {
|
||||
download?.status = .completed
|
||||
download?.progress = 1
|
||||
}
|
||||
} catch {
|
||||
NSLog("Error copying file downloaded for \(asset): \(error)")
|
||||
|
||||
storage.update {
|
||||
download?.status = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
guard let url = task.originalRequest?.url else { return }
|
||||
|
||||
let download = storage.asset(with: url)?.downloads.first
|
||||
storage.update {
|
||||
download?.status = error != nil ? .failed : .completed
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,13 @@ class VideosTableViewController: NSViewController {
|
||||
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
var sessions = Variable<Results<Session>?>(nil)
|
||||
var sessions: Results<Session>? {
|
||||
didSet {
|
||||
guard oldValue?.count != sessions?.count else { return }
|
||||
|
||||
updateSessionsList()
|
||||
}
|
||||
}
|
||||
var selectedSession = Variable<SessionViewModel?>(nil)
|
||||
|
||||
var viewModels: [SessionRow] = [] {
|
||||
@@ -45,6 +51,28 @@ class VideosTableViewController: NSViewController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateSessionsList() {
|
||||
guard let results = sessions else { return }
|
||||
|
||||
let sortedSessions = results.sorted(by: Session.standardSort)
|
||||
|
||||
var outViewModels: [SessionRow] = []
|
||||
let rowModels = sortedSessions.flatMap(SessionViewModel.init(session:)).map(SessionRow.init(viewModel:))
|
||||
|
||||
var previousRowModel: SessionRow? = nil
|
||||
for rowModel in rowModels {
|
||||
if rowModel.viewModel.trackName != previousRowModel?.viewModel.trackName {
|
||||
outViewModels.append(SessionRow(title: rowModel.viewModel.trackName))
|
||||
}
|
||||
|
||||
outViewModels.append(rowModel)
|
||||
|
||||
previousRowModel = rowModel
|
||||
}
|
||||
|
||||
self.viewModels = rowModels
|
||||
}
|
||||
|
||||
lazy var tableView: NSTableView = {
|
||||
let v = NSTableView()
|
||||
|
||||
@@ -102,27 +130,7 @@ class VideosTableViewController: NSViewController {
|
||||
return self.viewModels[index].viewModel
|
||||
}.bind(to: selectedSession).addDisposableTo(self.disposeBag)
|
||||
|
||||
sessions.asObservable().subscribe(onNext: { [weak self] results in
|
||||
guard let results = results else { return }
|
||||
|
||||
let sortedSessions = results.filter(NSPredicate(format: "assets.@count > 0")).sorted(by: Session.standardSort)
|
||||
|
||||
var outViewModels: [SessionRow] = []
|
||||
let rowModels = sortedSessions.flatMap(SessionViewModel.init(session:)).map(SessionRow.init(viewModel:))
|
||||
|
||||
var previousRowModel: SessionRow? = nil
|
||||
for rowModel in rowModels {
|
||||
if rowModel.viewModel.trackName != previousRowModel?.viewModel.trackName {
|
||||
outViewModels.append(SessionRow(title: rowModel.viewModel.trackName))
|
||||
}
|
||||
|
||||
outViewModels.append(rowModel)
|
||||
|
||||
previousRowModel = rowModel
|
||||
}
|
||||
|
||||
self?.viewModels = outViewModels
|
||||
}).addDisposableTo(self.disposeBag)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user