Skip to content

Commit

Permalink
wip: search with track listing
Browse files Browse the repository at this point in the history
  • Loading branch information
Lee Jun Kit committed Jun 24, 2021
1 parent 55dd838 commit 74509e6
Show file tree
Hide file tree
Showing 18 changed files with 322 additions and 104 deletions.
12 changes: 12 additions & 0 deletions Spottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
475EE24C267EBDE7007BEBDC /* SearchResultsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475EE24B267EBDE7007BEBDC /* SearchResultsResponse.swift */; };
475EE24F267EBFEA007BEBDC /* WebAPIPagingObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475EE24E267EBFEA007BEBDC /* WebAPIPagingObject.swift */; };
4764B39D26833B9900AE471E /* HoverState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4764B39C26833B9900AE471E /* HoverState.swift */; };
4764B39F2684001A00AE471E /* TokenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4764B39E2684001A00AE471E /* TokenObject.swift */; };
4764B3A1268407C600AE471E /* TrackListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4764B3A0268407C600AE471E /* TrackListItem.swift */; };
4764B3A526842ECA00AE471E /* WebAPIImageCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4764B3A426842ECA00AE471E /* WebAPIImageCollection.swift */; };
47C56F602679A789003EA20A /* PlayerCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C56F5F2679A789003EA20A /* PlayerCommands.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -149,6 +152,9 @@
475EE24B267EBDE7007BEBDC /* SearchResultsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsResponse.swift; sourceTree = "<group>"; };
475EE24E267EBFEA007BEBDC /* WebAPIPagingObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAPIPagingObject.swift; sourceTree = "<group>"; };
4764B39C26833B9900AE471E /* HoverState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverState.swift; sourceTree = "<group>"; };
4764B39E2684001A00AE471E /* TokenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenObject.swift; sourceTree = "<group>"; };
4764B3A0268407C600AE471E /* TrackListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackListItem.swift; sourceTree = "<group>"; };
4764B3A426842ECA00AE471E /* WebAPIImageCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAPIImageCollection.swift; sourceTree = "<group>"; };
47C56F5F2679A789003EA20A /* PlayerCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerCommands.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -296,6 +302,7 @@
4730618A26591E36001E3A1F /* CoverGroupObject.swift */,
4730618C26591E50001E3A1F /* ImageObject.swift */,
474BAFBB2668B0170006EB16 /* RepeatMode.swift */,
4764B39E2684001A00AE471E /* TokenObject.swift */,
);
path = Base;
sourceTree = "<group>";
Expand Down Expand Up @@ -323,6 +330,7 @@
475EE247267D7784007BEBDC /* SearchField.swift */,
475EE249267DA451007BEBDC /* GreenPlayButton.swift */,
4764B39C26833B9900AE471E /* HoverState.swift */,
4764B3A0268407C600AE471E /* TrackListItem.swift */,
);
path = Components;
sourceTree = "<group>";
Expand All @@ -341,6 +349,7 @@
475798D0266F103100AADF2F /* WebAPIPlaylistTrackObject.swift */,
475798D4266F1B9B00AADF2F /* WebAPIPlaylistTracksRefObject.swift */,
475EE24E267EBFEA007BEBDC /* WebAPIPagingObject.swift */,
4764B3A426842ECA00AE471E /* WebAPIImageCollection.swift */,
);
path = WebAPI;
sourceTree = "<group>";
Expand Down Expand Up @@ -445,6 +454,7 @@
4730618926591E16001E3A1F /* DateObject.swift in Sources */,
4730616826565B03001E3A1F /* PlayPauseButton.swift in Sources */,
4730616326565828001E3A1F /* Search.swift in Sources */,
4764B3A526842ECA00AE471E /* WebAPIImageCollection.swift in Sources */,
4730618726591DF7001E3A1F /* ArtistObject.swift in Sources */,
470201BF265BB4320030ECA9 /* PreviousTrackButton.swift in Sources */,
470201C3265CF29B0030ECA9 /* NowPlaying.swift in Sources */,
Expand Down Expand Up @@ -482,8 +492,10 @@
475798D1266F103100AADF2F /* WebAPIPlaylistTrackObject.swift in Sources */,
475798CF266F101600AADF2F /* WebAPIPublicUserObject.swift in Sources */,
4730618F26591E9C001E3A1F /* ContextChangedEvent.swift in Sources */,
4764B3A1268407C600AE471E /* TrackListItem.swift in Sources */,
474BAFC1266B84CD0006EB16 /* CarouselRow.swift in Sources */,
4730616526565841001E3A1F /* Library.swift in Sources */,
4764B39F2684001A00AE471E /* TokenObject.swift in Sources */,
4730619926591F92001E3A1F /* VolumeChangedEvent.swift in Sources */,
4730619726591F14001E3A1F /* MetadataAvailableEvent.swift in Sources */,
4764B39D26833B9900AE471E /* HoverState.swift in Sources */,
Expand Down
13 changes: 5 additions & 8 deletions Spottie/Backend/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,14 @@ struct HTTPClient {
return Response(value: Nothing() as! T, response: result.response)
}

// check for empty body
let response = result.response as! HTTPURLResponse
let contentType = response.allHeaderFields["Content-Type"] as? String

if let contentType = contentType {
if contentType.contains("application/json") {
let value = try decoder.decode(T.self, from: result.data)
return Response(value: value, response: result.response)
}
if response.statusCode == 204 {
throw APIError.contentTypeError
}

throw APIError.contentTypeError
let value = try decoder.decode(T.self, from: result.data)
return Response(value: value, response: result.response)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
Expand Down
20 changes: 18 additions & 2 deletions Spottie/Backend/SpotifyAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ extension SpotifyAPI {
]

var urlComponents = URLComponents(
url: base.appendingPathComponent("/web-api/v1/search"),
url: URL(string: "https://api.spotify.com/v1/search")!,
resolvingAgainstBaseURL: false)!
urlComponents.queryItems = queryItems

Expand All @@ -155,6 +155,22 @@ extension SpotifyAPI {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase;

return client.run(req, decoder).map(\.value).print().eraseToAnyPublisher()
// librespot-java web-api proxy has a bug where it doubly-percent encodes
// so we grab the token and perform the search ourselves
return
token("user-read-private")
.flatMap { tokenObj -> AnyPublisher<HTTPClient.Response<SearchResultsResponse?>, Error> in
let token = tokenObj!.token
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return client.run(req, decoder)
}
.map(\.value)
.eraseToAnyPublisher()
}

static func token(_ scopes: String) -> AnyPublisher<TokenObject?, Error> {
var req = URLRequest(url: base.appendingPathComponent("/token/\(scopes)"))
req.httpMethod = "POST"
return client.run(req).print().map(\.value).eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ enum RepeatState: String, Codable {
case context
}

struct CurrentlyPlayingContextObject: Codable {
struct CurrentlyPlayingContextObject: Decodable {
var device: WebAPIDeviceObject
var isPlaying: Bool
var shuffleState: Bool
Expand Down
12 changes: 12 additions & 0 deletions Spottie/Backend/Types/Base/TokenObject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// TokenObject.swift
// Spottie
//
// Created by Lee Jun Kit on 24/6/21.
//

import Foundation

struct TokenObject: Decodable {
let token: String
}
10 changes: 1 addition & 9 deletions Spottie/Backend/Types/WebAPI/WebAPIArtistObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,10 @@

import Foundation

struct WebAPIArtistObject: Codable {
struct WebAPIArtistObject: Decodable, WebAPIImageCollection {
var id: String
var uri: String
var name: String
var images: [WebAPIImageObject]
var popularity: Int

func getArtworkURL() -> URL {
if (self.images.isEmpty) {
return URL(string: "https://misc.scdn.co/liked-songs/liked-songs-640.png")!
}

return URL(string: self.images[0].url)!
}
}
34 changes: 34 additions & 0 deletions Spottie/Backend/Types/WebAPI/WebAPIImageCollection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// WebAPIImageCollection.swift
// Spottie
//
// Created by Lee Jun Kit on 24/6/21.
//

import Foundation

protocol WebAPIImageCollection {
var images: [WebAPIImageObject] { get }
}

extension WebAPIImageCollection {
func getImageURL(_ forSize: WebAPIImageObject.ImageSize) -> URL {
if images.isEmpty {
// return a placeholder
return URL(string: "https://misc.scdn.co/liked-songs/liked-songs-64.png")!
}

switch (forSize) {
case .small:
return URL(string: images[images.count - 1].url)!
case .large:
return URL(string: images[0].url)!
case .medium:
if images.count <= 2 {
return URL(string: images[0].url)!
} else {
return URL(string: images[images.count / 2].url)!
}
}
}
}
14 changes: 10 additions & 4 deletions Spottie/Backend/Types/WebAPI/WebAPIImageObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
// Created by Lee Jun Kit on 24/5/21.
//

struct WebAPIImageObject: Codable {
var url: String
var width: Int?
var height: Int?
import Foundation

struct WebAPIImageObject: Decodable {
enum ImageSize {
case small, medium, large
}

let url: String
let width: Int?
let height: Int?
}
10 changes: 1 addition & 9 deletions Spottie/Backend/Types/WebAPI/WebAPIPlaylistObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,12 @@

import Foundation

struct WebAPIPlaylistObject: Decodable {
struct WebAPIPlaylistObject: Decodable, WebAPIImageCollection {
var id: String
var uri: String
var name: String
var owner: WebAPIPublicUserObject
var description: String?
var images: [WebAPIImageObject]
var tracks: WebAPIPlaylistTracksRefObject

func getArtworkURL() -> URL {
if (self.images.isEmpty) {
return URL(string: "https://misc.scdn.co/liked-songs/liked-songs-640.png")!
}

return URL(string: self.images[0].url)!
}
}
10 changes: 1 addition & 9 deletions Spottie/Backend/Types/WebAPI/WebAPISimplifiedAlbumObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ enum ReleaseDatePrecision: String, Codable {
case day
}

struct WebAPISimplifiedAlbumObject: Codable {
struct WebAPISimplifiedAlbumObject: Decodable, WebAPIImageCollection {
var id: String
var uri: String
var albumType: AlbumType
Expand All @@ -29,12 +29,4 @@ struct WebAPISimplifiedAlbumObject: Codable {
var releaseDate: String
var releaseDatePrecision: ReleaseDatePrecision
var totalTracks: Int

func getArtworkURL() -> URL {
if (self.images.isEmpty) {
return URL(string: "https://misc.scdn.co/liked-songs/liked-songs-640.png")!
}

return URL(string: self.images[0].url)!
}
}
2 changes: 1 addition & 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: Codable {
struct WebAPITrackObject: Decodable {
var id: String
var uri: String
var album: WebAPISimplifiedAlbumObject
Expand Down
2 changes: 1 addition & 1 deletion Spottie/ViewModels/PlayerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class PlayerViewModel: PlayerStateProtocol {
self.isPlaying = ctx.isPlaying
self.trackName = ctx.item.name
self.artistName = ctx.item.artists[0].name
self.artworkURL = ctx.item.album.getArtworkURL()
self.artworkURL = ctx.item.album.getImageURL(.medium)
self.durationMs = ctx.item.durationMs
self.progressMs = ctx.progressMs
}
Expand Down
62 changes: 49 additions & 13 deletions Spottie/Views/Components/CarouselRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import SwiftUI

struct CarouselRow: View {
let viewModel: ViewModel
let onItemPressed: (String) -> Void
let numItemsToShow: Int
let onItemTapped: (String) -> Void
let onItemPlayButtonTapped: (String) -> Void

var body: some View {
VStack(alignment: .leading) {
Expand All @@ -24,27 +25,62 @@ struct CarouselRow: View {
.padding(.leading)
.padding(.bottom, 8)
})
HStack(alignment: .top, spacing: 40) {
ForEach(viewModel.getItemsToShow(requestedNumToShow: numItemsToShow)) { item in
CarouselRowItem(
vm: item,
onPlayButtonPressed: {
onItemPressed(item.id)
})
.onTapGesture {
print("unimplemented")
}

if viewModel.items.isEmpty {
HStack {
Text("No items to show")
.foregroundColor(.secondary)
}
.padding()
}

switch viewModel.type {
case .grid:
HStack(alignment: .top, spacing: 40) {
ForEach(viewModel.getItemsToShow(requestedNumToShow: numItemsToShow)) { item in
CarouselRowItem(
vm: item,
onPlayButtonPressed: {
onItemPlayButtonTapped(item.uri)
})
.onTapGesture {
onItemTapped(item.uri)
}
}
}
.padding([.leading, .trailing])
case .shortcuts:
ShortcutGrid(
items: viewModel.items,
onItemPlayButtonTapped: onItemPlayButtonTapped,
onItemTapped: onItemTapped
)
.padding([.leading, .trailing])
case .trackList:
LazyVStack {
ForEach(viewModel.getItemsToShow(requestedNumToShow: 8)) { item in
TrackListItem(viewModel: item)
.onTapGesture(count: 2) {
onItemPlayButtonTapped(item.uri)
}
}
}
.padding([.leading, .trailing])
}
.padding([.leading, .trailing])
}

}
}

extension CarouselRow {
enum RowType {
case shortcuts
case trackList
case grid
}

struct ViewModel: Identifiable {
var id: String
var type: RowType
var title: String
var subtitle: String?
var items: [CarouselRowItem.ViewModel]
Expand Down
12 changes: 8 additions & 4 deletions Spottie/Views/Components/CarouselRowItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ struct CarouselRowItem: View {
.foregroundColor(.primary)
.font(.headline)
.padding(.vertical, 4)
Text(vm.subtitle)
.lineLimit(2)
.foregroundColor(.secondary)
vm.subtitle.map {
Text($0)
.lineLimit(2)
.foregroundColor(.secondary)
}
}
.frame(maxWidth: 200)
.padding()
Expand All @@ -62,10 +64,12 @@ struct CarouselRowItem: View {
extension CarouselRowItem {
struct ViewModel: Identifiable {
var id: String
var uri: String
var title: String
var subtitle: String
var subtitle: String?
var artworkURL: URL
var artworkIsCircle = false
var duration: Int? // for track list items only: is there a better way?
}
}

Expand Down
Loading

0 comments on commit 74509e6

Please sign in to comment.