Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ data class SecretData(

class SpotifyAPI {
private val webPlayerURL = "https://open.spotify.com/"
private val baseURL = "https://api.spotify.com/v1"
private val baseURL = "https://api-partner.spotify.com/pathfinder/v1/query"

// TOTP variables
private var totpSecret: ByteArray? = null
Expand Down Expand Up @@ -64,11 +64,10 @@ class SpotifyAPI {
val responseBody = response.bodyAsText(Charsets.UTF_8)
val secretDataList = json.decodeFromString<List<SecretData>>(responseBody)

// Get the last element
val gitu = secretDataList.last()
val lastSecretData = secretDataList.last()

totpSecret = toSecret(gitu.secret)
totpVer = gitu.version
totpSecret = toSecret(lastSecretData.secret)
totpVer = lastSecretData.version

totpGenerator = TimeBasedOneTimePasswordGenerator(
totpSecret!!,
Expand Down Expand Up @@ -144,7 +143,7 @@ class SpotifyAPI {
parameter("productType", "mobile-web-player")
parameter("ts", totp.first)
parameter("totp", totp.second)
parameter("totpVer", totpVer) // Use dynamic version
parameter("totpVer", totpVer)
}
val responseBody = response.bodyAsText(Charsets.UTF_8)
val json = json.decodeFromString<WebPlayerTokenResponse>(responseBody)
Expand All @@ -165,39 +164,51 @@ class SpotifyAPI {
if (System.currentTimeMillis() - tokenTime > 1800000) // 30 minutes
refreshToken()

val search = withContext(Dispatchers.IO) {
val searchTerm = withContext(Dispatchers.IO) {
URLEncoder.encode(
"${query.songName} ${query.artistName}",
StandardCharsets.UTF_8.toString()
)
}

if (search == "+")
if (searchTerm == "+")
throw EmptyQueryException()

val variables = """{"searchTerm":"$searchTerm","offset":$offset,"limit":1,"numberOfTopResults":20,"includeAudiobooks":false}"""
val extensions = """{"persistedQuery":{"version":1,"sha256Hash":"1d021289df50166c61630e02f002ec91182b518e56bcd681ac6b0640390c0245"}}"""

val encodedVariables = withContext(Dispatchers.IO) {
URLEncoder.encode(variables, StandardCharsets.UTF_8.toString())
}
val encodedExtensions = withContext(Dispatchers.IO) {
URLEncoder.encode(extensions, StandardCharsets.UTF_8.toString())
}

val response = client.get(
"$baseURL/search?q=$search&type=track&limit=1&offset=$offset"
"$baseURL?operationName=searchTracks&variables=$encodedVariables&extensions=$encodedExtensions"
) {
headers.append("Authorization", "Bearer $spotifyToken")
headers.append("Authorization", "Bearer $spotifyToken")
}
val responseBody = response.bodyAsText(Charsets.UTF_8)

val json = json.decodeFromString<TrackSearchResult>(responseBody)
if (json.tracks.items.isEmpty())
throw NoTrackFoundException()
val track = json.tracks.items[0]
if (json.data.searchV2.tracksV2.items.isEmpty())
throw NoTrackFoundException()

val trackItem = json.data.searchV2.tracksV2.items[0]
val track = trackItem.item.data

val artists = track.artists.joinToString(", ") { it.name }
val artists = track.artists.items.joinToString(", ") { it.profile.name }

val albumArtURL = track.album.images[0].url
val albumArtURL = track.albumOfTrack.coverArt.sources[0].url

val spotifyURL: String = track.externalUrls.spotify
val spotifyURL = "https://open.spotify.com/track/${track.id}"

return SongInfo(
track.name,
artists,
spotifyURL,
albumArtURL
track.name,
artists,
spotifyURL,
albumArtURL
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,96 +5,142 @@ import kotlinx.serialization.Serializable

@Serializable
data class TrackSearchResult(
val tracks: Tracks
)

@Serializable
data class Tracks(
val href: String,
val items: List<Track>,
val limit: Int,
val next: String?,
val offset: Int,
val previous: String?,
val total: Int
)

@Serializable
data class Track(
val album: Album,
val artists: List<Artist>,
@SerialName("available_markets")
val availableMarkets: List<String>,
@SerialName("disc_number")
val discNumber: Int,
@SerialName("duration_ms")
val durationMs: Int,
val explicit: Boolean,
@SerialName("external_ids")
val externalIds: ExternalIds,
@SerialName("external_urls")
val externalUrls: ExternalUrls,
val href: String,
val id: String,
@SerialName("is_local")
val isLocal: Boolean,
val name: String,
val popularity: Int,
@SerialName("preview_url")
val previewUrl: String?,
@SerialName("track_number")
val trackNumber: Int,
val type: String,
val uri: String
)

@Serializable
data class Album(
@SerialName("album_type")
val albumType: String,
val artists: List<Artist>,
@SerialName("available_markets")
val availableMarkets: List<String>,
@SerialName("external_urls")
val externalUrls: ExternalUrls,
val href: String,
val id: String,
val images: List<Image>,
val name: String,
@SerialName("release_date")
val releaseDate: String,
@SerialName("release_date_precision")
val releaseDatePrecision: String,
@SerialName("total_tracks")
val totalTracks: Int,
val type: String,
val uri: String
val data: Data,
val extensions: Extensions? = null
)

@Serializable
data class Data(
val searchV2: SearchV2
)

@Serializable
data class SearchV2(
val query: String,
val tracksV2: TracksV2
)

@Serializable
data class TracksV2(
val totalCount: Int,
val items: List<TrackItem>,
val pagingInfo: PagingInfo
)

@Serializable
data class TrackItem(
val matchedFields: List<String>,
val item: Item
)

@Serializable
data class Item(
val data: TrackData
)

@Serializable
data class Artist(
val externalUrls: ExternalUrls? = null,
val href: String,
data class TrackData(
@SerialName("__typename")
val typename: String,
val uri: String,
val id: String,
val name: String,
val type: String,
val uri: String
val albumOfTrack: AlbumOfTrack,
val artists: Artists,
val contentRating: ContentRating,
val duration: Duration,
val playability: Playability
)

@Suppress("SpellCheckingInspection")
@Serializable
data class ExternalIds(
val isrc: String
data class AlbumOfTrack(
val uri: String,
val name: String,
val coverArt: CoverArt,
val id: String
)

@Serializable
data class ExternalUrls(
val spotify: String
data class CoverArt(
val sources: List<ImageSource>,
val extractedColors: ExtractedColors
)

@Serializable
data class Image(
val height: Int,
data class ImageSource(
val url: String,
val width: Int
val width: Int,
val height: Int
)

@Serializable
data class ExtractedColors(
val colorDark: ColorDark
)

@Serializable
data class ColorDark(
val hex: String,
val isFallback: Boolean
)

@Serializable
data class Artists(
val items: List<ArtistItem>
)

@Serializable
data class ArtistItem(
val uri: String,
val profile: Profile
)

@Serializable
data class Profile(
val name: String
)

@Serializable
data class ContentRating(
val label: String
)

@Serializable
data class Duration(
val totalMilliseconds: Int
)

@Serializable
data class Playability(
val playable: Boolean
)

@Serializable
data class PagingInfo(
val nextOffset: Int,
val limit: Int
)

@Serializable
data class Extensions(
val requestIds: RequestIds,
val cacheControl: CacheControl? = null
)

@Serializable
data class RequestIds(
@SerialName("/searchV2")
val searchV2: SearchV2RequestId
)

@Serializable
data class SearchV2RequestId(
@SerialName("search-api")
val searchApi: String
)

@Serializable
data class CacheControl(
val version: Int,
val hints: List<String> = emptyList()
)