implemented codable for network json decoding and userdefault storage

This commit is contained in:
aj 2020-03-06 23:36:51 +00:00
parent 43de19246e
commit d012566f04
12 changed files with 351 additions and 215 deletions

View File

@ -30,7 +30,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var controller: UIViewController var controller: UIViewController
if keychain["username"] != nil && keychain["password"] != nil { 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)) controller = UIHostingController(rootView: contentView.environmentObject(liveUser))
} else { } else {
let storyboard = UIStoryboard(name: "Main", bundle: nil) let storyboard = UIStoryboard(name: "Main", bundle: nil)

View File

@ -7,6 +7,8 @@
// //
import Foundation import Foundation
import Alamofire
import SwiftyJSON
class LiveUser: ObservableObject { class LiveUser: ObservableObject {
@ -26,4 +28,80 @@ class LiveUser: ObservableObject {
} }
self.playlists[index] = playlistIn 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
}
} }

View File

@ -6,16 +6,17 @@
// Copyright © 2020 Sarsoo. All rights reserved. // Copyright © 2020 Sarsoo. All rights reserved.
// //
import Foundation
import UIKit import UIKit
import SwiftyJSON import SwiftyJSON
class Playlist: Identifiable, Equatable { class Playlist: Identifiable, Equatable, Codable {
//MARK: Properties //MARK: Properties
var name: String var name: String
var uri: String var uri: String
var username: String var username: String?
var include_recommendations: Bool var include_recommendations: Bool
var recommendation_sample: Int var recommendation_sample: Int
@ -23,23 +24,83 @@ class Playlist: Identifiable, Equatable {
var parts: Array<String> var parts: Array<String>
var playlist_references: Array<String> var playlist_references: Array<String>
var shuffle: Bool 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 //MARK: Initialization
init(name: String, init(name: String,
uri: String, uri: String = "spotify::",
username: String, username: String = "NO USER",
include_recommendations: Bool, include_recommendations: Bool = false,
recommendation_sample: Int, recommendation_sample: Int = 0,
include_library_tracks: Bool, include_library_tracks: Bool = false,
parts: Array<String>, parts: Array<String> = [],
playlist_references: Array<String>, playlist_references: Array<String> = [],
shuffle: Bool = false,
shuffle: Bool){ sort: String = "NO SORT",
description_overwrite: String? = nil,
description_suffix: String? = nil,
last_updated: String = "",
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.name = name
self.uri = uri self.uri = uri
@ -51,61 +112,23 @@ class Playlist: Identifiable, Equatable {
self.parts = parts self.parts = parts
self.playlist_references = playlist_references self.playlist_references = playlist_references
self.shuffle = shuffle self.shuffle = shuffle
}
static func fromDict(dictionary: JSON) -> Playlist? { self.sort = sort
switch dictionary["type"].string { self.description_overwrite = description_overwrite
case "default": self.description_suffix = description_suffix
return Playlist(name: dictionary["name"].stringValue,
uri: dictionary["uri"].stringValue,
username: dictionary["username"].stringValue,
include_recommendations: dictionary["include_recommendations"].boolValue, self.last_updated = last_updated
recommendation_sample: dictionary["recommendation_sample"].intValue,
include_library_tracks: dictionary["include_library_tracks"].boolValue,
parts: dictionary["parts"].arrayObject as! Array<String>, self.lastfm_stat_count = lastfm_stat_count
playlist_references: dictionary["playlist_references"].arrayObject as! Array<String>, self.lastfm_stat_album_count = lastfm_stat_album_count
self.lastfm_stat_artist_count = lastfm_stat_artist_count
shuffle: dictionary["shuffle"].boolValue) self.lastfm_stat_percent = lastfm_stat_percent
case "recents": self.lastfm_stat_album_percent = lastfm_stat_album_percent
return RecentsPlaylist(name: dictionary["name"].stringValue, self.lastfm_stat_artist_percent = lastfm_stat_artist_percent
uri: dictionary["uri"].stringValue,
username: dictionary["username"].stringValue,
include_recommendations: dictionary["include_recommendations"].boolValue, self.lastfm_stat_last_refresh = lastfm_stat_last_refresh
recommendation_sample: dictionary["recommendation_sample"].intValue,
include_library_tracks: dictionary["include_library_tracks"].boolValue,
parts: dictionary["parts"].arrayObject as! Array<String>,
playlist_references: dictionary["playlist_references"].arrayObject as! Array<String>,
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,
parts: dictionary["parts"].arrayObject as! Array<String>,
playlist_references: dictionary["playlist_references"].arrayObject as! Array<String>,
shuffle: dictionary["shuffle"].boolValue,
chart_range: LastFmRange(rawValue: dictionary["chart_range"].stringValue)!,
chart_limit: dictionary["chart_limit"].intValue)
default:
return nil
}
} }
var link: String { var link: String {
@ -117,6 +140,38 @@ class Playlist: Identifiable, Equatable {
return lhs.name == rhs.name return lhs.name == rhs.name
// && lhs.username == rhs.username // && 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 add_this_month: Bool
var day_boundary: Int var day_boundary: Int
private enum CodingKeys: String, CodingKey { case add_last_month; case add_this_month; case day_boundary }
//MARK: Initialization //MARK: Initialization
init(name: String, init(name: String,
uri: String, username: String = "NO USER",
username: String,
include_recommendations: Bool, add_last_month: Bool = false,
recommendation_sample: Int, add_this_month: Bool = false,
include_library_tracks: Bool, day_boundary: Int = 14){
parts: Array<String>,
playlist_references: Array<String>,
shuffle: Bool,
add_last_month: Bool,
add_this_month: Bool,
day_boundary: Int){
self.add_last_month = add_last_month self.add_last_month = add_last_month
self.add_this_month = add_this_month self.add_this_month = add_this_month
self.day_boundary = day_boundary 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 overall = "OVERALL"
case week = "WEEK" case week = "WEEK"
case month = "MONTH" case month = "MONTH"
@ -171,27 +228,28 @@ class LastFMChartPlaylist: Playlist {
var chart_range: LastFmRange var chart_range: LastFmRange
var chart_limit: Int var chart_limit: Int
private enum CodingKeys: String, CodingKey { case chart_range; case chart_limit }
//MARK: Initialization //MARK: Initialization
init(name: String, init(name: String,
uri: String, username: String = "NO USER",
username: String,
include_recommendations: Bool, chart_range: LastFmRange = .overall,
recommendation_sample: Int, chart_limit: Int = 10){
include_library_tracks: Bool,
parts: Array<String>,
playlist_references: Array<String>,
shuffle: Bool,
chart_range: LastFmRange,
chart_limit: Int){
self.chart_range = chart_range self.chart_range = chart_range
self.chart_limit = chart_limit 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)
} }
} }

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import SwiftyJSON import SwiftyJSON
class Tag: Identifiable, Equatable { class Tag: Identifiable, Equatable, Decodable {
//MARK: Properties //MARK: Properties
@ -58,22 +58,6 @@ class Tag: Identifiable, Equatable {
self.last_updated = last_updated 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 { static func == (lhs: Tag, rhs: Tag) -> Bool {
return lhs.tag_id == rhs.tag_id return lhs.tag_id == rhs.tag_id
// && lhs.username == rhs.username // && lhs.username == rhs.username

View File

@ -9,12 +9,12 @@
import UIKit import UIKit
import SwiftyJSON import SwiftyJSON
enum UserType: String { enum UserType: String, Decodable {
case user = "user" case user = "user"
case admin = "admin" case admin = "admin"
} }
class User: Identifiable { class User: Identifiable, Decodable {
//MARK: Properties //MARK: Properties
@ -44,15 +44,5 @@ class User: Identifiable {
self.spotify_linked = spotify_linked self.spotify_linked = spotify_linked
self.lastfm_username = lastfm_username 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)
}
} }

View File

@ -110,6 +110,68 @@ extension PlaylistApi: ApiRequest {
return ApiRequestDefaults.authMethod 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
}
} }

View File

@ -100,6 +100,32 @@ extension TagApi: ApiRequest {
return ApiRequestDefaults.authMethod 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
}
} }

View File

@ -77,13 +77,13 @@ struct AddPlaylistSheet: View {
var playlist: Playlist? = nil var playlist: Playlist? = nil
switch PlaylistType(rawValue: selectedType) ?? .defaultPlaylist { switch PlaylistType(rawValue: selectedType) ?? .defaultPlaylist {
case .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 break
case .recents: 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 break
case .fmchart: 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 break
} }

View File

@ -46,8 +46,8 @@ struct PlaylistRow: View {
struct PlaylistRow_Previews: PreviewProvider { struct PlaylistRow_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
PlaylistRow(playlist: PlaylistView(playlist: .constant(
.constant(Playlist(name: "", uri: "", username: "", include_recommendations: true, recommendation_sample: 1, include_library_tracks: true, parts: [], playlist_references: [], shuffle: true)) Playlist(name: "playlist name", username: "username")
) ))
} }
} }

View File

@ -180,10 +180,7 @@ struct PlaylistView: View {
fatalError("error getting playlist") fatalError("error getting playlist")
} }
guard let json = try? JSON(data: data) else { self.playlist = PlaylistApi.fromJSON(playlist: data)!
fatalError("error parsing reponse")
}
self.playlist = Playlist.fromDict(dictionary: json)!
self.isRefreshing = false self.isRefreshing = false
} }
//TODO: do better error checking //TODO: do better error checking
@ -193,18 +190,7 @@ struct PlaylistView: View {
struct PlaylistView_Previews: PreviewProvider { struct PlaylistView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
PlaylistView(playlist: .constant( PlaylistView(playlist: .constant(
Playlist(name: "playlist name", Playlist(name: "playlist name", username: "username")
uri: "uri",
username: "username",
include_recommendations: true,
recommendation_sample: 5,
include_library_tracks: true,
parts: ["name"],
playlist_references: ["ref name"],
shuffle: true)
)) ))
} }
} }

View File

@ -44,7 +44,8 @@ struct RootView: View {
} }
} }
.pullToRefresh(isShowing: $isRefreshingPlaylists) { .pullToRefresh(isShowing: $isRefreshingPlaylists) {
self.refreshPlaylists() self.liveUser.refreshPlaylists()
self.isRefreshingPlaylists = false
} }
.navigationBarTitle(Text("Playlists").font(.title)) .navigationBarTitle(Text("Playlists").font(.title))
@ -85,7 +86,8 @@ struct RootView: View {
} }
} }
.pullToRefresh(isShowing: $isRefreshingTags) { .pullToRefresh(isShowing: $isRefreshingTags) {
self.refreshTags() self.liveUser.refreshTags()
self.isRefreshingTags = false
} }
.navigationBarTitle(Text("Tags").font(.title)) .navigationBarTitle(Text("Tags").font(.title))
@ -124,61 +126,8 @@ struct RootView: View {
} }
private func fetchAll() { private func fetchAll() {
refreshPlaylists() self.liveUser.refreshPlaylists()
refreshTags() self.liveUser.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
}
} }
} }

View File

@ -113,7 +113,10 @@ struct TagView: View {
guard let json = try? JSON(data: data) else { guard let json = try? JSON(data: data) else {
fatalError("error parsing reponse") 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 self.isRefreshing = false
} }
//TODO: do better error checking //TODO: do better error checking