diff --git a/Music Tools/Application/SceneDelegate.swift b/Music Tools/Application/SceneDelegate.swift index 8f3a79f..627d7d1 100644 --- a/Music Tools/Application/SceneDelegate.swift +++ b/Music Tools/Application/SceneDelegate.swift @@ -30,7 +30,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var controller: UIViewController if keychain["username"] != nil && keychain["password"] != nil { - let liveUser = LiveUser(playlists: [], tags: [], username: keychain["username"]!) + let liveUser = LiveUser(playlists: [], tags: [], username: keychain["username"]!).loadUserDefaults() controller = UIHostingController(rootView: contentView.environmentObject(liveUser)) } else { let storyboard = UIStoryboard(name: "Main", bundle: nil) diff --git a/Music Tools/Model/LiveUser.swift b/Music Tools/Model/LiveUser.swift index e212c75..aa2d679 100644 --- a/Music Tools/Model/LiveUser.swift +++ b/Music Tools/Model/LiveUser.swift @@ -7,6 +7,8 @@ // import Foundation +import Alamofire +import SwiftyJSON class LiveUser: ObservableObject { @@ -26,4 +28,80 @@ class LiveUser: ObservableObject { } self.playlists[index] = playlistIn } + + func refreshPlaylists() { + let api = PlaylistApi.getPlaylists + RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in + + guard let data = response.data else { + fatalError("error getting playlists") + } + + guard let json = try? JSON(data: data) else { + fatalError("error parsing reponse") + } + + let playlists = json["playlists"].arrayValue + + // update state + self.playlists = PlaylistApi.fromJSON(playlist: playlists).sorted(by: { $0.name.lowercased() < $1.name.lowercased() }) + + let encoder = JSONEncoder() + let defaults = UserDefaults.standard + do { + defaults.set(String(data: try encoder.encode(playlists), encoding: .utf8), forKey: "playlists") + } catch { + print("error encoding playlists: \(error)") + } + } + } + + func refreshTags() { + let api = TagApi.getTags + RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in + + guard let data = response.data else { + fatalError("error getting tags") + } + + guard let json = try? JSON(data: data) else { + fatalError("error parsing reponse") + } + + let tags = json["tags"].arrayValue + + // update state + self.tags = TagApi.fromJSON(tag: tags).sorted(by: { $0.name.lowercased() < $1.name.lowercased() }) + + let encoder = JSONEncoder() + let defaults = UserDefaults.standard + do { + defaults.set(String(data: try encoder.encode(tags), encoding: .utf8), forKey: "tags") + } catch { + print("error encoding tags: \(error)") + } + } + } + + func loadUserDefaults() -> LiveUser { + let defaults = UserDefaults.standard + let decoder = JSONDecoder() + + let _strPlaylists = defaults.string(forKey: "playlists") + let _strTags = defaults.string(forKey: "tags") + + do { + if let _strPlaylists = _strPlaylists { + self.playlists = (try decoder.decode([Playlist].self, from: _strPlaylists.data(using: .utf8)!)).sorted(by: { $0.name.lowercased() < $1.name.lowercased() }) + } + + if let _strTags = _strTags { + self.tags = (try decoder.decode([Tag].self, from: _strTags.data(using: .utf8)!)).sorted(by: { $0.name.lowercased() < $1.name.lowercased() }) + } + } catch { + print("error decoding: \(error)") + } + + return self + } } diff --git a/Music Tools/Model/Playlist.swift b/Music Tools/Model/Playlist.swift index 83575ac..792d19d 100644 --- a/Music Tools/Model/Playlist.swift +++ b/Music Tools/Model/Playlist.swift @@ -6,16 +6,17 @@ // Copyright © 2020 Sarsoo. All rights reserved. // +import Foundation import UIKit import SwiftyJSON -class Playlist: Identifiable, Equatable { +class Playlist: Identifiable, Equatable, Codable { //MARK: Properties var name: String var uri: String - var username: String + var username: String? var include_recommendations: Bool var recommendation_sample: Int @@ -23,89 +24,111 @@ class Playlist: Identifiable, Equatable { var parts: Array var playlist_references: Array - var shuffle: Bool + var sort: String + var description_overwrite: String? + var description_suffix: String? + + var last_updated: String + + var lastfm_stat_count: Int + var lastfm_stat_album_count: Int + var lastfm_stat_artist_count: Int + + var lastfm_stat_percent: Float + var lastfm_stat_album_percent: Float + var lastfm_stat_artist_percent: Float + + var lastfm_stat_last_refresh: String + + private enum CodingKeys: String, CodingKey { + case name + case uri + case username + + case include_recommendations + case recommendation_sample + case include_library_tracks + + case parts + case playlist_references + case shuffle + + case sort + case description_overwrite + case description_suffix + + case last_updated + + case lastfm_stat_count + case lastfm_stat_album_count + case lastfm_stat_artist_count + + case lastfm_stat_percent + case lastfm_stat_album_percent + case lastfm_stat_artist_percent + + case lastfm_stat_last_refresh + } + //MARK: Initialization init(name: String, - uri: String, - username: String, + uri: String = "spotify::", + username: String = "NO USER", + + include_recommendations: Bool = false, + recommendation_sample: Int = 0, + include_library_tracks: Bool = false, + + parts: Array = [], + playlist_references: Array = [], + shuffle: Bool = false, - include_recommendations: Bool, - recommendation_sample: Int, - include_library_tracks: Bool, + sort: String = "NO SORT", + description_overwrite: String? = nil, + description_suffix: String? = nil, - parts: Array, - playlist_references: Array, + last_updated: String = "", - shuffle: Bool){ - + lastfm_stat_count: Int = 0, + lastfm_stat_album_count: Int = 0, + lastfm_stat_artist_count: Int = 0, + + lastfm_stat_percent: Float = 0, + lastfm_stat_album_percent: Float = 0, + lastfm_stat_artist_percent: Float = 0, + + lastfm_stat_last_refresh: String = ""){ + self.name = name self.uri = uri self.username = username - + self.include_recommendations = include_recommendations self.recommendation_sample = recommendation_sample self.include_library_tracks = include_library_tracks - + self.parts = parts self.playlist_references = playlist_references - self.shuffle = shuffle - } - - static func fromDict(dictionary: JSON) -> Playlist? { - switch dictionary["type"].string { - case "default": - return Playlist(name: dictionary["name"].stringValue, - uri: dictionary["uri"].stringValue, - username: dictionary["username"].stringValue, - - include_recommendations: dictionary["include_recommendations"].boolValue, - recommendation_sample: dictionary["recommendation_sample"].intValue, - include_library_tracks: dictionary["include_library_tracks"].boolValue, - parts: dictionary["parts"].arrayObject as! Array, - playlist_references: dictionary["playlist_references"].arrayObject as! Array, - - shuffle: dictionary["shuffle"].boolValue) - case "recents": - return RecentsPlaylist(name: dictionary["name"].stringValue, - uri: dictionary["uri"].stringValue, - username: dictionary["username"].stringValue, - - include_recommendations: dictionary["include_recommendations"].boolValue, - recommendation_sample: dictionary["recommendation_sample"].intValue, - include_library_tracks: dictionary["include_library_tracks"].boolValue, + self.sort = sort + self.description_overwrite = description_overwrite + self.description_suffix = description_suffix - parts: dictionary["parts"].arrayObject as! Array, - playlist_references: dictionary["playlist_references"].arrayObject as! Array, - - shuffle: dictionary["shuffle"].boolValue, - - add_last_month: dictionary["add_last_month"].boolValue, - add_this_month: dictionary["add_this_month"].boolValue, - day_boundary: dictionary["day_boundary"].intValue) - case "fmchart": - return LastFMChartPlaylist(name: dictionary["name"].stringValue, - uri: dictionary["uri"].stringValue, - username: dictionary["username"].stringValue, - - include_recommendations: dictionary["include_recommendations"].boolValue, - recommendation_sample: dictionary["recommendation_sample"].intValue, - include_library_tracks: dictionary["include_library_tracks"].boolValue, + self.last_updated = last_updated - parts: dictionary["parts"].arrayObject as! Array, - playlist_references: dictionary["playlist_references"].arrayObject as! Array, - - shuffle: dictionary["shuffle"].boolValue, - - chart_range: LastFmRange(rawValue: dictionary["chart_range"].stringValue)!, - chart_limit: dictionary["chart_limit"].intValue) - default: - return nil - } + self.lastfm_stat_count = lastfm_stat_count + self.lastfm_stat_album_count = lastfm_stat_album_count + self.lastfm_stat_artist_count = lastfm_stat_artist_count + + self.lastfm_stat_percent = lastfm_stat_percent + self.lastfm_stat_album_percent = lastfm_stat_album_percent + self.lastfm_stat_artist_percent = lastfm_stat_artist_percent + + self.lastfm_stat_last_refresh = lastfm_stat_last_refresh } var link: String { @@ -117,6 +140,38 @@ class Playlist: Identifiable, Equatable { return lhs.name == rhs.name // && lhs.username == rhs.username } + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + name = try container.decode(String.self, forKey: .name) + uri = try container.decode(String.self, forKey: .uri) +// username = try container.decode(String.self, forKey: .username) + + include_recommendations = try container.decode(Bool.self, forKey: .include_recommendations) + recommendation_sample = try container.decode(Int.self, forKey: .recommendation_sample) + include_library_tracks = try container.decode(Bool.self, forKey: .include_library_tracks) + + parts = try container.decode([String].self, forKey: .parts) + playlist_references = try container.decode([String].self, forKey: .playlist_references) + shuffle = try container.decode(Bool.self, forKey: .shuffle) + + sort = try container.decode(String.self, forKey: .sort) +// description_overwrite = try container.decode(String.self, forKey: .description_overwrite) +// description_suffix = try container.decode(String.self, forKey: .description_suffix) + + last_updated = try container.decode(String.self, forKey: .last_updated) + + lastfm_stat_count = try container.decode(Int.self, forKey: .lastfm_stat_count) + lastfm_stat_album_count = try container.decode(Int.self, forKey: .lastfm_stat_album_count) + lastfm_stat_artist_count = try container.decode(Int.self, forKey: .lastfm_stat_artist_count) + + lastfm_stat_percent = try container.decode(Float.self, forKey: .lastfm_stat_percent) + lastfm_stat_album_percent = try container.decode(Float.self, forKey: .lastfm_stat_album_percent) + lastfm_stat_artist_percent = try container.decode(Float.self, forKey: .lastfm_stat_artist_percent) + + lastfm_stat_last_refresh = try container.decode(String.self, forKey: .lastfm_stat_last_refresh) + + } } @@ -128,34 +183,36 @@ class RecentsPlaylist: Playlist { var add_this_month: Bool var day_boundary: Int + private enum CodingKeys: String, CodingKey { case add_last_month; case add_this_month; case day_boundary } + //MARK: Initialization init(name: String, - uri: String, - username: String, + username: String = "NO USER", - include_recommendations: Bool, - recommendation_sample: Int, - include_library_tracks: Bool, - - parts: Array, - playlist_references: Array, - - shuffle: Bool, - - add_last_month: Bool, - add_this_month: Bool, - day_boundary: Int){ - + add_last_month: Bool = false, + add_this_month: Bool = false, + day_boundary: Int = 14){ + self.add_last_month = add_last_month self.add_this_month = add_this_month self.day_boundary = day_boundary - super.init(name: name, uri: uri, username: username, include_recommendations: include_recommendations, recommendation_sample: recommendation_sample, include_library_tracks: include_library_tracks, parts: parts, playlist_references: playlist_references, shuffle: shuffle) + super.init(name: name, username: username) + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + add_last_month = try container.decode(Bool.self, forKey: .add_last_month) + add_this_month = try container.decode(Bool.self, forKey: .add_this_month) + day_boundary = try container.decode(Int.self, forKey: .day_boundary) + + try super.init(from: decoder) } } -enum LastFmRange: String { +enum LastFmRange: String, Decodable { case overall = "OVERALL" case week = "WEEK" case month = "MONTH" @@ -171,27 +228,28 @@ class LastFMChartPlaylist: Playlist { var chart_range: LastFmRange var chart_limit: Int + private enum CodingKeys: String, CodingKey { case chart_range; case chart_limit } + //MARK: Initialization init(name: String, - uri: String, - username: String, + username: String = "NO USER", - include_recommendations: Bool, - recommendation_sample: Int, - include_library_tracks: Bool, - - parts: Array, - playlist_references: Array, - - shuffle: Bool, - - chart_range: LastFmRange, - chart_limit: Int){ - + chart_range: LastFmRange = .overall, + chart_limit: Int = 10){ + self.chart_range = chart_range self.chart_limit = chart_limit - super.init(name: name, uri: uri, username: username, include_recommendations: include_recommendations, recommendation_sample: recommendation_sample, include_library_tracks: include_library_tracks, parts: parts, playlist_references: playlist_references, shuffle: shuffle) + super.init(name: name, username: username) + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + chart_range = try LastFmRange(rawValue: container.decode(String.self, forKey: .chart_range))! + chart_limit = try container.decode(Int.self, forKey: .chart_limit) + + try super.init(from: decoder) } } diff --git a/Music Tools/Model/Tag.swift b/Music Tools/Model/Tag.swift index 981445c..a0ccca9 100644 --- a/Music Tools/Model/Tag.swift +++ b/Music Tools/Model/Tag.swift @@ -9,7 +9,7 @@ import UIKit import SwiftyJSON -class Tag: Identifiable, Equatable { +class Tag: Identifiable, Equatable, Decodable { //MARK: Properties @@ -32,48 +32,32 @@ class Tag: Identifiable, Equatable { init(tag_id: String, name: String, username: String, - + tracks: [JSON], albums: [JSON], artists: [JSON], - + count: Int, proportion: Double, total_user_scrobbles: Int, - + last_updated: String){ - + self.tag_id = tag_id self.name = name self.username = username - + self.tracks = tracks self.albums = albums self.artists = artists - + self.count = count self.proportion = proportion self.total_user_scrobbles = total_user_scrobbles - + self.last_updated = last_updated } - static func fromDict(dictionary: JSON) -> Tag { - return Tag(tag_id: dictionary["tag_id"].stringValue, - name: dictionary["name"].stringValue, - username: dictionary["username"].stringValue, - - tracks: dictionary["tracks"].arrayValue, - albums: dictionary["albums"].arrayValue, - artists: dictionary["artists"].arrayValue, - - count: dictionary["count"].intValue, - proportion: dictionary["proportion"].doubleValue, - total_user_scrobbles: dictionary["total_user_scrobbles"].intValue, - - last_updated: dictionary["last_updated"].stringValue) - } - static func == (lhs: Tag, rhs: Tag) -> Bool { return lhs.tag_id == rhs.tag_id // && lhs.username == rhs.username diff --git a/Music Tools/Model/User.swift b/Music Tools/Model/User.swift index 967e9dc..122b827 100644 --- a/Music Tools/Model/User.swift +++ b/Music Tools/Model/User.swift @@ -9,12 +9,12 @@ import UIKit import SwiftyJSON -enum UserType: String { +enum UserType: String, Decodable { case user = "user" case admin = "admin" } -class User: Identifiable { +class User: Identifiable, Decodable { //MARK: Properties @@ -43,16 +43,6 @@ class User: Identifiable { self.last_login = last_login self.spotify_linked = spotify_linked self.lastfm_username = lastfm_username - } - - static func fromDict(dictionary: JSON) -> User { - return User(username: dictionary["username"].stringValue, - email: dictionary["username"].stringValue, - type: UserType(rawValue: dictionary["type"].stringValue) ?? .user, - last_login: dictionary["last_login"].stringValue, - spotify_linked: dictionary["spotify_linked"].boolValue, - lastfm_username: dictionary["lastfm_username"].stringValue) - } - + } } diff --git a/Music Tools/Network/PlaylistApi.swift b/Music Tools/Network/PlaylistApi.swift index 01665b1..6ea9581 100644 --- a/Music Tools/Network/PlaylistApi.swift +++ b/Music Tools/Network/PlaylistApi.swift @@ -110,6 +110,68 @@ extension PlaylistApi: ApiRequest { return ApiRequestDefaults.authMethod } + static func fromJSON(playlist: Data) -> Playlist? { + + let decoder = JSONDecoder() + do { + let json = try JSON(data: playlist) + switch json["type"].string { + case "default": + let playlist = try decoder.decode(Playlist.self, from: playlist) + return playlist + case "recents": + let playlist = try decoder.decode(RecentsPlaylist.self, from: playlist) + return playlist + case "fmchart": + let playlist = try decoder.decode(LastFMChartPlaylist.self, from: playlist) + return playlist + default: + return nil + } + } catch { + print(error) + } + return nil + } + + static func fromJSON(playlist: JSON) -> Playlist? { + + let _json = playlist.rawString()?.data(using: .utf8) + + if let data = _json { + let decoder = JSONDecoder() + do { + switch playlist["type"].string { + case "default": + let playlist = try decoder.decode(Playlist.self, from: data) + return playlist + case "recents": + let playlist = try decoder.decode(RecentsPlaylist.self, from: data) + return playlist + case "fmchart": + let playlist = try decoder.decode(LastFMChartPlaylist.self, from: data) + return playlist + default: + return nil + } + } catch { + print(error) + } + } + print(playlist) + return nil + } + + static func fromJSON(playlist: [JSON]) -> [Playlist] { + var _playlists: [Playlist] = [] + for dict in playlist { + let _iter = self.fromJSON(playlist: dict) + if let returned = _iter { + _playlists.append(returned) + } + } + return _playlists + } } diff --git a/Music Tools/Network/TagApi.swift b/Music Tools/Network/TagApi.swift index b848b64..c4ccbbc 100644 --- a/Music Tools/Network/TagApi.swift +++ b/Music Tools/Network/TagApi.swift @@ -100,6 +100,32 @@ extension TagApi: ApiRequest { return ApiRequestDefaults.authMethod } + static func fromJSON(tag: JSON) -> Tag? { + + let _json = tag.rawString()?.data(using: .utf8) + + if let data = _json { + let decoder = JSONDecoder() + do { + let _tag = try decoder.decode(Tag.self, from: data) + return _tag + } catch { + print(error) + } + } + return nil + } + // TODO this loop could be condensed + static func fromJSON(tag: [JSON]) -> [Tag] { + var _tags: [Tag] = [] + for dict in tag { + let _iter = self.fromJSON(tag: dict) + if let returned = _iter { + _tags.append(returned) + } + } + return _tags + } } diff --git a/Music Tools/Views/Playlist/AddPlaylistSheet.swift b/Music Tools/Views/Playlist/AddPlaylistSheet.swift index 83fec22..337bd78 100644 --- a/Music Tools/Views/Playlist/AddPlaylistSheet.swift +++ b/Music Tools/Views/Playlist/AddPlaylistSheet.swift @@ -77,13 +77,13 @@ struct AddPlaylistSheet: View { var playlist: Playlist? = nil switch PlaylistType(rawValue: selectedType) ?? .defaultPlaylist { case .defaultPlaylist: - playlist = Playlist(name: name, uri: "", username: username, include_recommendations: false, recommendation_sample: 10, include_library_tracks: false, parts: [], playlist_references: [], shuffle: false) + playlist = Playlist(name: name, username: username) break case .recents: - playlist = RecentsPlaylist(name: name, uri: "", username: username, include_recommendations: false, recommendation_sample: 10, include_library_tracks: false, parts: [], playlist_references: [], shuffle: false, add_last_month: false, add_this_month: false, day_boundary: 14) + playlist = RecentsPlaylist(name: name, username: username) break case .fmchart: - playlist = LastFMChartPlaylist(name: name, uri: "", username: username, include_recommendations: false, recommendation_sample: 10, include_library_tracks: false, parts: [], playlist_references: [], shuffle: false, chart_range: .month, chart_limit: 10) + playlist = LastFMChartPlaylist(name: name, username: username) break } diff --git a/Music Tools/Views/Playlist/PlaylistRow.swift b/Music Tools/Views/Playlist/PlaylistRow.swift index 0a7e068..c53a4a4 100644 --- a/Music Tools/Views/Playlist/PlaylistRow.swift +++ b/Music Tools/Views/Playlist/PlaylistRow.swift @@ -46,8 +46,8 @@ struct PlaylistRow: View { struct PlaylistRow_Previews: PreviewProvider { static var previews: some View { - PlaylistRow(playlist: - .constant(Playlist(name: "", uri: "", username: "", include_recommendations: true, recommendation_sample: 1, include_library_tracks: true, parts: [], playlist_references: [], shuffle: true)) - ) + PlaylistView(playlist: .constant( + Playlist(name: "playlist name", username: "username") + )) } } diff --git a/Music Tools/Views/Playlist/PlaylistView.swift b/Music Tools/Views/Playlist/PlaylistView.swift index 196a24e..17a3c3c 100644 --- a/Music Tools/Views/Playlist/PlaylistView.swift +++ b/Music Tools/Views/Playlist/PlaylistView.swift @@ -180,10 +180,7 @@ struct PlaylistView: View { fatalError("error getting playlist") } - guard let json = try? JSON(data: data) else { - fatalError("error parsing reponse") - } - self.playlist = Playlist.fromDict(dictionary: json)! + self.playlist = PlaylistApi.fromJSON(playlist: data)! self.isRefreshing = false } //TODO: do better error checking @@ -193,18 +190,7 @@ struct PlaylistView: View { struct PlaylistView_Previews: PreviewProvider { static var previews: some View { PlaylistView(playlist: .constant( - Playlist(name: "playlist name", - uri: "uri", - username: "username", - - include_recommendations: true, - recommendation_sample: 5, - include_library_tracks: true, - - parts: ["name"], - playlist_references: ["ref name"], - - shuffle: true) + Playlist(name: "playlist name", username: "username") )) } } diff --git a/Music Tools/Views/RootView.swift b/Music Tools/Views/RootView.swift index adaad99..c26aa9a 100644 --- a/Music Tools/Views/RootView.swift +++ b/Music Tools/Views/RootView.swift @@ -44,7 +44,8 @@ struct RootView: View { } } .pullToRefresh(isShowing: $isRefreshingPlaylists) { - self.refreshPlaylists() + self.liveUser.refreshPlaylists() + self.isRefreshingPlaylists = false } .navigationBarTitle(Text("Playlists").font(.title)) @@ -85,7 +86,8 @@ struct RootView: View { } } .pullToRefresh(isShowing: $isRefreshingTags) { - self.refreshTags() + self.liveUser.refreshTags() + self.isRefreshingTags = false } .navigationBarTitle(Text("Tags").font(.title)) @@ -124,61 +126,8 @@ struct RootView: View { } private func fetchAll() { - refreshPlaylists() - refreshTags() - } - - public func refreshPlaylists() { - let api = PlaylistApi.getPlaylists - RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in - - guard let data = response.data else { - fatalError("error getting playlists") - } - - guard let json = try? JSON(data: data) else { - fatalError("error parsing reponse") - } - - let playlists = json["playlists"].arrayValue - // parse playlists - .map({ dict in - Playlist.fromDict(dictionary: dict)! - }) - // sort - .sorted(by: { $0.name.lowercased() < $1.name.lowercased() }) - - // update state - self.liveUser.playlists = playlists - self.isRefreshingPlaylists = false - } - //TODO: do better error checking - } - - public func refreshTags() { - let tagApi = TagApi.getTags - RequestBuilder.buildRequest(apiRequest: tagApi).responseJSON{ response in - - guard let data = response.data else { - fatalError("error getting playlists") - } - - guard let json = try? JSON(data: data) else { - fatalError("error parsing reponse") - } - - let tags = json["tags"].arrayValue - // parse playlists - .map({ dict in - Tag.fromDict(dictionary: dict) - }) - // sort - .sorted(by: { $0.name.lowercased() < $1.name.lowercased() }) - - // update state - self.liveUser.tags = tags - self.isRefreshingTags = false - } + self.liveUser.refreshPlaylists() + self.liveUser.refreshTags() } } diff --git a/Music Tools/Views/Tag/TagView.swift b/Music Tools/Views/Tag/TagView.swift index 3ed8c6c..d68da38 100644 --- a/Music Tools/Views/Tag/TagView.swift +++ b/Music Tools/Views/Tag/TagView.swift @@ -113,7 +113,10 @@ struct TagView: View { guard let json = try? JSON(data: data) else { fatalError("error parsing reponse") } - self.tag = Tag.fromDict(dictionary: json["tag"]) + let _tag = TagApi.fromJSON(tag: json["tag"]) + if let tag = _tag { + self.tag = tag + } self.isRefreshing = false } //TODO: do better error checking