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

fix(covers): catch covers API timeouts and hardcode cache file extensions #123

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
12 changes: 10 additions & 2 deletions src/main/xerus/monstercat/MonsterUtilities.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import xerus.monstercat.api.Player
import xerus.monstercat.downloader.TabDownloader
import xerus.monstercat.tabs.*
import java.io.File
import java.io.IOException
import java.net.URL
import java.net.UnknownHostException
import java.util.*
Expand Down Expand Up @@ -362,7 +363,7 @@ class MonsterUtilities(checkForUpdate: Boolean): JFXMessageDisplay {
pane.add(Label("Cover loading..."))
pane.add(largeImage)

App.stage.createStage(title, pane).apply {
val stage = App.stage.createStage(title, pane).apply {
height = windowSize
width = windowSize
this.isResizable = isResizable
Expand Down Expand Up @@ -396,7 +397,14 @@ class MonsterUtilities(checkForUpdate: Boolean): JFXMessageDisplay {
}

GlobalScope.launch {
largeImage.image = Covers.getCoverImage(coverUrl, windowSize.toInt())
try {
largeImage.image = Covers.getCoverImage(coverUrl, windowSize.toInt())
} catch (e: IOException) {
onFx {
stage.close()
showError(e, "Cover could not be fetched.")
}
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/main/xerus/monstercat/api/APIConnection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mu.KotlinLogging
import org.apache.http.HttpResponse
import org.apache.http.client.ClientProtocolException
import org.apache.http.client.config.CookieSpecs
import org.apache.http.client.config.RequestConfig
import org.apache.http.client.methods.CloseableHttpResponse
Expand Down Expand Up @@ -123,6 +124,8 @@ class APIConnection(vararg path: String): HTTPQuery<APIConnection>() {
CONNECTSID.listen { updateConnectsid(it) }
}

/**@return a [CloseableHttpResponse] resulting from the HTTP [request]
* @throws IOException when the request fails */
fun executeRequest(request: HttpUriRequest, context: HttpClientContext? = null): CloseableHttpResponse {
logger.trace { "Connecting to ${request.uri}" }
return httpClient.execute(request, context)
Expand Down
31 changes: 22 additions & 9 deletions src/main/xerus/monstercat/api/Covers.kt
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
package xerus.monstercat.api

import javafx.scene.image.Image
import mu.KotlinLogging
import org.apache.http.HttpEntity
import org.apache.http.client.ClientProtocolException
import org.apache.http.client.methods.HttpGet
import xerus.ktutil.replaceIllegalFileChars
import xerus.monstercat.cacheDir
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.lang.IllegalArgumentException
import java.net.URI

object Covers {

val logger = KotlinLogging.logger { }
private val coverCacheDir = cacheDir.resolve("cover-images").apply { mkdirs() }

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

/** Returns an Image of the cover in the requested size using caching.
* @param size the size of the Image that is returned - the image file will always be 64x64 */
fun getThumbnailImage(coverUrl: String, size: Number = 64, invalidate: Boolean = false): Image =
getCover(coverUrl, 64, invalidate).use { createImage(it, size) }
* @param size the size of the Image that is returned - the image file will always be 64x64
* @throws IOException propagation of [getCover]'s exception */
fun getThumbnailImage(coverUrl: String, size: Number = 64, invalidate: Boolean = false) =
getCover(coverUrl, 64, invalidate).use { createImage(it, size) }

/** Returns a larger Image of the cover in the requested size using caching.
* @param size the size of the Image that is returned - the image file will always be 1024x1024 */
* @param size the size of the Image that is returned - the image file will always be 1024x1024
* @throws IOException propagation of [getCover]'s exception */
fun getCoverImage(coverUrl: String, size: Int = 1024, invalidate: Boolean = false): Image =
getCover(coverUrl, 1024, invalidate).use { createImage(it, size) }

Expand All @@ -37,13 +44,17 @@ object Covers {
* @param coverUrl the URL for fetching the cover
* @param size the dimensions of the cover file
* @param invalidate set to true to ignore already existing cache files
* @throws IOException propagation of [fetchCover]'s exception if it fails (connectivity issues)
*/
fun getCover(coverUrl: String, size: Int, invalidate: Boolean = false): InputStream {
val coverFile = coverCacheFile(coverUrl, size)
if(!invalidate)
try {
return coverFile.inputStream()
} catch(e: Exception) {
} catch (e: FileNotFoundException) {
logger.warn("Cover file at ${coverFile.path} not found, invalidating.")
} catch (e: SecurityException) {
logger.warn("Cover file at ${coverFile.path} cannot be opened, invalidating.")
}
fetchCover(coverUrl, size).content.use { input ->
val tempFile = File.createTempFile(coverFile.name, size.toString())
Expand All @@ -58,7 +69,9 @@ object Covers {
/** Fetches the given [coverUrl] with an [APIConnection] in the requested [size].
* @param coverUrl the base url to fetch the cover
* @param size the size of the cover to be fetched from the api, with all powers of 2 being available.
* By default null, which results in the biggest size possible, usually between 2k and 8k. */
* By default null, which results in the biggest size possible, usually between 2k and 8k.
* @throws IOException in case of timeout, connectivity issues...
*/
fun fetchCover(coverUrl: String, size: Int? = null): HttpEntity =
APIConnection.executeRequest(HttpGet(getCoverUrl(coverUrl, size))).entity

Expand Down
13 changes: 9 additions & 4 deletions src/main/xerus/monstercat/api/Player.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import xerus.monstercat.Settings
import xerus.monstercat.api.response.Release
import xerus.monstercat.api.response.Track
import xerus.monstercat.monsterUtilities
import java.io.IOException
import java.nio.file.Files
import java.nio.file.StandardOpenOption
import java.util.*
Expand Down Expand Up @@ -296,11 +297,15 @@ object Player: FadingHBox(true, targetHeight = 25) {
logger.debug("Updating cover: $coverUrl")
this.coverUrl = coverUrl
GlobalScope.launch {
val image: Image? = coverUrl?.let { Covers.getThumbnailImage(it) }
onFx {
backgroundCover.value = image?.let {
Background(BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.CENTER, BackgroundSize(100.0, 100.0, true, true, true, true)))
try {
val image: Image? = coverUrl?.let { Covers.getThumbnailImage(it) }
onFx {
backgroundCover.value = image?.let {
Background(BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.CENTER, BackgroundSize(100.0, 100.0, true, true, true, true)))
}
}
} catch (e: IOException) {
logger.warn("Background Cover could not be fetched.", e)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/xerus/monstercat/downloader/SongView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ class SongView(private val sorter: ObservableValue<ReleaseSorting>):
setOnMouseClicked {
if(it.clickCount == 2) {
val selected = selectionModel.selectedItem ?: return@setOnMouseClicked
val value = selected.value
when(value) {
when(val value = selected.value) {
is Release -> Player.play(value)
is Track -> Player.playTrack(value)
else -> selected.isExpanded = !selected.isExpanded
Expand Down Expand Up @@ -219,10 +218,11 @@ class SongView(private val sorter: ObservableValue<ReleaseSorting>):
}
GlobalScope.launch(globalDispatcher) {
var image = Covers.getThumbnailImage(release.coverUrl, 16)
image.onError {
fun invalidateImage() {
image = Covers.getThumbnailImage(release.coverUrl, 16, true)
image.onError { logger.debug("Failed to load coverUrl ${release.coverUrl} for $release", it) }
}
image.onError { invalidateImage() }
onFx {
treeItem.graphic = ImageView(image)
done++
Expand Down