-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
big commit for backend types and services
- Loading branch information
Lee Jun Kit
committed
Oct 9, 2021
1 parent
67ac070
commit 2baa905
Showing
11 changed files
with
284 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
19
Spottie/Backend/Types/WebAPI/WebAPISimplifiedPlaylistObject.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
32
Spottie/Persistence/v1.xcdatamodeld/v1.xcdatamodel/contents
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file was deleted.
Oops, something went wrong.