Skip to content

Commit

Permalink
Cleaned up storing analysis and audio files
Browse files Browse the repository at this point in the history
  • Loading branch information
floriegl committed Jan 19, 2025
1 parent d3a9bda commit 2dbc443
Show file tree
Hide file tree
Showing 13 changed files with 325 additions and 585 deletions.
2 changes: 1 addition & 1 deletion src/main/kotlin/org/abimon/eternalJukebox/MediaWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ object MediaWrapper {
return ffmpegProcess.exitValue() == 0
}

ffmpegProcess.destroyForcibly()
ffmpegProcess.destroyForcibly().waitFor()
return false
}
}
Expand Down
10 changes: 0 additions & 10 deletions src/main/kotlin/org/abimon/eternalJukebox/VertxExtensions.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.abimon.eternalJukebox

import io.vertx.core.buffer.Buffer
import io.vertx.core.http.HttpServerResponse
import io.vertx.core.json.JsonArray
import io.vertx.core.json.JsonObject
Expand All @@ -11,8 +10,6 @@ import io.vertx.kotlin.coroutines.dispatcher
import kotlinx.coroutines.launch
import org.abimon.eternalJukebox.objects.ClientInfo
import org.abimon.eternalJukebox.objects.ConstantValues
import org.abimon.visi.io.DataSource
import org.abimon.visi.io.readChunked

fun HttpServerResponse.end(json: JsonArray) = putHeader("Content-Type", "application/json").end(json.toString())
fun HttpServerResponse.end(json: JsonObject) = putHeader("Content-Type", "application/json").end(json.toString())
Expand All @@ -27,13 +24,6 @@ fun RoutingContext.endWithStatusCode(statusCode: Int, init: JsonObject.() -> Uni
.end(json.toString())
}

fun HttpServerResponse.end(data: DataSource, contentType: String = "application/octet-stream") {
putHeader("Content-Type", contentType)
putHeader("Content-Length", "${data.size}")
data.use { stream -> stream.readChunked { chunk -> write(Buffer.buffer(chunk)) } }
end()
}

fun HttpServerResponse.redirect(url: String): Unit = putHeader("Location", url).setStatusCode(307).end()

val RoutingContext.clientInfo: ClientInfo
Expand Down
15 changes: 8 additions & 7 deletions src/main/kotlin/org/abimon/eternalJukebox/data/NodeSource.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
package org.abimon.eternalJukebox.data

import com.github.kittinunf.fuel.Fuel
import org.abimon.visi.io.DataSource
import org.abimon.visi.io.HTTPDataSource
import java.net.URL
import io.vertx.ext.web.RoutingContext
import org.abimon.eternalJukebox.redirect
import java.util.*

abstract class NodeSource {
abstract val nodeHosts: Array<String>

private val rng: Random = Random()

fun provide(path: String): DataSource? {
fun provide(path: String, context: RoutingContext): Boolean {
val starting = rng.nextInt(nodeHosts.size)

for (i in nodeHosts.indices) {
val host = nodeHosts[(starting + i) % nodeHosts.size]
val (_, healthy) = Fuel.get("$host/api/node/healthy").timeout(5 * 1000).response()
if (healthy.statusCode == 200)
return HTTPDataSource(URL("$host/api/node/$path"))
if (healthy.statusCode == 200) {
context.response().redirect("$host/api/node/$path")
return true
}
}

return null
return false
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
package org.abimon.eternalJukebox.data.audio

import io.vertx.ext.web.RoutingContext
import org.abimon.eternalJukebox.EternalJukebox
import org.abimon.eternalJukebox.objects.ClientInfo
import org.abimon.eternalJukebox.objects.JukeboxInfo
import org.abimon.visi.io.DataSource
import java.net.URL

@FunctionalInterface
interface IAudioSource {
val audioSourceOptions
get() = EternalJukebox.config.audioSourceOptions
/**
* Provide the audio data for a required song
* Returns a data source pointing to a **valid audio file**, or null if none can be obtained
* Provide the audio data for a required song to the routing context.
* Returns true if handled; false otherwise.
*/
suspend fun provide(info: JukeboxInfo, clientInfo: ClientInfo?): DataSource?
suspend fun provide(info: JukeboxInfo, context: RoutingContext): Boolean

/**
* Provide a location for a required song
* The provided location may not be a direct download link, and may not contain valid audio data.
* The provided location, however, should be a link to said song where possible, or return null if nothing could be found.
*/
suspend fun provideLocation(info: JukeboxInfo, clientInfo: ClientInfo?): URL? = null
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package org.abimon.eternalJukebox.data.audio

import io.vertx.ext.web.RoutingContext
import org.abimon.eternalJukebox.clientInfo
import org.abimon.eternalJukebox.data.NodeSource
import org.abimon.eternalJukebox.objects.ClientInfo
import org.abimon.eternalJukebox.objects.JukeboxInfo
import org.abimon.visi.io.DataSource

@Suppress("UNCHECKED_CAST")
object NodeAudioSource: NodeSource(), IAudioSource {
@Suppress("JoinDeclarationAndAssignment")
override val nodeHosts: Array<String>

override suspend fun provide(info: JukeboxInfo, clientInfo: ClientInfo?): DataSource? = provide("audio/${info.id}?user_uid=${clientInfo?.userUID}")
override suspend fun provide(info: JukeboxInfo, context: RoutingContext): Boolean = provide("audio/${info.id}?user_uid=${context.clientInfo.userUID}", context)

init {
nodeHosts = if (audioSourceOptions.containsKey("NODE_HOST"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package org.abimon.eternalJukebox.data.audio

import com.github.kittinunf.fuel.Fuel
import io.vertx.ext.web.RoutingContext
import kotlinx.coroutines.*
import org.abimon.eternalJukebox.EternalJukebox
import org.abimon.eternalJukebox.MediaWrapper
import org.abimon.eternalJukebox.guaranteeDelete
import org.abimon.eternalJukebox.*
import org.abimon.eternalJukebox.objects.*
import org.abimon.eternalJukebox.useThenDelete
import org.abimon.visi.io.DataSource
import org.abimon.visi.io.FileDataSource
import org.schabi.newpipe.extractor.*
import org.schabi.newpipe.extractor.search.SearchInfo
Expand Down Expand Up @@ -56,8 +53,8 @@ object YoutubeAudioSource : IAudioSource, CoroutineScope {
private val hitQuota = AtomicLong(-1)
private val QUOTA_TIMEOUT = TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES)

override suspend fun provide(info: JukeboxInfo, clientInfo: ClientInfo?): DataSource? {
logger.trace("[{}] Attempting to provide audio for {}", clientInfo?.userUID, info.id)
override suspend fun provide(info: JukeboxInfo, context: RoutingContext): Boolean {
logger.trace("[{}] Attempting to provide audio for {}", context.clientInfo.userUID, info.id)

var youTubeUrl: String? = null
val queryText = "${info.artist} - ${info.title}"
Expand All @@ -74,21 +71,21 @@ object YoutubeAudioSource : IAudioSource, CoroutineScope {

videoDetails.minByOrNull { abs(info.duration - it.contentDetails.duration.toMillis()) }
?.let { youTubeUrl = VIDEO_LINK_PREFIX + it.id } ?: run {
logger.warn("[${clientInfo?.userUID}] Searches for \"$queryText\" using YouTube Data API v3 turned up nothing")
logger.warn("[${context.clientInfo.userUID}] Searches for \"$queryText\" using YouTube Data API v3 turned up nothing")
}
}
if (youTubeUrl == null) {
val infoItems = getInfoItemsFromNewPipeSearch(queryText)

infoItems.minByOrNull { abs(info.duration - TimeUnit.SECONDS.toMillis(it.duration)) }
?.let { youTubeUrl = it.url } ?: run {
logger.error("[${clientInfo?.userUID}] Searches for \"$queryText\" using NewPipeExtractor turned up nothing")
logger.error("[${context.clientInfo.userUID}] Searches for \"$queryText\" using NewPipeExtractor turned up nothing")
}
}

if (youTubeUrl == null) return null
if (youTubeUrl == null) return false
logger.trace(
"[{}] Settled on {}", clientInfo?.userUID, youTubeUrl
"[{}] Settled on {}", context.clientInfo.userUID, youTubeUrl
)

val tmpFile = File("$uuid.tmp")
Expand All @@ -110,33 +107,33 @@ object YoutubeAudioSource : IAudioSource, CoroutineScope {
if (!downloadProcess.waitFor(90, TimeUnit.SECONDS)) {
downloadProcess.destroyForcibly().waitFor()
logger.error(
"[{}] Forcibly destroyed the download process for {}", clientInfo?.userUID, youTubeUrl
"[{}] Forcibly destroyed the download process for {}", context.clientInfo.userUID, youTubeUrl
)
}
}

if (!endGoalTmp.exists()) {
logger.warn(
"[{}] {} does not exist, attempting to convert with ffmpeg", clientInfo?.userUID, endGoalTmp
"[{}] {} does not exist, attempting to convert with ffmpeg", context.clientInfo.userUID, endGoalTmp
)

if (!tmpFile.exists()) {
logger.error("[{}] {} does not exist, what happened?", clientInfo?.userUID, tmpFile)
return null
logger.error("[{}] {} does not exist, what happened?", context.clientInfo.userUID, tmpFile)
return false
}

if (MediaWrapper.ffmpeg.installed) {
if (withContext(Dispatchers.IO) { !MediaWrapper.ffmpeg.convert(tmpFile, endGoalTmp, ffmpegLog) }) {
logger.error("[{}] Failed to convert {} to {}. Check {}", clientInfo?.userUID, tmpFile, endGoalTmp, ffmpegLog.name)
return null
logger.error("[{}] Failed to convert {} to {}. Check {}", context.clientInfo.userUID, tmpFile, endGoalTmp, ffmpegLog.name)
return false
}

if (!endGoalTmp.exists()) {
logger.error("[{}] {} does not exist, check {}", clientInfo?.userUID, endGoalTmp, ffmpegLog.name)
return null
logger.error("[{}] {} does not exist, check {}", context.clientInfo.userUID, endGoalTmp, ffmpegLog.name)
return false
}
} else {
logger.debug("[{}] ffmpeg not installed, nothing we can do", clientInfo?.userUID)
logger.debug("[{}] ffmpeg not installed, nothing we can do", context.clientInfo.userUID)
}
}

Expand All @@ -151,32 +148,32 @@ object YoutubeAudioSource : IAudioSource, CoroutineScope {
}
if (videoId != null) {
logger.debug("Storing Location from yt-dlp")
EternalJukebox.database.storeAudioLocation(info.id, VIDEO_LINK_PREFIX + videoId, clientInfo)
EternalJukebox.database.storeAudioLocation(info.id, VIDEO_LINK_PREFIX + videoId, context.clientInfo)
}
endGoalTmp.useThenDelete {
EternalJukebox.storage.store(
"${info.id}.$format",
EnumStorageType.AUDIO,
FileDataSource(it),
mimes[format] ?: "audio/mpeg",
clientInfo
context.clientInfo
)
}
}

return EternalJukebox.storage.provide("${info.id}.$format", EnumStorageType.AUDIO, clientInfo)
return EternalJukebox.storage.safeProvide("${info.id}.$format", EnumStorageType.AUDIO, context)
} finally {
tmpFile.guaranteeDelete()
File(tmpFile.absolutePath + ".part").guaranteeDelete()
withContext(Dispatchers.IO) {
tmpLog.useThenDelete {
EternalJukebox.storage.store(
it.name, EnumStorageType.LOG, FileDataSource(it), "text/plain", clientInfo
it.name, EnumStorageType.LOG, FileDataSource(it), "text/plain", context.clientInfo
)
}
ffmpegLog.useThenDelete {
EternalJukebox.storage.store(
it.name, EnumStorageType.LOG, FileDataSource(it), "text/plain", clientInfo
it.name, EnumStorageType.LOG, FileDataSource(it), "text/plain", context.clientInfo
)
}
endGoalTmp.useThenDelete {
Expand All @@ -185,7 +182,7 @@ object YoutubeAudioSource : IAudioSource, CoroutineScope {
EnumStorageType.AUDIO,
FileDataSource(it),
mimes[format] ?: "audio/mpeg",
clientInfo
context.clientInfo
)
}
}
Expand Down
Loading

0 comments on commit 2dbc443

Please sign in to comment.