mirror of
https://github.com/tappollo/WWDC.git
synced 2026-04-30 04:45:17 +08:00
Updated for 2017
This commit is contained in:
@@ -45,10 +45,10 @@ public final class AppleAPIClient {
|
||||
return try self?.failableAdaptCollection(newsItemsJson, using: NewsItemsJSONAdapter())
|
||||
}
|
||||
|
||||
service.configureTransformer(environment.sessionsPath) { [weak self] (entity: Entity<JSON>) throws -> ScheduleResponse? in
|
||||
service.configureTransformer(environment.sessionsPath) { [weak self] (entity: Entity<JSON>) throws -> ContentsResponse? in
|
||||
let json = entity.content as JSON
|
||||
|
||||
return try self?.failableAdapt(json, using: ScheduleResponseAdapter())
|
||||
return try self?.failableAdapt(json, using: ContentsResponseAdapter())
|
||||
}
|
||||
|
||||
service.configureTransformer(environment.videosPath) { [weak self] (entity: Entity<JSON>) throws -> SessionsResponse? in
|
||||
@@ -122,7 +122,7 @@ public final class AppleAPIClient {
|
||||
// MARK: - Standard API requests
|
||||
|
||||
private var liveVideoAssetsResource: Resource!
|
||||
private var scheduleResource: Resource!
|
||||
private var contentsResource: Resource!
|
||||
private var sessionsResource: Resource!
|
||||
private var newsItemsResource: Resource!
|
||||
|
||||
@@ -142,26 +142,15 @@ public final class AppleAPIClient {
|
||||
currentLiveVideosRequest = liveVideoAssetsResource.loadIfNeeded()
|
||||
}
|
||||
|
||||
public func fetchSchedule(completion: @escaping (Result<ScheduleResponse, APIError>) -> Void) {
|
||||
if scheduleResource == nil {
|
||||
scheduleResource = schedule.addObserver(owner: self) { [weak self] resource, event in
|
||||
public func fetchContent(completion: @escaping (Result<ContentsResponse, APIError>) -> Void) {
|
||||
if contentsResource == nil {
|
||||
contentsResource = schedule.addObserver(owner: self) { [weak self] resource, event in
|
||||
self?.process(resource, event: event, with: completion)
|
||||
}
|
||||
}
|
||||
|
||||
currentScheduleRequest?.cancel()
|
||||
currentScheduleRequest = scheduleResource.loadIfNeeded()
|
||||
}
|
||||
|
||||
public func fetchSessions(completion: @escaping (Result<SessionsResponse, APIError>) -> Void) {
|
||||
if sessionsResource == nil {
|
||||
sessionsResource = sessions.addObserver(owner: self) { [weak self] resource, event in
|
||||
self?.process(resource, event: event, with: completion)
|
||||
}
|
||||
}
|
||||
|
||||
currentSessionsRequest?.cancel()
|
||||
currentSessionsRequest = sessionsResource.loadIfNeeded()
|
||||
currentScheduleRequest = contentsResource.loadIfNeeded()
|
||||
}
|
||||
|
||||
public func fetchNewsItems(completion: @escaping (Result<[NewsItem], APIError>) -> Void) {
|
||||
|
||||
@@ -77,9 +77,9 @@ extension Environment {
|
||||
newsPath: "/news.json",
|
||||
liveVideosPath: "/videos_live.json")
|
||||
|
||||
public static let production = Environment(baseURL: "https://api.wwdc.io",
|
||||
public static let production = Environment(baseURL: "https://api2017.wwdc.io",
|
||||
videosPath: "/videos.json",
|
||||
sessionsPath: "/sessions.json",
|
||||
sessionsPath: "/contents.json",
|
||||
newsPath: "/news.json",
|
||||
liveVideosPath: "/videos_live.json")
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ public class Event: Object {
|
||||
/// Sessions held at this event
|
||||
public let sessions = List<Session>()
|
||||
|
||||
public dynamic var imagesPath = ""
|
||||
|
||||
/// Session instances for schedule
|
||||
public var sessionInstances = List<SessionInstance>()
|
||||
|
||||
@@ -47,7 +49,7 @@ public class Event: Object {
|
||||
]
|
||||
}
|
||||
|
||||
public static func make(identifier: String, name: String, startDate: Date, endDate: Date, isCurrent: Bool) -> Event {
|
||||
public static func make(identifier: String, name: String, startDate: Date, endDate: Date, isCurrent: Bool, imagesPath: String) -> Event {
|
||||
let event = Event()
|
||||
|
||||
event.identifier = identifier
|
||||
@@ -55,6 +57,7 @@ public class Event: Object {
|
||||
event.startDate = startDate
|
||||
event.endDate = endDate
|
||||
event.isCurrent = isCurrent
|
||||
event.imagesPath = imagesPath
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
private enum EventKeys: String, JSONSubscriptType {
|
||||
case name, current, identifier, start, end
|
||||
case name, current
|
||||
case start = "startTime"
|
||||
case end = "endTime"
|
||||
case identifier = "id"
|
||||
case imagesPath
|
||||
|
||||
var jsonKey: JSONKey {
|
||||
return JSONKey.key(rawValue)
|
||||
@@ -34,6 +38,10 @@ final class EventsJSONAdapter: Adapter {
|
||||
return .error(.missingKey(EventKeys.current))
|
||||
}
|
||||
|
||||
guard let imagesPath = input[EventKeys.imagesPath].string else {
|
||||
return .error(.missingKey(EventKeys.imagesPath))
|
||||
}
|
||||
|
||||
guard let rawStart = input[EventKeys.start].string else {
|
||||
return .error(.missingKey(EventKeys.start))
|
||||
}
|
||||
@@ -42,11 +50,11 @@ final class EventsJSONAdapter: Adapter {
|
||||
return .error(.missingKey(EventKeys.end))
|
||||
}
|
||||
|
||||
guard case .success(let startDate) = DateAdapter().adapt(rawStart) else {
|
||||
guard case .success(let startDate) = DateTimeAdapter().adapt(rawStart) else {
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
guard case .success(let endDate) = DateAdapter().adapt(rawEnd) else {
|
||||
guard case .success(let endDate) = DateTimeAdapter().adapt(rawEnd) else {
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
@@ -54,7 +62,8 @@ final class EventsJSONAdapter: Adapter {
|
||||
name: name,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
isCurrent: current)
|
||||
isCurrent: current,
|
||||
imagesPath: imagesPath)
|
||||
|
||||
return .success(event)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import RealmSwift
|
||||
/// Represents a room or venue where sessions are held
|
||||
public class Room: Object {
|
||||
|
||||
public dynamic var identifier = ""
|
||||
|
||||
/// Name of the map file (maps are not present in the macOS app because they are embedded in the iOS app's binary, not given by the API)
|
||||
public dynamic var mapName = ""
|
||||
|
||||
@@ -34,9 +36,10 @@ public class Room: Object {
|
||||
]
|
||||
}
|
||||
|
||||
public static func make(name: String, mapName: String, floor: String) -> Room {
|
||||
public static func make(identifier: String, name: String, mapName: String, floor: String) -> Room {
|
||||
let room = Room()
|
||||
|
||||
room.identifier = identifier
|
||||
room.name = name
|
||||
room.mapName = mapName
|
||||
room.floor = floor
|
||||
|
||||
@@ -11,6 +11,7 @@ import SwiftyJSON
|
||||
|
||||
private enum RoomKeys: String, JSONSubscriptType {
|
||||
case name, mapName, floor
|
||||
case identifier = "id"
|
||||
|
||||
var jsonKey: JSONKey {
|
||||
return JSONKey.key(rawValue)
|
||||
@@ -22,19 +23,15 @@ final class RoomsJSONAdapter: Adapter {
|
||||
typealias OutputType = Room
|
||||
|
||||
func adapt(_ input: JSON) -> Result<Room, AdapterError> {
|
||||
guard let identifier = input[RoomKeys.identifier].int else {
|
||||
return .error(.missingKey(RoomKeys.identifier))
|
||||
}
|
||||
|
||||
guard let name = input[RoomKeys.name].string else {
|
||||
return .error(.missingKey(RoomKeys.name))
|
||||
}
|
||||
|
||||
guard let mapName = input[RoomKeys.mapName].string else {
|
||||
return .error(.missingKey(RoomKeys.mapName))
|
||||
}
|
||||
|
||||
guard let floor = input[RoomKeys.floor].string else {
|
||||
return .error(.missingKey(RoomKeys.floor))
|
||||
}
|
||||
|
||||
let room = Room.make(name: name, mapName: mapName, floor: floor)
|
||||
let room = Room.make(identifier: "\(identifier)", name: name, mapName: "", floor: "")
|
||||
|
||||
return .success(room)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ScheduleResponse {
|
||||
public struct ContentsResponse {
|
||||
|
||||
public let events: [Event]
|
||||
public let rooms: [Room]
|
||||
public let tracks: [Track]
|
||||
public let instances: [SessionInstance]
|
||||
public let sessions: [Session]
|
||||
|
||||
}
|
||||
|
||||
@@ -9,52 +9,64 @@
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
private enum ScheduleKeys: String, JSONSubscriptType {
|
||||
case response, rooms, tracks, sessions
|
||||
private enum ContentKeys: String, JSONSubscriptType {
|
||||
case response, rooms, tracks, sessions, events, contents
|
||||
|
||||
var jsonKey: JSONKey {
|
||||
return JSONKey.key(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
final class ScheduleResponseAdapter: Adapter {
|
||||
final class ContentsResponseAdapter: Adapter {
|
||||
|
||||
typealias InputType = JSON
|
||||
typealias OutputType = ScheduleResponse
|
||||
typealias OutputType = ContentsResponse
|
||||
|
||||
func adapt(_ input: JSON) -> Result<ScheduleResponse, AdapterError> {
|
||||
guard let roomsJson = input[ScheduleKeys.response][ScheduleKeys.rooms].array else {
|
||||
return .error(.missingKey(ScheduleKeys.rooms))
|
||||
func adapt(_ input: JSON) -> Result<ContentsResponse, AdapterError> {
|
||||
guard let eventsJson = input[ContentKeys.events].array else {
|
||||
return .error(.missingKey(ContentKeys.events))
|
||||
}
|
||||
|
||||
guard let tracksJson = input[ScheduleKeys.response][ScheduleKeys.tracks].array else {
|
||||
return .error(.missingKey(ScheduleKeys.rooms))
|
||||
guard case .success(let events) = EventsJSONAdapter().adapt(eventsJson) else {
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
guard let instancesJson = input[ScheduleKeys.response][ScheduleKeys.sessions].array else {
|
||||
return .error(.missingKey(ScheduleKeys.rooms))
|
||||
guard let roomsJson = input[ContentKeys.rooms].array else {
|
||||
return .error(.missingKey(ContentKeys.rooms))
|
||||
}
|
||||
|
||||
guard case .success(let rooms) = RoomsJSONAdapter().adapt(roomsJson) else {
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
guard let tracksJson = input[ContentKeys.tracks].array else {
|
||||
return .error(.missingKey(ContentKeys.rooms))
|
||||
}
|
||||
|
||||
guard case .success(var tracks) = TracksJSONAdapter().adapt(tracksJson) else {
|
||||
guard case .success(let tracks) = TracksJSONAdapter().adapt(tracksJson) else {
|
||||
return .error(.missingKey(ContentKeys.tracks))
|
||||
}
|
||||
|
||||
guard let sessionsJson = input[ContentKeys.contents].array else {
|
||||
return .error(.missingKey(ContentKeys.contents))
|
||||
}
|
||||
|
||||
guard case .success(var sessions) = SessionsJSONAdapter().adapt(sessionsJson) else {
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
// add order to tracks using the order from the server
|
||||
for i in 0..<tracks.count {
|
||||
tracks[i].order = i
|
||||
}
|
||||
|
||||
guard case .success(let instances) = SessionInstancesJSONAdapter().adapt(instancesJson) else {
|
||||
guard case .success(let instances) = SessionInstancesJSONAdapter().adapt(sessionsJson) else {
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
let response = ScheduleResponse(rooms: rooms,
|
||||
tracks: tracks,
|
||||
instances: instances)
|
||||
// remove duplicated sessions
|
||||
instances.forEach { instance in
|
||||
guard let index = sessions.index(where: { $0.identifier == instance.session?.identifier }) else { return }
|
||||
|
||||
sessions.remove(at: index)
|
||||
}
|
||||
|
||||
let response = ContentsResponse(events: events, rooms: rooms, tracks: tracks, instances: instances, sessions: sessions)
|
||||
|
||||
return .success(response)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ public class Session: Object {
|
||||
/// Title
|
||||
public dynamic var title = ""
|
||||
|
||||
public dynamic var staticContentId = ""
|
||||
|
||||
/// Description
|
||||
public dynamic var summary = ""
|
||||
|
||||
@@ -31,7 +33,7 @@ public class Session: Object {
|
||||
public dynamic var eventIdentifier = ""
|
||||
|
||||
/// Track name
|
||||
public dynamic var trackName = ""
|
||||
public dynamic var trackIdentifier = ""
|
||||
|
||||
/// The session's focuses
|
||||
public let focuses = List<Focus>()
|
||||
@@ -115,7 +117,8 @@ public class Session: Object {
|
||||
self.number = other.number
|
||||
self.summary = other.summary
|
||||
self.eventIdentifier = other.eventIdentifier
|
||||
self.trackName = other.trackName
|
||||
self.trackIdentifier = other.trackIdentifier
|
||||
self.staticContentId = other.staticContentId
|
||||
|
||||
let otherFocuses = other.focuses.map { newFocus -> (Focus) in
|
||||
if newFocus.realm == nil,
|
||||
|
||||
@@ -9,100 +9,13 @@
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
private enum AssetKeys: String, JSONSubscriptType {
|
||||
case id, year, title, download_hd, download_sd, slides, webpageURL, url, images, shelf
|
||||
|
||||
var jsonKey: JSONKey {
|
||||
return JSONKey.key(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
final class SessionAssetsJSONAdapter: Adapter {
|
||||
|
||||
typealias InputType = JSON
|
||||
typealias OutputType = [SessionAsset]
|
||||
|
||||
func adapt(_ input: JSON) -> Result<[SessionAsset], AdapterError> {
|
||||
guard let title = input[AssetKeys.title].string else {
|
||||
return .error(.missingKey(AssetKeys.title))
|
||||
}
|
||||
|
||||
guard let sessionId = input[AssetKeys.id].string else {
|
||||
return .error(.missingKey(AssetKeys.id))
|
||||
}
|
||||
|
||||
guard let year = input[AssetKeys.year].int else {
|
||||
return .error(.missingKey(AssetKeys.year))
|
||||
}
|
||||
|
||||
var output = [SessionAsset]()
|
||||
|
||||
if let url = input[AssetKeys.url].string {
|
||||
let streaming = SessionAsset()
|
||||
streaming.rawAssetType = SessionAssetType.streamingVideo.rawValue
|
||||
streaming.remoteURL = url
|
||||
streaming.year = year
|
||||
streaming.sessionId = sessionId
|
||||
output.append(streaming)
|
||||
}
|
||||
|
||||
if let hd = input[AssetKeys.download_hd].string {
|
||||
let hdVideo = SessionAsset()
|
||||
hdVideo.rawAssetType = SessionAssetType.hdVideo.rawValue
|
||||
hdVideo.remoteURL = hd
|
||||
hdVideo.year = year
|
||||
hdVideo.sessionId = sessionId
|
||||
|
||||
let filename = URL(string: hd)?.lastPathComponent ?? "\(title).mp4"
|
||||
hdVideo.relativeLocalURL = "\(year)/\(filename)"
|
||||
|
||||
output.append(hdVideo)
|
||||
}
|
||||
|
||||
if let sd = input[AssetKeys.download_sd].string {
|
||||
let sdVideo = SessionAsset()
|
||||
sdVideo.rawAssetType = SessionAssetType.sdVideo.rawValue
|
||||
sdVideo.remoteURL = sd
|
||||
sdVideo.year = year
|
||||
sdVideo.sessionId = sessionId
|
||||
|
||||
let filename = URL(string: sd)?.lastPathComponent ?? "\(title).mp4"
|
||||
sdVideo.relativeLocalURL = "\(year)/\(filename)"
|
||||
|
||||
output.append(sdVideo)
|
||||
}
|
||||
|
||||
if let slides = input[AssetKeys.slides].string {
|
||||
let slidesAsset = SessionAsset()
|
||||
slidesAsset.rawAssetType = SessionAssetType.slides.rawValue
|
||||
slidesAsset.remoteURL = slides
|
||||
slidesAsset.year = year
|
||||
slidesAsset.sessionId = sessionId
|
||||
|
||||
output.append(slidesAsset)
|
||||
}
|
||||
|
||||
if let webpage = input[AssetKeys.webpageURL].string {
|
||||
let webpageAsset = SessionAsset()
|
||||
webpageAsset.rawAssetType = SessionAssetType.webpage.rawValue
|
||||
webpageAsset.remoteURL = webpage
|
||||
webpageAsset.year = year
|
||||
webpageAsset.sessionId = sessionId
|
||||
|
||||
output.append(webpageAsset)
|
||||
}
|
||||
|
||||
if let image = input[AssetKeys.images][AssetKeys.shelf].string {
|
||||
let imageAsset = SessionAsset()
|
||||
imageAsset.rawAssetType = SessionAssetType.image.rawValue
|
||||
imageAsset.remoteURL = image
|
||||
imageAsset.year = year
|
||||
imageAsset.sessionId = sessionId
|
||||
|
||||
output.append(imageAsset)
|
||||
}
|
||||
|
||||
return .success(output)
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -75,9 +75,14 @@ public class SessionInstance: Object {
|
||||
/// Room name
|
||||
public dynamic var roomName = ""
|
||||
|
||||
/// Room unique identifier
|
||||
public dynamic var roomIdentifier = ""
|
||||
|
||||
// Track name
|
||||
public dynamic var trackName = ""
|
||||
|
||||
public dynamic var trackIdentifier = ""
|
||||
|
||||
/// The track associated with the instance
|
||||
public let track = LinkingObjects(fromType: Track.self, property: "instances")
|
||||
|
||||
@@ -123,8 +128,10 @@ public class SessionInstance: Object {
|
||||
self.sessionType = other.sessionType
|
||||
self.startTime = other.startTime
|
||||
self.endTime = other.endTime
|
||||
self.roomName = other.roomName
|
||||
self.roomIdentifier = other.roomIdentifier
|
||||
self.trackName = other.trackName
|
||||
self.trackIdentifier = other.trackIdentifier
|
||||
self.eventIdentifier = other.eventIdentifier
|
||||
|
||||
if let otherSession = other.session {
|
||||
self.session = realm.object(ofType: Session.self, forPrimaryKey: otherSession.identifier)
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
private enum SessionInstanceKeys: String, JSONSubscriptType {
|
||||
case id, track, room, keywords, startGMT, endGMT, type
|
||||
enum SessionInstanceKeys: String, JSONSubscriptType {
|
||||
case id, keywords, startTime, endTime, type
|
||||
case favId = "fav_id"
|
||||
case room = "roomId"
|
||||
case track = "trackId"
|
||||
|
||||
var jsonKey: JSONKey {
|
||||
return JSONKey.key(rawValue)
|
||||
@@ -28,36 +30,33 @@ final class SessionInstancesJSONAdapter: Adapter {
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
guard let id = input[SessionInstanceKeys.id].string else {
|
||||
return .error(.missingKey(SessionInstanceKeys.id))
|
||||
// not an instance
|
||||
guard session.year == Calendar.current.component(.year, from: Date()) else {
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
guard let trackName = input[SessionInstanceKeys.track].string else {
|
||||
return .error(.missingKey(SessionInstanceKeys.track))
|
||||
guard let startGMT = input[SessionInstanceKeys.startTime].string else {
|
||||
return .error(.missingKey(SessionInstanceKeys.startTime))
|
||||
}
|
||||
|
||||
guard let roomName = input[SessionInstanceKeys.room].string else {
|
||||
return .error(.missingKey(SessionInstanceKeys.room))
|
||||
}
|
||||
|
||||
guard let startGMT = input[SessionInstanceKeys.startGMT].string else {
|
||||
return .error(.missingKey(SessionInstanceKeys.startGMT))
|
||||
}
|
||||
|
||||
guard let endGMT = input[SessionInstanceKeys.endGMT].string else {
|
||||
return .error(.missingKey(SessionInstanceKeys.endGMT))
|
||||
guard let endGMT = input[SessionInstanceKeys.endTime].string else {
|
||||
return .error(.missingKey(SessionInstanceKeys.startTime))
|
||||
}
|
||||
|
||||
guard let rawType = input[SessionInstanceKeys.type].string else {
|
||||
return .error(.missingKey(SessionInstanceKeys.type))
|
||||
}
|
||||
|
||||
guard let keywordsJson = input[SessionInstanceKeys.keywords].array else {
|
||||
return .error(.missingKey(SessionInstanceKeys.keywords))
|
||||
|
||||
guard let id = input[SessionInstanceKeys.id].string else {
|
||||
return .error(.missingKey(SessionInstanceKeys.id))
|
||||
}
|
||||
|
||||
guard case .success(let keywords) = KeywordsJSONAdapter().adapt(keywordsJson) else {
|
||||
return .error(.invalidData)
|
||||
|
||||
guard let roomIdentifier = input[SessionInstanceKeys.room].int else {
|
||||
return .error(.missingKey(SessionInstanceKeys.room))
|
||||
}
|
||||
|
||||
guard let trackIdentifier = input[SessionInstanceKeys.track].int else {
|
||||
return .error(.missingKey(SessionInstanceKeys.track))
|
||||
}
|
||||
|
||||
guard case .success(let startDate) = DateTimeAdapter().adapt(startGMT) else {
|
||||
@@ -70,17 +69,22 @@ final class SessionInstancesJSONAdapter: Adapter {
|
||||
|
||||
let instance = SessionInstance()
|
||||
|
||||
if let keywordsJson = input[SessionInstanceKeys.keywords].array {
|
||||
if case .success(let keywords) = KeywordsJSONAdapter().adapt(keywordsJson) {
|
||||
instance.keywords.append(objectsIn: keywords)
|
||||
}
|
||||
}
|
||||
|
||||
instance.identifier = session.identifier
|
||||
instance.eventIdentifier = Event.identifier(from: startDate)
|
||||
instance.number = id
|
||||
instance.session = session
|
||||
instance.trackName = trackName
|
||||
instance.roomName = roomName
|
||||
instance.trackIdentifier = "\(trackIdentifier)"
|
||||
instance.roomIdentifier = "\(roomIdentifier)"
|
||||
instance.rawSessionType = rawType
|
||||
instance.sessionType = SessionInstanceType(rawSessionType: rawType)?.rawValue ?? 0
|
||||
instance.startTime = startDate
|
||||
instance.endTime = endDate
|
||||
instance.keywords.append(objectsIn: keywords)
|
||||
|
||||
return .success(instance)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,18 @@
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
enum AssetKeys: String, JSONSubscriptType {
|
||||
case id, year, title, downloadHD, downloadSD, slides, hls, images, shelf
|
||||
|
||||
var jsonKey: JSONKey {
|
||||
return JSONKey.key(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
private enum SessionKeys: String, JSONSubscriptType {
|
||||
case id, year, title, track, focus, description, startGMT
|
||||
case id, year, title, platforms, description, startTime, eventContentId, eventId, media, webPermalink, staticContentId
|
||||
|
||||
case track = "trackId"
|
||||
|
||||
var jsonKey: JSONKey {
|
||||
return JSONKey.key(rawValue)
|
||||
@@ -23,24 +33,15 @@ final class SessionsJSONAdapter: Adapter {
|
||||
typealias OutputType = Session
|
||||
|
||||
func adapt(_ input: JSON) -> Result<Session, AdapterError> {
|
||||
guard let id = input[SessionKeys.id].string else {
|
||||
guard let id = input[SessionKeys.id].string?.replacingOccurrences(of: "wwdc", with: "") else {
|
||||
return .error(.missingKey(SessionKeys.id))
|
||||
}
|
||||
|
||||
var eventYear = ""
|
||||
|
||||
if let year = input[SessionKeys.year].int {
|
||||
eventYear = "\(year)"
|
||||
} else if let startGMT = input[SessionKeys.startGMT].string {
|
||||
guard let year = startGMT.components(separatedBy: "-").first else {
|
||||
return .error(.missingKey(SessionKeys.year))
|
||||
}
|
||||
|
||||
eventYear = year
|
||||
guard let eventIdentifier = input[SessionKeys.eventId].string else {
|
||||
return .error(.missingKey(SessionKeys.eventId))
|
||||
}
|
||||
|
||||
let identifier = "\(eventYear)-\(id)"
|
||||
let eventIdentifier = "wwdc\(eventYear)"
|
||||
let eventYear = eventIdentifier.replacingOccurrences(of: "wwdc", with: "")
|
||||
|
||||
guard let title = input[SessionKeys.title].string else {
|
||||
return .error(.missingKey(SessionKeys.title))
|
||||
@@ -50,27 +51,87 @@ final class SessionsJSONAdapter: Adapter {
|
||||
return .error(.missingKey(SessionKeys.description))
|
||||
}
|
||||
|
||||
guard let trackName = input[SessionKeys.track].string else {
|
||||
guard let trackIdentifier = input[SessionKeys.track].int else {
|
||||
return .error(.missingKey(SessionKeys.track))
|
||||
}
|
||||
|
||||
guard let focusesJson = input[SessionKeys.focus].array else {
|
||||
return .error(.missingKey(SessionKeys.focus))
|
||||
}
|
||||
|
||||
guard case .success(let focuses) = FocusesJSONAdapter().adapt(focusesJson) else {
|
||||
return .error(.invalidData)
|
||||
guard let eventContentId = input[SessionKeys.eventContentId].int else {
|
||||
return .error(.missingKey(SessionKeys.eventContentId))
|
||||
}
|
||||
|
||||
let session = Session()
|
||||
|
||||
session.identifier = identifier
|
||||
if let focusesJson = input[SessionKeys.platforms].array {
|
||||
if case .success(let focuses) = FocusesJSONAdapter().adapt(focusesJson) {
|
||||
session.focuses.append(objectsIn: focuses)
|
||||
}
|
||||
}
|
||||
|
||||
if let url = input[SessionKeys.media][AssetKeys.hls].string {
|
||||
let streaming = SessionAsset()
|
||||
|
||||
streaming.rawAssetType = SessionAssetType.streamingVideo.rawValue
|
||||
streaming.remoteURL = url
|
||||
streaming.year = Int(eventYear) ?? -1
|
||||
streaming.sessionId = id
|
||||
|
||||
session.assets.append(streaming)
|
||||
}
|
||||
|
||||
if let hd = input[SessionKeys.media][AssetKeys.downloadHD].string {
|
||||
let hdVideo = SessionAsset()
|
||||
hdVideo.rawAssetType = SessionAssetType.hdVideo.rawValue
|
||||
hdVideo.remoteURL = hd
|
||||
hdVideo.year = Int(eventYear) ?? -1
|
||||
hdVideo.sessionId = id
|
||||
|
||||
let filename = URL(string: hd)?.lastPathComponent ?? "\(title).mp4"
|
||||
hdVideo.relativeLocalURL = "\(eventYear)/\(filename)"
|
||||
|
||||
session.assets.append(hdVideo)
|
||||
}
|
||||
|
||||
if let sd = input[SessionKeys.media][AssetKeys.downloadSD].string {
|
||||
let sdVideo = SessionAsset()
|
||||
sdVideo.rawAssetType = SessionAssetType.sdVideo.rawValue
|
||||
sdVideo.remoteURL = sd
|
||||
sdVideo.year = Int(eventYear) ?? -1
|
||||
sdVideo.sessionId = id
|
||||
|
||||
let filename = URL(string: sd)?.lastPathComponent ?? "\(title).mp4"
|
||||
sdVideo.relativeLocalURL = "\(eventYear)/\(filename)"
|
||||
|
||||
session.assets.append(sdVideo)
|
||||
}
|
||||
|
||||
if let slides = input[SessionKeys.media][AssetKeys.slides].string {
|
||||
let slidesAsset = SessionAsset()
|
||||
slidesAsset.rawAssetType = SessionAssetType.slides.rawValue
|
||||
slidesAsset.remoteURL = slides
|
||||
slidesAsset.year = Int(eventYear) ?? -1
|
||||
slidesAsset.sessionId = id
|
||||
|
||||
session.assets.append(slidesAsset)
|
||||
}
|
||||
|
||||
if let permalink = input[SessionKeys.webPermalink].string {
|
||||
let webPageAsset = SessionAsset()
|
||||
webPageAsset.rawAssetType = SessionAssetType.webpage.rawValue
|
||||
webPageAsset.remoteURL = permalink
|
||||
webPageAsset.year = Int(eventYear) ?? -1
|
||||
webPageAsset.sessionId = id
|
||||
|
||||
session.assets.append(webPageAsset)
|
||||
}
|
||||
|
||||
session.staticContentId = "\(input[SessionKeys.staticContentId].intValue)"
|
||||
session.identifier = id
|
||||
session.year = Int(eventYear) ?? -1
|
||||
session.number = id
|
||||
session.number = "\(eventContentId)"
|
||||
session.title = title
|
||||
session.summary = summary
|
||||
session.trackName = trackName
|
||||
session.focuses.append(objectsIn: focuses)
|
||||
session.trackIdentifier = "\(trackIdentifier)"
|
||||
|
||||
session.eventIdentifier = eventIdentifier
|
||||
|
||||
return .success(session)
|
||||
|
||||
@@ -23,44 +23,45 @@ final class SessionsResponseAdapter: Adapter {
|
||||
typealias OutputType = SessionsResponse
|
||||
|
||||
func adapt(_ input: JSON) -> Result<SessionsResponse, AdapterError> {
|
||||
guard let eventsJson = input[SessionResponseKeys.events].array else {
|
||||
return .error(.missingKey(SessionResponseKeys.events))
|
||||
}
|
||||
|
||||
guard let sessionsJson = input[SessionResponseKeys.sessions].array else {
|
||||
return .error(.missingKey(SessionResponseKeys.sessions))
|
||||
}
|
||||
|
||||
guard case .success(let events) = EventsJSONAdapter().adapt(eventsJson) else {
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
guard case .success(let sessions) = SessionsJSONAdapter().adapt(sessionsJson) else {
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
guard case .success(let assets) = SessionAssetsJSONAdapter().adapt(sessionsJson) else {
|
||||
return .error(.invalidData)
|
||||
}
|
||||
|
||||
var trackNames = Set<String>()
|
||||
|
||||
sessions.forEach { session in
|
||||
guard !trackNames.contains(session.trackName) else { return }
|
||||
trackNames.insert(session.trackName)
|
||||
}
|
||||
|
||||
let tracks: [Track] = trackNames.map { name in
|
||||
let track = Track()
|
||||
|
||||
track.name = name
|
||||
|
||||
return track
|
||||
}
|
||||
|
||||
let response = SessionsResponse(events: events, sessions: sessions, assets: assets.flatMap({$0}), tracks: tracks)
|
||||
|
||||
return .success(response)
|
||||
return .error(.invalidData)
|
||||
// guard let eventsJson = input[SessionResponseKeys.events].array else {
|
||||
// return .error(.missingKey(SessionResponseKeys.events))
|
||||
// }
|
||||
//
|
||||
// guard let sessionsJson = input[SessionResponseKeys.sessions].array else {
|
||||
// return .error(.missingKey(SessionResponseKeys.sessions))
|
||||
// }
|
||||
//
|
||||
// guard case .success(let events) = EventsJSONAdapter().adapt(eventsJson) else {
|
||||
// return .error(.invalidData)
|
||||
// }
|
||||
//
|
||||
// guard case .success(let sessions) = SessionsJSONAdapter().adapt(sessionsJson) else {
|
||||
// return .error(.invalidData)
|
||||
// }
|
||||
//
|
||||
// guard case .success(let assets) = SessionAssetsJSONAdapter().adapt(sessionsJson) else {
|
||||
// return .error(.invalidData)
|
||||
// }
|
||||
//
|
||||
// var trackNames = Set<String>()
|
||||
//
|
||||
// sessions.forEach { session in
|
||||
// guard !trackNames.contains(session.trackName) else { return }
|
||||
// trackNames.insert(session.trackName)
|
||||
// }
|
||||
//
|
||||
// let tracks: [Track] = trackNames.map { name in
|
||||
// let track = Track()
|
||||
//
|
||||
// track.name = name
|
||||
//
|
||||
// return track
|
||||
// }
|
||||
//
|
||||
// let response = SessionsResponse(events: events, sessions: sessions, assets: assets.flatMap({$0}), tracks: tracks)
|
||||
//
|
||||
// return .success(response)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -103,15 +103,12 @@ public final class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
func store(sessionsResult: Result<SessionsResponse, APIError>, scheduleResult: Result<ScheduleResponse, APIError>, completion: @escaping () -> Void) {
|
||||
if case let .error(sessionsError) = sessionsResult {
|
||||
print("Error downloading sessions: \(sessionsError)")
|
||||
}
|
||||
if case let .error(scheduleError) = scheduleResult {
|
||||
print("Error downloading schedule: \(scheduleError)")
|
||||
func store(contentResult: Result<ContentsResponse, APIError>, completion: @escaping () -> Void) {
|
||||
if case let .error(error) = contentResult {
|
||||
print("Error downloading sessions: \(error)")
|
||||
}
|
||||
|
||||
guard case let .success(sessionsResponse) = sessionsResult, case let .success(scheduleResponse) = scheduleResult else {
|
||||
guard case let .success(sessionsResponse) = contentResult else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -139,32 +136,8 @@ public final class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
// Associate assets with sessions
|
||||
consolidatedSessions.forEach { session in
|
||||
autoreleasepool {
|
||||
let components = session.identifier.components(separatedBy: "-")
|
||||
guard components.count == 2 else { return }
|
||||
guard let year = Int(components[0]) else { return }
|
||||
|
||||
session.assets.removeAll()
|
||||
|
||||
// Merge assets, preserving user-defined data
|
||||
let assets = sessionsResponse.assets.filter({ $0.year == year && $0.sessionId == components[1] }).map { newAsset -> (SessionAsset) in
|
||||
if let existingAsset = self.realm.object(ofType: SessionAsset.self, forPrimaryKey: newAsset.identifier) {
|
||||
existingAsset.merge(with: newAsset, in: self.realm)
|
||||
|
||||
return existingAsset
|
||||
} else {
|
||||
return newAsset
|
||||
}
|
||||
}
|
||||
|
||||
session.assets.append(objectsIn: assets)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge existing instance data, preserving user-defined data
|
||||
scheduleResponse.instances.forEach { newInstance in
|
||||
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)
|
||||
@@ -177,8 +150,8 @@ public final class Storage {
|
||||
}
|
||||
|
||||
// Save everything
|
||||
self.realm.add(scheduleResponse.rooms, update: true)
|
||||
self.realm.add(scheduleResponse.tracks, update: true)
|
||||
self.realm.add(sessionsResponse.rooms, update: true)
|
||||
self.realm.add(sessionsResponse.tracks, update: true)
|
||||
self.realm.add(sessionsResponse.events, update: true)
|
||||
|
||||
do {
|
||||
@@ -195,8 +168,9 @@ public final class Storage {
|
||||
|
||||
// add instances to rooms
|
||||
targetRealm.objects(Room.self).forEach { room in
|
||||
let instances = targetRealm.objects(SessionInstance.self).filter("roomName == %@", room.name)
|
||||
let instances = targetRealm.objects(SessionInstance.self).filter("roomIdentifier == %@", room.identifier)
|
||||
|
||||
instances.forEach({ $0.roomName = room.name })
|
||||
room.instances.append(objectsIn: instances)
|
||||
}
|
||||
|
||||
@@ -211,11 +185,13 @@ public final class Storage {
|
||||
|
||||
// add instances and sessions to tracks
|
||||
targetRealm.objects(Track.self).forEach { track in
|
||||
let instances = targetRealm.objects(SessionInstance.self).filter("trackName == %@", track.name)
|
||||
let sessions = targetRealm.objects(Session.self).filter("trackName == %@", track.name)
|
||||
let instances = targetRealm.objects(SessionInstance.self).filter("trackIdentifier == %@", track.identifier)
|
||||
let sessions = targetRealm.objects(Session.self).filter("trackIdentifier == %@", track.identifier)
|
||||
|
||||
track.instances.append(objectsIn: instances)
|
||||
track.sessions.append(objectsIn: sessions)
|
||||
|
||||
instances.forEach({ $0.trackName = track.name })
|
||||
}
|
||||
|
||||
// add live video assets to sessions
|
||||
|
||||
@@ -41,15 +41,11 @@ public final class SyncEngine {
|
||||
}
|
||||
}
|
||||
|
||||
public func syncSessionsAndSchedule() {
|
||||
client.fetchSessions { [weak self] sessionsResult in
|
||||
public func syncContent() {
|
||||
client.fetchContent { [unowned self] scheduleResult in
|
||||
DispatchQueue.main.async {
|
||||
self?.client.fetchSchedule { scheduleResult in
|
||||
DispatchQueue.main.async {
|
||||
self?.storage.store(sessionsResult: sessionsResult, scheduleResult: scheduleResult) {
|
||||
NotificationCenter.default.post(name: .SyncEngineDidSyncSessionsAndSchedule, object: self)
|
||||
}
|
||||
}
|
||||
self.storage.store(contentResult: scheduleResult) {
|
||||
NotificationCenter.default.post(name: .SyncEngineDidSyncSessionsAndSchedule, object: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import RealmSwift
|
||||
/// Tracks represent a specific are of interest (ex: "System Frameworks", "Graphics and Games")
|
||||
public class Track: Object {
|
||||
|
||||
/// Unique identifier
|
||||
public dynamic var identifier = ""
|
||||
|
||||
/// The name of the track
|
||||
public dynamic var name = ""
|
||||
|
||||
@@ -47,13 +50,15 @@ public class Track: Object {
|
||||
]
|
||||
}
|
||||
|
||||
public static func make(name: String,
|
||||
darkColor: String,
|
||||
lightBackgroundColor: String,
|
||||
lightColor: String,
|
||||
titleColor: String) -> Track {
|
||||
public static func make(identifier: String,
|
||||
name: String,
|
||||
darkColor: String,
|
||||
lightBackgroundColor: String,
|
||||
lightColor: String,
|
||||
titleColor: String) -> Track {
|
||||
let track = Track()
|
||||
|
||||
track.identifier = identifier
|
||||
track.name = name
|
||||
track.darkColor = darkColor
|
||||
track.lightBackgroundColor = lightBackgroundColor
|
||||
|
||||
@@ -10,7 +10,8 @@ import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
private enum TrackKeys: String, JSONSubscriptType {
|
||||
case name, color, darkColor, titleColor, lightBGColor
|
||||
case name, color, darkColor, titleColor, lightBGColor, ordinal
|
||||
case identifier = "id"
|
||||
|
||||
var jsonKey: JSONKey {
|
||||
return JSONKey.key(rawValue)
|
||||
@@ -23,6 +24,10 @@ final class TracksJSONAdapter: Adapter {
|
||||
typealias OutputType = Track
|
||||
|
||||
func adapt(_ input: JSON) -> Result<Track, AdapterError> {
|
||||
guard let identifier = input[TrackKeys.identifier].int else {
|
||||
return .error(.missingKey(TrackKeys.identifier))
|
||||
}
|
||||
|
||||
guard let name = input[TrackKeys.name].string else {
|
||||
return .error(.missingKey(TrackKeys.name))
|
||||
}
|
||||
@@ -43,12 +48,15 @@ final class TracksJSONAdapter: Adapter {
|
||||
return .error(.missingKey(TrackKeys.lightBGColor))
|
||||
}
|
||||
|
||||
let track = Track.make(name: name,
|
||||
let track = Track.make(identifier: "\(identifier)",
|
||||
name: name,
|
||||
darkColor: darkColor,
|
||||
lightBackgroundColor: lightBGColor,
|
||||
lightColor: color,
|
||||
titleColor: titleColor)
|
||||
|
||||
track.order = input[TrackKeys.ordinal].intValue
|
||||
|
||||
return .success(track)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user