Skip to content

Commit

Permalink
big commit for backend types and services
Browse files Browse the repository at this point in the history
  • Loading branch information
Lee Jun Kit committed Oct 9, 2021
1 parent 67ac070 commit 2baa905
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 41 deletions.
4 changes: 3 additions & 1 deletion Spottie/Backend/EventBroker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import Foundation
import Combine

class EventBroker : ObservableObject {
private let websocketClient = WebsocketClient(url: URL(string: "ws://localhost:24879/events")!)
// private let websocketClient = WebsocketClient(url: URL(string: "ws://localhost:24879/events")!)
private var cancellables = [AnyCancellable]()
private let decoder = JSONDecoder()

let onEventReceived = PassthroughSubject<SpotifyEvent, Never>()

init() {
/*
websocketClient
.onMessageReceived
.receive(on: DispatchQueue.main)
Expand All @@ -32,5 +33,6 @@ class EventBroker : ObservableObject {
print(error)
}
}.store(in: &cancellables)
*/
}
}
8 changes: 6 additions & 2 deletions Spottie/Backend/SpotifyAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import Foundation
import Combine

enum APIError: Error {
case unknown
}

enum SpotifyAPI {
static let client = HTTPClient()
static let base = URL(string: "http://localhost:24879")!
Expand Down Expand Up @@ -160,8 +164,8 @@ extension SpotifyAPI {
return
token("user-read-private")
.flatMap { tokenObj -> AnyPublisher<HTTPClient.Response<SearchResultsResponse?>, Error> in
let token = tokenObj!.token
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let token = "\(tokenObj!.tokenType) \(tokenObj!.accessToken)"
req.addValue(token, forHTTPHeaderField: "Authorization")
return client.run(req, decoder)
}
.map(\.value)
Expand Down
158 changes: 158 additions & 0 deletions Spottie/Backend/SpotifyWebAPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//
// SpotifyWebAPI.swift
// SpotifyWebAPI
//
// Created by Lee Jun Kit on 21/8/21.
//

import Foundation
import Combine

enum WebAPIError: Error {
case unknown
case authTokenError(CurlError)
case decodingError(Error)
}

actor TrackCache {
var cachedTracks = [String: WebAPITrackObject]()
func get(_ id: String) -> WebAPITrackObject? {
return cachedTracks[id]
}

func set(tracks: [WebAPITrackObject]) {
for track in tracks {
cachedTracks[track.id] = track
}
}
}

struct SpotifyWebAPI {
let playerCore: PlayerCore
let trackCache = TrackCache()
let decoder = JSONDecoder()

init(playerCore: PlayerCore) {
self.playerCore = playerCore
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
self.decoder.dateDecodingStrategy = .iso8601
}

func getTrack(_ id: String) async -> Result<WebAPITrackObject, WebAPIError> {
if let track = await trackCache.get(id) {
return .success(track)
}

let url = "https://api.spotify.com/v1/tracks/\(id)"
var req = URLRequest(url: URL(string: url)!)
req = await addAuthorizationHeader(request: &req)

do {
let (data, _) = try await URLSession.shared.data(for: req)
let track = try decoder.decode(WebAPITrackObject.self, from: data)
await trackCache.set(tracks: [track])
return .success(track)
} catch {
return .failure(.unknown)
}
}

func getSavedTracks() -> AnyPublisher<[WebAPISavedTrackObject], WebAPIError> {
let subject = PassthroughSubject<[WebAPISavedTrackObject], WebAPIError>()
let endpoint = URL(string: "https://api.spotify.com/v1/me/tracks?limit=50&offset=0")!
Task.init {
await sendItemsToSubjectFromPagedEndpoint(
type: WebAPISavedTrackObject.self,
endpoint: endpoint,
subject: subject
)
}

return subject.eraseToAnyPublisher()
}

func getLibraryPlaylists() -> AnyPublisher<[WebAPISimplifiedPlaylistObject], WebAPIError> {
let subject = PassthroughSubject<[WebAPISimplifiedPlaylistObject], WebAPIError>()
let endpoint = URL(string: "https://api.spotify.com/v1/me/playlists")!
Task.init {
await sendItemsToSubjectFromPagedEndpoint(
type: WebAPISimplifiedPlaylistObject.self,
endpoint: endpoint,
subject: subject
)
}

return subject.eraseToAnyPublisher()
}

func sendItemsToSubjectFromPagedEndpoint<T: Decodable>(type: T.Type, endpoint: URL, subject: PassthroughSubject<[T], WebAPIError>) async {
var url: URL? = endpoint
while let u = url {
var req = URLRequest(url: u)
req = await addAuthorizationHeader(request: &req)

do {
let (data, _) = try await URLSession.shared.data(for: req)
let pager = try decoder.decode(WebAPIPagingObject<T>.self, from: data)
subject.send(pager.items)

// set next url
if let nextURLString = pager.next {
url = URL(string: nextURLString)
} else {
url = nil
subject.send(completion: .finished)
}
} catch {
// TODO: handle errors
subject.send(completion: .failure(.unknown))
}
}
}

func getPersonalizedRecommendations() async -> Result<RecommendationsResponse, WebAPIError> {
// get the current date
let date = Date()
let formatter = ISO8601DateFormatter()
formatter.formatOptions.insert(.withFractionalSeconds)

var comps = URLComponents(string: "https://api.spotify.com/v1/views/personalized-recommendations")!
comps.queryItems = [
URLQueryItem(name: "timestamp", value: formatter.string(from: date)),
URLQueryItem(name: "platform", value: "web"),
URLQueryItem(name: "content_limit", value: "10"),
URLQueryItem(name: "limit", value: "20"),
URLQueryItem(name: "types", value: "album,playlist,artist,show,station,episode"),
URLQueryItem(name: "image_style", value: "gradient_overlay"),
URLQueryItem(name: "country", value: "SG"),
URLQueryItem(name: "locale", value: "en"),
URLQueryItem(name: "market", value: "from_token")
]

var req = URLRequest(url: comps.url!)
req.httpMethod = "GET"
req = await addAuthorizationHeader(request: &req)

do {
let (data, _) = try await URLSession.shared.data(for: req)
let recommendations = try decoder.decode(RecommendationsResponse.self, from: data)
return .success(recommendations)
} catch {
print(error)
return .failure(.unknown)
}
}

private func addAuthorizationHeader(request: inout URLRequest) async -> URLRequest {
let result = await playerCore.token()
if case let .success(tokenObj) = result {
print(tokenObj.tokenType)
request.setValue("Bearer \(tokenObj.accessToken)", forHTTPHeaderField: "Authorization")
} else {

// TODO: handle errors here
}

return request
}
}
11 changes: 10 additions & 1 deletion Spottie/Backend/Types/Base/TokenObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,14 @@
import Foundation

struct TokenObject: Decodable {
let token: String
let accessToken: String
let tokenType: String
let expiresIn: Int
let scope: [String]

var authHeader: String {
get {
return "\(tokenType) \(accessToken)"
}
}
}
13 changes: 13 additions & 0 deletions Spottie/Backend/Types/WebAPI/WebAPISavedTrackObject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// WebAPISavedTrackObject.swift
// WebAPISavedTrackObject
//
// Created by Lee Jun Kit on 21/8/21.
//

import Foundation

struct WebAPISavedTrackObject: Decodable {
var addedAt: Date
var track: WebAPITrackObject
}
19 changes: 19 additions & 0 deletions Spottie/Backend/Types/WebAPI/WebAPISimplifiedPlaylistObject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// WebAPISimplifiedPlaylistObject.swift
// WebAPISimplifiedPlaylistObject
//
// Created by Lee Jun Kit on 22/9/21.
//

import Foundation

struct WebAPISimplifiedPlaylistObject: Decodable, Identifiable, WebAPIImageCollection {
let collaborative: Bool
let description: String?
let id: String
let uri: String
let images: [WebAPIImageObject]
let name: String
let snapshotId: String
let tracks: WebAPIPlaylistTracksRefObject
}
15 changes: 14 additions & 1 deletion Spottie/Backend/Types/WebAPI/WebAPITrackObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Lee Jun Kit on 24/5/21.
//

struct WebAPITrackObject: Decodable {
struct WebAPITrackObject: Decodable, Identifiable {
var id: String
var uri: String
var album: WebAPISimplifiedAlbumObject
Expand All @@ -16,4 +16,17 @@ struct WebAPITrackObject: Decodable {
var popularity: Int
var discNumber: Int
var trackNumber: Int

var artistString: String {
get {
return artists.map({$0.name}).joined(separator: ", ")
}
}

var durationString: String {
get {
let durationSeconds = Double(durationMs / 1000)
return DurationFormatter.shared.format(durationSeconds)
}
}
}
24 changes: 8 additions & 16 deletions Spottie/Commands/PlayerCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,17 @@ import SwiftUI
import Combine

struct PlayerCommands: Commands {
struct MenuContent: View {
private var viewModel: ViewModel = ViewModel()
var body: some View {
Button("Toggle Play/Pause") {
SpotifyAPI.togglePlayPause().sink { _ in } receiveValue: { _ in }.store(in: &viewModel.cancellables)
}
.keyboardShortcut(" ", modifiers: [])
}
}
@Inject var playerCore: PlayerCore
@Inject var webAPI: SpotifyWebAPI

var body: some Commands {
CommandMenu("Player") {
MenuContent()
Button("Toggle Playback") {
Task.init {
await playerCore.togglePlayback()
}
}
.keyboardShortcut(" ", modifiers: [])
}
}
}

extension PlayerCommands.MenuContent {
class ViewModel: ObservableObject {
var cancellables = [AnyCancellable]()
}
}
21 changes: 21 additions & 0 deletions Spottie/Persistence/DBService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// DBService.swift
// DBService
//
// Created by Lee Jun Kit on 25/8/21.
//

import Foundation
import CoreData

struct DBService {
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "v1")
container.loadPersistentStores { storeDescription, error in
if let error = error {
fatalError("Fatal: Cannot load persistent stores: \(error)")
}
}
}
}
32 changes: 32 additions & 0 deletions Spottie/Persistence/v1.xcdatamodeld/v1.xcdatamodel/contents
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19197" systemVersion="21A5304g" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="CachedTrack" representedClassName="CachedTrack" syncable="YES" codeGenerationType="class">
<attribute name="albumName" optional="YES" attributeType="String"/>
<attribute name="artists" optional="YES" attributeType="String"/>
<attribute name="durationMs" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/>
<relationship name="images" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="SpotifyImage"/>
<fetchIndex name="byId">
<fetchIndexElement property="id" type="Binary" order="descending"/>
</fetchIndex>
</entity>
<entity name="EtagCachedResponse" representedClassName="EtagCachedResponse" syncable="YES" codeGenerationType="class">
<attribute name="etag" optional="YES" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="String"/>
<attribute name="value" optional="YES" attributeType="Binary"/>
<fetchIndex name="byUrl">
<fetchIndexElement property="url" type="Binary" order="descending"/>
</fetchIndex>
</entity>
<entity name="SpotifyImage" representedClassName="SpotifyImage" syncable="YES" codeGenerationType="class">
<attribute name="height" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="url" optional="YES" attributeType="String"/>
<attribute name="width" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
<elements>
<element name="CachedTrack" positionX="-63" positionY="-18" width="128" height="119"/>
<element name="SpotifyImage" positionX="-27" positionY="54" width="128" height="74"/>
<element name="EtagCachedResponse" positionX="0" positionY="90" width="128" height="74"/>
</elements>
</model>
20 changes: 0 additions & 20 deletions Spottie/PlayerCore/player_core.h

This file was deleted.

0 comments on commit 2baa905

Please sign in to comment.