Skip to content
This repository has been archived by the owner on Dec 12, 2020. It is now read-only.

Commit

Permalink
chore: merge branch 'api-v2' (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
xeruf committed Apr 2, 2020
2 parents 5464b6e + d448faa commit ecce33f
Show file tree
Hide file tree
Showing 12 changed files with 107 additions and 77 deletions.
28 changes: 20 additions & 8 deletions src/main/xerus/monstercat/api/APIConnection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ import xerus.ktutil.javafx.properties.SimpleObservable
import xerus.ktutil.javafx.properties.listen
import xerus.monstercat.Settings
import xerus.monstercat.Sheets
import xerus.monstercat.api.response.ReleaseResponse
import xerus.monstercat.api.response.Session
import xerus.monstercat.api.response.TrackResponse
import xerus.monstercat.api.response.declaredKeys
import xerus.monstercat.api.response.*
import xerus.monstercat.downloader.CONNECTSID
import xerus.monstercat.downloader.QUALITY
import java.io.IOException
Expand Down Expand Up @@ -74,11 +71,12 @@ class APIConnection(vararg path: String): HTTPQuery<APIConnection>() {

/** @return null when the connection fails, else the parsed result */
fun getReleases() =
parseJSON(ReleaseResponse::class.java)?.results?.map { it.init() }
parseJSON(ReleaseListResponse::class.java)?.results?.map { it.init() }

fun getRelease() = parseJSON(ReleaseResponse::class.java)

/** @return null when the connection fails, else the parsed result */
fun getTracks() =
parseJSON(TrackResponse::class.java)?.results
fun getTracks() = getRelease()?.tracks

private var httpRequest: HttpUriRequest? = null
/** Aborts this connection and thus terminates the InputStream if active */
Expand Down Expand Up @@ -130,6 +128,20 @@ class APIConnection(vararg path: String): HTTPQuery<APIConnection>() {
return httpClient.execute(request, context)
}

/**
* Uses [APIConnection]'s HTTP client to process an HTTP 307 redirection to fetch the stream URL of a [track]
*
* @param track to get the stream URL from
* @return the redirected (real) stream URL for use with [javafx.scene.media.Media] which doesn't support redirections
*/
fun getRedirectedStreamURL(track: Track): String?{
val connection = APIConnection("v2", "release", track.release.id, "track-stream", track.id)
val context = HttpClientContext()
connection.execute(HttpGet(connection.uri), context)
return if (connection.response?.getLastHeader("Content-Type")?.value == "audio/mpeg")
context.redirectLocations.lastOrNull().toString() else null
}

private fun updateConnectsid(connectsid: String) {
val oldClient = httpClient
val manager = connectionManager
Expand Down Expand Up @@ -201,7 +213,7 @@ class APIConnection(vararg path: String): HTTPQuery<APIConnection>() {
}

private fun getConnectValidity(connectsid: String): ConnectResult {
val session = APIConnection("api", "self", "session").parseJSON(Session::class.java)
val session = APIConnection("v2", "self", "session").parseJSON(Session::class.java)
val validity = when {
session == null -> ConnectValidity.NOCONNECTION
session.user == null -> ConnectValidity.NOUSER
Expand Down
12 changes: 6 additions & 6 deletions src/main/xerus/monstercat/api/Cache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import xerus.monstercat.Settings
import xerus.monstercat.Sheets
import xerus.monstercat.api.response.Release
import xerus.monstercat.api.response.ReleaseList
import xerus.monstercat.api.response.ReleaseResponse
import xerus.monstercat.api.response.ReleaseListResponse
import xerus.monstercat.api.response.Track
import xerus.monstercat.cacheDir
import xerus.monstercat.downloader.CONNECTSID
import xerus.monstercat.globalDispatcher
import java.io.File

private const val cacheVersion = 6
private const val cacheVersion = 10

object Cache: Refresher() {
private val logger = KotlinLogging.logger { }
Expand Down Expand Up @@ -58,16 +58,16 @@ object Cache: Refresher() {

if(releases.isEmpty() && Settings.ENABLECACHE())
readCache()
val releaseResponse = APIConnection("api", "catalog", "release").fields(Release::class)
val releaseResponse = APIConnection("v2", "releases").fields(Release::class)
.limit(((currentSeconds() - lastRefresh) / 80_000).coerceIn(4, 9))
.parseJSON(ReleaseResponse::class.java)?.also { it.results.forEach { it.init() } }
.parseJSON(ReleaseListResponse::class.java)?.also { it.results.forEach { it.init() } }
?: run {
logger.info("Release refresh failed!")
return
}
val results = releaseResponse.results

val releaseConnection = APIConnection("api", "catalog", "release").fields(Release::class)
val releaseConnection = APIConnection("v2", "releases").fields(Release::class)
when {
releaseResponse.total - releases.size > results.size || !releases.contains(results.last()) -> {
logger.info("Full Release refresh initiated")
Expand Down Expand Up @@ -109,7 +109,7 @@ object Cache: Refresher() {
releases.associateWith { release ->
if(release.tracks.isNotEmpty()) return@associateWith null
GlobalScope.async(globalDispatcher) {
val tracks = APIConnection("api", "catalog", "release", release.id, "tracks").getTracks()
val tracks = APIConnection("v2", "catalog", "release", release.catalogId).getTracks()
if(tracks == null) {
logger.warn("Couldn't fetch tracks for $release")
failed++
Expand Down
2 changes: 1 addition & 1 deletion src/main/xerus/monstercat/api/Covers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ object Covers {

private fun coverCacheFile(coverUrl: String, size: Int): File {
coverCacheDir.mkdirs()
val newFile = coverCacheDir.resolve(coverUrl.substringAfterLast('/').replaceIllegalFileChars())
val newFile = coverCacheDir.resolve(coverUrl.substringBeforeLast('/').substringAfterLast('/').replaceIllegalFileChars())
return coverCacheDir.resolve("${newFile.nameWithoutExtension}x$size.${newFile.extension}")
}

Expand Down
45 changes: 25 additions & 20 deletions src/main/xerus/monstercat/api/Player.kt
Original file line number Diff line number Diff line change
Expand Up @@ -138,32 +138,37 @@ object Player: FadingHBox(true, targetHeight = 25) {
/** Plays the given [track] in the Player, stopping the previous MediaPlayer if necessary */
fun playTrack(track: Track) {
disposePlayer()
val hash = track.streamHash ?: run {
if(!track.streamable) {
showError("$track is currently not available for streaming!")
return
}
updateCover(track.release.coverUrl)
logger.debug("Loading $track from $hash")
activePlayer.value = MediaPlayer(Media("https://s3.amazonaws.com/data.monstercat.com/blobs/$hash"))
updateVolume()
onFx {
activeTrack.value = track
}
playing("Loading $track")
player?.run {
play()
setOnEndOfMedia { playNext() }
setOnReady {
label.text = "Now Playing: $track"
val total = totalDuration.toMillis()
seekBar.progressProperty().dependOn(currentTimeProperty()) { it.toMillis() / total }
seekBar.transitionToHeight(Settings.PLAYERSEEKBARHEIGHT(), 1.0)
GlobalScope.launch {
logger.debug("Fetching stream url for $track")
val streamUrl = APIConnection.getRedirectedStreamURL(track)
logger.debug("Loading $track from '$streamUrl'")
activePlayer.value = MediaPlayer(Media(streamUrl))
updateVolume()
onFx {
activeTrack.value = track
}
setOnError {
logger.warn("Error loading $track: $error", error)
showError("Error loading $track: ${error.message?.substringAfter(": ")}")
playing("Loading $track")
player?.run {
play()
setOnEndOfMedia { playNext() }
setOnReady {
label.text = "Now Playing: $track"
val total = totalDuration.toMillis()
seekBar.progressProperty().dependOn(currentTimeProperty()) { it.toMillis() / total }
seekBar.transitionToHeight(Settings.PLAYERSEEKBARHEIGHT(), 1.0)
}
setOnError {
logger.warn("Error loading $track: $error", error)
showError("Error loading $track: ${error.message?.substringAfter(": ")}")
}
}
}

updateCover(track.release.coverUrl)
}

/** Disposes the [activePlayer] and hides the [seekBar] */
Expand Down
8 changes: 7 additions & 1 deletion src/main/xerus/monstercat/api/response/Artist.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ abstract class Artist {
override fun equals(other: Any?): Boolean = this === other || (other is Artist && name == other.name)
}

class ArtistRel(@Key override var name: String = "", @Key var role: String = "", @Key var vendor: String? = null, @Key override var artistId: String = ""): Artist() {
class ArtistRel(
@Key override var name: String = "",
@Key var role: String = "",
@Key var vendor: String? = null,
@Key("id") override var artistId: String = "",
@Key var uri: String? = null
): Artist() {
fun debugString() = "ArtistRel(name=$name, role=$role, vendor=$vendor)"
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/xerus/monstercat/api/response/ListResponse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ open class ListResponse<T> {
override fun toString() = "${this.javaClass.simpleName}($total elements): $results"
}

class ReleaseResponse: ListResponse<Release>()
class TrackResponse: ListResponse<Track>()
class ReleaseListResponse: ListResponse<Release>()
class TrackListResponse: ListResponse<Track>()

class ReleaseList: ArrayList<Release>()
17 changes: 9 additions & 8 deletions src/main/xerus/monstercat/api/response/Release.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import xerus.ktutil.to
private val logger = KotlinLogging.logger { }

data class Release(
@Key("_id") override var id: String = "",
@Key override var id: String = "",
@Key var catalogId: String = "",
@Key var releaseDate: String = "",
@Key var type: String = "",
@Key var renderedArtists: String = "",
@Key var artistsTitle: String = "",
@Key override var title: String = "",
@Key var coverUrl: String = "",
@Key var downloadable: Boolean = false): MusicItem() {

@Key var isCollection: Boolean = false
Expand All @@ -25,11 +24,13 @@ data class Release(
field = value
}

val coverUrl: String
get() = "https://connect.monstercat.com/v2/release/$id/cover"

fun init(): Release {
renderedArtists = formatArtists(renderedArtists)
artistsTitle = formatArtists(artistsTitle)
title = title.trim()
releaseDate = releaseDate.substring(0, 10)
coverUrl = coverUrl.replace(" ", "%20").replace("[", "%5B").replace("]", "%5D")

if(!isType(Type.MIX, Type.PODCAST)) {
val typeValue = Type.values().find {
Expand All @@ -49,10 +50,10 @@ data class Release(
fun isType(vararg types: Type): Boolean = types.any { type.equals(it.displayName, true) }

override fun toString(): String =
renderedArtists.isEmpty().to("%2\$s", "%s - %s").format(renderedArtists, title)
artistsTitle.isEmpty().to("%2\$s", "%s - %s").format(artistsTitle, title)

fun debugString(): String =
"Release(id='$id', releaseDate='$releaseDate', type='$type', renderedArtists='$renderedArtists', title='$title', coverUrl='$coverUrl', downloadable=$downloadable, isCollection=$isCollection)"
"Release(id='$id', releaseDate='$releaseDate', type='$type', artistsTitle='$artistsTitle', title='$title', coverUrl='$coverUrl', downloadable=$downloadable, isCollection=$isCollection)"

enum class Type(override val displayName: String, val isCollection: Boolean, val matcher: (Release.() -> Boolean)? = null): Named, CharSequence by displayName {
MCOLLECTION("Monstercat Collection", true,
Expand All @@ -67,4 +68,4 @@ data class Release(
override fun toString() = displayName
}

}
}
18 changes: 18 additions & 0 deletions src/main/xerus/monstercat/api/response/ReleaseResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package xerus.monstercat.api.response

import com.google.api.client.util.Key

/**
* Response sent by the API when doing a request to the endpoint for a Release.
* This is the only way to get the tracks of a release starting with API v2.
* Endpoint is connect.monstercat.com/v2/catalog/release/([Release.catalogId])
*
* @param release an instance of the requested [Release]
* @param tracks a list of [tracks][Track] that appears on the [release]
* @param related other releases that might be of interest for the user
*/
data class ReleaseResponse(
@Key var release: Release? = null,
@Key var tracks: ArrayList<Track>? = null,
@Key var related: ReleaseListResponse? = null
)
36 changes: 12 additions & 24 deletions src/main/xerus/monstercat/api/response/Track.kt
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
package xerus.monstercat.api.response

import com.google.api.client.util.Key
import xerus.ktutil.splitTitleTrimmed
import xerus.monstercat.api.splitArtists
import xerus.monstercat.api.splitTitle
import xerus.ktutil.splitTitleTrimmed
import java.util.Collections.emptyList

open class Track: MusicItem() {

@Key("_id")
override var id: String = ""
@Key
var artistsTitle: String = ""
@Key
override var title: String = ""
@Key
var version: String = ""
@Key
var albums: List<Album> = emptyList()
@Key("artistRelationships")
var artists: List<ArtistRel> = emptyList()
@Key
var bpm: Double? = null
@Key
var licensable: Boolean = false
@Key override var id: String = ""
@Key var artistsTitle: String = ""
@Key override var title: String = ""
@Key var version: String = ""
@Key var artists: List<ArtistRel> = emptyList()
@Key var bpm: Double? = null
@Key var streamable: Boolean = false
@Key var trackNumber = -1
@Key var licensable: Boolean = false

var artistsSplit: List<String> = emptyList()
var titleClean: String = ""
Expand All @@ -37,10 +30,6 @@ open class Track: MusicItem() {
var albumArtists = ""
var albumId = ""
var albumName = ""
var trackNumber = -1

val streamHash: String?
get() = albums.find { it.streamHash.isNotEmpty() }?.streamHash

val isAlbumMix
get() = title.contains("Album Mix")
Expand All @@ -50,10 +39,9 @@ open class Track: MusicItem() {
return this

if(::release.isInitialized) {
albumArtists = release.renderedArtists
albumArtists = release.artistsTitle
albumName = release.title
albumId = release.catalogId
trackNumber = albums.find { it.albumId == release.id }?.trackNumber ?: -1
}

artistsTitle = formatArtists(artistsTitle)
Expand Down Expand Up @@ -91,5 +79,5 @@ open class Track: MusicItem() {
override fun hashCode() = id.hashCode()

fun debugString(): String =
"Track(id='$id', artistsTitle='$artistsTitle', title='$title', albums=$albums, artists=$artists, bpm=$bpm, artistsSplit=$artistsSplit, titleClean='$titleClean', remix='$remix', feat='$feat', extra='$extra', splitTitle=$splitTitle, release=${if(::release.isInitialized) release else null}, albumArtists='$albumArtists', albumId='$albumId', albumName='$albumName', trackNumber=$trackNumber)"
"Track(id='$id', artistsTitle='$artistsTitle', title='$title', artists=$artists, bpm=$bpm, artistsSplit=$artistsSplit, titleClean='$titleClean', remix='$remix', feat='$feat', extra='$extra', splitTitle=$splitTitle, release=${if(::release.isInitialized) release else null}, albumArtists='$albumArtists', albumId='$albumId', albumName='$albumName', trackNumber=$trackNumber)"
}
6 changes: 3 additions & 3 deletions src/main/xerus/monstercat/downloader/Download.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class ReleaseDownload(private val release: Release, private var tracks: Collecti
private var totalProgress = 0

fun downloadTrack(releaseId: String, trackId: String, path: Path) {
val connection = APIConnection("api", "release", releaseId, "download").addQueries("method=download", "type=${QUALITY()}", "track=$trackId")
val connection = APIConnection("v2", "release", releaseId, "track-download", trackId).addQuery("format", QUALITY())
val httpResponse = connection.getResponse()
val entity = httpResponse.entity
val contentLength = entity.contentLength
Expand Down Expand Up @@ -129,9 +129,9 @@ class ReleaseDownload(private val release: Release, private var tracks: Collecti
totalProgress++
}
if(!isCancelled && downloadCover) {
val coverName = release.toString(COVERPATTERN()).replaceIllegalFileChars() + "." + release.coverUrl.substringAfterLast('.')
updateMessage(coverName)
val entity = Covers.fetchCover(release.coverUrl, COVERARTSIZE())
val coverName = release.toString(COVERPATTERN()).replaceIllegalFileChars() + "." + entity.contentType.value.substringAfter('/')
updateMessage(coverName)
val length = entity.contentLength.toDouble()
downloadFile(entity.content, downloadFolder.resolve(coverName), true) {
totalProgress + (coverFraction * it / length)
Expand Down
6 changes: 3 additions & 3 deletions src/main/xerus/monstercat/downloader/DownloaderSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ val SONGSORTING = DownloaderSettings.create("songViewReleaseSorting", ReleaseSor
enum class ReleaseSorting(val selector: (Release) -> String) : Named {
DATE(Release::releaseDate),
TITLE(Release::title),
ARTIST(Release::renderedArtists),
ARTIST(Release::artistsTitle),
AMOUNT_OF_TRACKS({ it.tracks.size.toString().padStart(2, '0') });

override val displayName = name.replace('_', ' ').toLowerCase().capitalize()
}

val DOWNLOADDIR = DownloaderSettings.create("directory", Paths.get("Monstercat"))
val DOWNLOADDIRSINGLE = DownloaderSettings.create("directorySingle")
val DOWNLOADDIRALBUM = DownloaderSettings.create("directoryAlbum", "{%renderedArtists% - }%title%")
val DOWNLOADDIRALBUM = DownloaderSettings.create("directoryAlbum", "{%artistsTitle% - }%title%")
val DOWNLOADDIRPODCAST = DownloaderSettings.create("directoryPodcasts", "Podcast")
val DOWNLOADDIRMIXES = DownloaderSettings.create("directoryMixes", "Mixes")

Expand All @@ -46,7 +46,7 @@ enum class AlbumMixes(override val displayName: String) : Named {


val COVERARTSIZE = DownloaderSettings.create("coverArtSize", 1024)
val COVERPATTERN = DownloaderSettings.create("coverPattern", "{%renderedArtists% - }%title%")
val COVERPATTERN = DownloaderSettings.create("coverPattern", "{%artistsTitle% - }%title%")

val QUALITY = DownloaderSettings.create("quality")
val CONNECTSID = DownloaderSettings.create("connect.sid")
Expand Down
2 changes: 1 addition & 1 deletion src/main/xerus/monstercat/downloader/SongView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ class SongView(private val sorter: ObservableValue<ReleaseSorting>):
}
logger.debug { "$notDownloadable of ${releases.size} Releases are not downloadable" }
sorter.listen { sortReleases(it.selector) }.changed(null, null, sorter.value)
roots.flatMap { it.value.internalChildren }.forEach { release -> (release as FilterableTreeItem).internalChildren.sortBy { (it.value as Track).albums.find { it.albumId == release.value.id }?.trackNumber } }
roots.flatMap { it.value.internalChildren }.forEach { release -> (release as FilterableTreeItem).internalChildren.sortBy { (it.value as Track).trackNumber } }
}

private fun <T: Comparable<T>> sortReleases(selector: (Release) -> T) {
Expand Down

0 comments on commit ecce33f

Please sign in to comment.