Renamed track to song to prevent adblocker from blocking the endpoints

This commit is contained in:
FyloZ 2022-12-22 13:33:34 -05:00
parent c0c7a56f96
commit a73ba4cd77
Signed by: william
GPG Key ID: 835378AE9AF4AE97
30 changed files with 276 additions and 227 deletions

View File

@ -28,6 +28,7 @@ dependencies {
implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-server-call-logging-jvm:$ktor_version")
implementation("io.ktor:ktor-server-cors:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version")
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
implementation("io.ktor:ktor-client-core:$ktor_version")
@ -40,6 +41,7 @@ dependencies {
implementation("io.insert-koin:koin-ktor:$koin_version")
implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-server-cors-jvm:2.1.3")
testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")

View File

@ -2,13 +2,13 @@ package dev.fyloz.musicplayer.core
import com.typesafe.config.ConfigFactory
import dev.fyloz.musicplayer.core.data.RepositoryInjection
import dev.fyloz.musicplayer.core.factory.TrackFactoryProxy
import dev.fyloz.musicplayer.core.factory.SongFactoryProxy
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.http.configureTrackRoutes
import dev.fyloz.musicplayer.core.logic.TrackLogic
import dev.fyloz.musicplayer.core.http.configureSongRoutes
import dev.fyloz.musicplayer.core.logic.SongLogic
import dev.fyloz.musicplayer.modules.Module
import io.ktor.client.*
import io.ktor.client.plugins.logging.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.config.*
@ -16,6 +16,7 @@ import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.callloging.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.routing.*
import kotlinx.serialization.json.Json
import org.koin.dsl.module
@ -50,8 +51,8 @@ fun Application.module() {
RepositoryInjection.koinBeans,
module {
single { TrackFactoryProxy() }
single { TrackLogic() }
single { SongFactoryProxy() }
single { SongLogic() }
single {
HttpClient {
install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) {
@ -60,10 +61,10 @@ fun Application.module() {
})
}
install(Logging) {
level = LogLevel.ALL
// filter { call -> call.request.path().startsWith("/") }
}
// install(Logging) {
// level = LogLevel.ALL
//// filter { call -> call.request.path().startsWith("/") }
// }
}
}
@ -79,9 +80,19 @@ fun Application.module() {
registeredModules.values.forEach { it.configureRoutes(this) }
route("/api/v1") {
configureTrackRoutes()
configureSongRoutes()
}
}
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch)
allowHeader(HttpHeaders.Authorization)
allowHeader("MyCustomHeader")
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
}
}
private val registeredModules = mutableMapOf<String, Module>()

View File

@ -1,10 +1,10 @@
package dev.fyloz.musicplayer.core.data
import dev.fyloz.musicplayer.core.data.memory.TrackMemoryRepository
import dev.fyloz.musicplayer.core.data.memory.SongMemoryRepository
import org.koin.dsl.module
object RepositoryInjection {
val koinBeans = module {
single<TrackRepository> { TrackMemoryRepository() }
single<SongRepository> { SongMemoryRepository() }
}
}

View File

@ -0,0 +1,5 @@
package dev.fyloz.musicplayer.core.data
import dev.fyloz.musicplayer.core.model.Song
interface SongRepository : Repository<Song>

View File

@ -1,5 +0,0 @@
package dev.fyloz.musicplayer.core.data
import dev.fyloz.musicplayer.core.model.Track
interface TrackRepository : Repository<Track>

View File

@ -0,0 +1,8 @@
package dev.fyloz.musicplayer.core.data.memory
import dev.fyloz.musicplayer.core.data.SongRepository
import dev.fyloz.musicplayer.core.model.Song
class SongMemoryRepository : BaseMemoryRepository<Song>(), SongRepository {
override fun getId(t: Song) = t.id
}

View File

@ -1,8 +0,0 @@
package dev.fyloz.musicplayer.core.data.memory
import dev.fyloz.musicplayer.core.data.TrackRepository
import dev.fyloz.musicplayer.core.model.Track
class TrackMemoryRepository : BaseMemoryRepository<Track>(), TrackRepository {
override fun getId(t: Track) = t.id
}

View File

@ -0,0 +1,10 @@
package dev.fyloz.musicplayer.core.factory
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.model.SearchResultItem
import dev.fyloz.musicplayer.core.model.Song
interface SongFactory {
suspend fun search(query: String, auth: AuthorizationData): Collection<SearchResultItem>
suspend fun create(id: String, songId: String, auth: AuthorizationData): Song
}

View File

@ -0,0 +1,29 @@
package dev.fyloz.musicplayer.core.factory
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.model.SearchResult
import dev.fyloz.musicplayer.core.model.SearchResultItem
import dev.fyloz.musicplayer.core.model.Song
typealias STR = String
class SongFactoryProxy {
private val factories = mutableMapOf<String, SongFactory>()
fun registerFactory(type: String, factory: SongFactory) {
factories[type] = factory;
}
suspend fun search(query: String, auth: AuthorizationData): SearchResult {
val results = mutableMapOf<String, Collection<SearchResultItem>>()
factories.forEach { (type, factory) ->
results[type] = factory.search(query, auth)
}
return results.toMap()
}
suspend fun create(type: String, id: String, songId: String, auth: AuthorizationData): Song =
getFactory(type).create(id, songId, auth)
private fun getFactory(type: String) = factories[type]!!;
}

View File

@ -1,9 +0,0 @@
package dev.fyloz.musicplayer.core.factory
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.model.Track
interface TrackFactory {
suspend fun searchTrack(query: String, auth: AuthorizationData): Collection<Track>
suspend fun createTrack(id: String, trackId: String, auth: AuthorizationData): Track
}

View File

@ -1,20 +0,0 @@
package dev.fyloz.musicplayer.core.factory
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.model.Track
class TrackFactoryProxy {
private val factories = mutableMapOf<String, TrackFactory>()
fun registerFactory(type: String, factory: TrackFactory) {
factories[type] = factory;
}
suspend fun search(query: String, auth: AuthorizationData) =
factories.values.map { it.searchTrack(query, auth) }.flatten()
suspend fun createTrack(type: String, id: String, trackId: String, auth: AuthorizationData): Track =
getFactory(type).createTrack(id, trackId, auth)
private fun getFactory(type: String) = factories[type]!!;
}

View File

@ -0,0 +1,53 @@
package dev.fyloz.musicplayer.core.http
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
abstract class HttpProvider(private val baseUrl: String) : KoinComponent {
protected val httpClient by inject<HttpClient>()
protected open val acceptContentType = ContentType.Application.Json
protected suspend inline fun <reified T> get(
path: String,
bearer: String? = null,
block: HttpRequestBuilder.() -> Unit = {}
): T =
httpClient.get(path.toUrl()) {
accept(acceptContentType)
headers {
if (bearer != null) {
append(HttpHeaders.Authorization, bearer.toBearer())
}
}
block()
}.process()
protected suspend inline fun <reified T> HttpResponse.process(): T {
if (this.status.isSuccess()) {
return this.body()
}
throw IllegalStateException("TODO: Unsuccessful HTTP request: ${this.body<String>()}")
}
protected fun String.toUrl(): String {
if (baseUrl.endsWith('/') && this.startsWith('/')) {
return baseUrl + this.substring(1, this.length)
}
if (!this.startsWith('/')) {
return "$baseUrl/$this"
}
return baseUrl + this
}
protected fun String.toBearer() = "Bearer $this"
}

View File

@ -0,0 +1,32 @@
package dev.fyloz.musicplayer.core.http
import dev.fyloz.musicplayer.core.getAuthorizationData
import dev.fyloz.musicplayer.core.http.requests.CreateSongRequest
import dev.fyloz.musicplayer.core.logic.SongLogic
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.inject
fun Route.configureSongRoutes() {
val logic by inject<SongLogic>()
route("/song") {
get("/") {
call.respond(logic.getAll().toList())
}
get("/search") {
val query = call.request.queryParameters["q"]!!
val songs = logic.search(query, call.getAuthorizationData())
call.respond(songs)
}
post {
val request = call.receive<CreateSongRequest>()
val song = logic.save(request.type, request.songId, call.getAuthorizationData())
call.respond(song)
}
}
}

View File

@ -1,32 +0,0 @@
package dev.fyloz.musicplayer.core.http
import dev.fyloz.musicplayer.core.getAuthorizationData
import dev.fyloz.musicplayer.core.http.requests.CreateTrackRequest
import dev.fyloz.musicplayer.core.logic.TrackLogic
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.inject
fun Route.configureTrackRoutes() {
val logic by inject<TrackLogic>()
route("/track") {
get("/") {
call.respond(logic.getAll().toList())
}
get("/search") {
val query = call.request.queryParameters["q"]!!
val tracks = logic.search(query, call.getAuthorizationData())
call.respond(tracks)
}
post {
val request = call.receive<CreateTrackRequest>()
val track = logic.save(request.type, request.trackId, call.getAuthorizationData())
call.respond(track)
}
}
}

View File

@ -3,4 +3,4 @@ package dev.fyloz.musicplayer.core.http.requests
import kotlinx.serialization.Serializable
@Serializable
data class CreateTrackRequest(val type: String, val trackId: String)
data class CreateSongRequest(val type: String, val songId: String)

View File

@ -0,0 +1,31 @@
package dev.fyloz.musicplayer.core.logic
import dev.fyloz.musicplayer.core.data.SongRepository
import dev.fyloz.musicplayer.core.factory.SongFactoryProxy
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.model.SearchResult
import dev.fyloz.musicplayer.core.model.Song
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.UUID
class SongLogic : KoinComponent {
private val repository by inject<SongRepository>()
private val songFactory by inject<SongFactoryProxy>()
fun getAll() = repository.findAll()
fun getById(id: String) = repository.findById(id)
suspend fun search(query: String, auth: AuthorizationData): SearchResult {
return songFactory.search(query, auth)
}
suspend fun save(type: String, songId: String, auth: AuthorizationData): Song {
val id = generateId()
val song = songFactory.create(type, id, songId, auth)
repository.save(song)
return song
}
private fun generateId() = UUID.randomUUID().toString()
}

View File

@ -1,30 +0,0 @@
package dev.fyloz.musicplayer.core.logic
import dev.fyloz.musicplayer.core.data.TrackRepository
import dev.fyloz.musicplayer.core.factory.TrackFactoryProxy
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.model.Track
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.UUID
class TrackLogic : KoinComponent {
private val repository by inject<TrackRepository>()
private val trackFactory by inject<TrackFactoryProxy>()
fun getAll() = repository.findAll()
fun getById(id: String) = repository.findById(id)
suspend fun search(query: String, auth: AuthorizationData): Collection<Track> {
return trackFactory.search(query, auth)
}
suspend fun save(type: String, trackId: String, auth: AuthorizationData): Track {
val id = generateId()
val track = trackFactory.createTrack(type, id, trackId, auth)
repository.save(track)
return track
}
private fun generateId() = UUID.randomUUID().toString()
}

View File

@ -0,0 +1,12 @@
package dev.fyloz.musicplayer.core.model
import kotlinx.serialization.Serializable
typealias SearchResult = Map<String, Collection<SearchResultItem>>
@Serializable
data class SearchResultItem(
val songId: String,
val name: String,
val authors: Collection<String>
)

View File

@ -0,0 +1,18 @@
package dev.fyloz.musicplayer.core.model
/**
* A generic song.
*/
abstract class Song {
/** The id of the song in the local system. **/
abstract val id: String
/** The id of the song in the remote service. **/
abstract val songId: String
/** The name of the song. **/
abstract val name: String
/** The name of the authors of the song. **/
abstract val authors: Collection<String>
}

View File

@ -1,15 +0,0 @@
package dev.fyloz.musicplayer.core.model
/**
* A generic track.
*/
abstract class Track {
/** The id of the track in the local system. **/
abstract val id: String
/** The id of the track in the remote service. **/
abstract val trackId: String
/** The name of the track. **/
abstract val name: String
}

View File

@ -1,8 +1,8 @@
package dev.fyloz.musicplayer.modules
import dev.fyloz.musicplayer.core.KoinModule
import dev.fyloz.musicplayer.core.factory.TrackFactory
import dev.fyloz.musicplayer.core.factory.TrackFactoryProxy
import dev.fyloz.musicplayer.core.factory.SongFactory
import dev.fyloz.musicplayer.core.factory.SongFactoryProxy
import io.ktor.server.application.*
import io.ktor.server.config.*
import io.ktor.server.routing.*
@ -14,8 +14,8 @@ abstract class Module(private val moduleName: String) {
open fun configure(app: Application) {
with(app) {
val trackFactoryProxy by inject<TrackFactoryProxy>()
trackFactoryProxy.registerFactory(moduleName, getTrackFactory())
val songFactoryProxy by inject<SongFactoryProxy>()
songFactoryProxy.registerFactory(moduleName, getSongFactory())
}
}
@ -39,5 +39,5 @@ abstract class Module(private val moduleName: String) {
protected open fun Route.configureModuleRoutes() {
}
protected abstract fun getTrackFactory(): TrackFactory
protected abstract fun getSongFactory(): SongFactory
}

View File

@ -1,26 +1,12 @@
package dev.fyloz.musicplayer.modules.spotify
import dev.fyloz.musicplayer.modules.spotify.api.SearchRequest
import dev.fyloz.musicplayer.core.http.HttpProvider
import dev.fyloz.musicplayer.modules.spotify.api.SearchResponse
import dev.fyloz.musicplayer.modules.spotify.api.Track
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class SpotifyApiProvider : KoinComponent {
private val client by inject<HttpClient>()
suspend fun search(query: String, type: String, accessToken: String): Collection<Track> {
val requestBody = SearchRequest(query, type, "audio", 50, 0)
val response = client.get("https://api.spotify.com/v1/search") {
accept(ContentType.Application.Json)
headers {
append(HttpHeaders.Authorization, "Bearer $accessToken")
}
// setBody(requestBody)
class SpotifyApiProvider : HttpProvider("https://api.spotify.com/v1") {
suspend fun search(query: String, type: String, accessToken: String): Collection<Track> =
get<SearchResponse>("search", accessToken) {
url {
parameters.append("q", query)
parameters.append("type", type)
@ -28,19 +14,8 @@ class SpotifyApiProvider : KoinComponent {
parameters.append("limit", "50")
parameters.append("offset", "0")
}
}.body<SearchResponse>()
}.tracks.items
return response.tracks.items
}
suspend fun getTrackById(trackId: String, accessToken: String): Track {
val response = client.get("https://api.spotify.com/v1/tracks/$trackId") {
accept(ContentType.Application.Json)
headers {
append(HttpHeaders.Authorization, "Bearer $accessToken")
}
}
return response.body()
}
suspend fun getSongById(songId: String, accessToken: String): Track =
get("tracks/$songId", accessToken)
}

View File

@ -4,6 +4,6 @@ import org.koin.dsl.module
object SpotifyInjection {
val koinBeans = module {
single<SpotifyTrackFactory> { SpotifyTrackFactory() }
single<SpotifySongFactory> { SpotifySongFactory() }
}
}

View File

@ -59,15 +59,15 @@ class SpotifyModule : Module(moduleName) {
get("/login-callback") {
call.principal<OAuthAccessTokenResponse.OAuth2>()?.let {
with(call.response.cookies) {
append("Spotify-Access-Token", it.accessToken, maxAge = it.expiresIn)
append("Spotify-Refresh-Token", it.refreshToken!!)
append("Spotify-Access-Token", it.accessToken, maxAge = it.expiresIn, path = "/api/v1")
append("Spotify-Refresh-Token", it.refreshToken!!, path = "/api/v1")
}
}
}
}
}
override fun getTrackFactory() = SpotifyTrackFactory()
override fun getSongFactory() = SpotifySongFactory()
companion object {
const val moduleName = "spotify"

View File

@ -0,0 +1,12 @@
package dev.fyloz.musicplayer.modules.spotify
import dev.fyloz.musicplayer.core.model.Song
import kotlinx.serialization.Serializable
@Serializable
data class SpotifySong(
override val id: String,
override val songId: String,
override val name: String,
override val authors: Collection<String>
) : Song()

View File

@ -0,0 +1,23 @@
package dev.fyloz.musicplayer.modules.spotify
import dev.fyloz.musicplayer.core.factory.SongFactory
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.model.SearchResultItem
import dev.fyloz.musicplayer.core.model.Song
class SpotifySongFactory : SongFactory {
private val apiProvider = SpotifyApiProvider()
override suspend fun search(query: String, auth: AuthorizationData): Collection<SearchResultItem> {
val apiSongs = apiProvider.search(query, "track", auth.accessToken)
return apiSongs.map { SearchResultItem(it.id, it.name, it.artists.map { a -> a.name }) }
}
override suspend fun create(id: String, songId: String, auth: AuthorizationData): Song {
val spotifyApiSong = apiProvider.getSongById(songId, auth.accessToken);
return SpotifySong(id, songId, spotifyApiSong.name, spotifyApiSong.artists.map { it.name })
}
private val AuthorizationData.accessToken
get() = getModuleData<SpotifyAuthorizationData>(SpotifyModule.moduleName).accessToken
}

View File

@ -1,11 +0,0 @@
package dev.fyloz.musicplayer.modules.spotify
import dev.fyloz.musicplayer.core.model.Track
import kotlinx.serialization.Serializable
@Serializable
data class SpotifyTrack(
override val id: String,
override val trackId: String,
override val name: String
) : Track()

View File

@ -1,22 +0,0 @@
package dev.fyloz.musicplayer.modules.spotify
import dev.fyloz.musicplayer.core.factory.TrackFactory
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.model.Track
class SpotifyTrackFactory : TrackFactory {
private val apiProvider = SpotifyApiProvider()
override suspend fun searchTrack(query: String, auth: AuthorizationData): Collection<Track> {
val apiTracks = apiProvider.search(query, "track", auth.accessToken)
return apiTracks.map { SpotifyTrack("not-an-id", it.id, it.name) }
}
override suspend fun createTrack(id: String, trackId: String, auth: AuthorizationData): Track {
val spotifyApiTrack = apiProvider.getTrackById(trackId, auth.accessToken);
return SpotifyTrack(id, trackId, spotifyApiTrack.name)
}
private val AuthorizationData.accessToken
get() = getModuleData<SpotifyAuthorizationData>(SpotifyModule.moduleName).accessToken
}

View File

@ -1,19 +0,0 @@
package dev.fyloz.musicplayer.modules.spotify.api
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SearchRequest(
@SerialName("q")
val query: String,
val type: String,
@SerialName("include_external")
val includeExternal: String,
val limit: Int,
val offset: Int
)

View File

@ -1,6 +1,5 @@
package dev.fyloz.musicplayer.modules.spotify.api
import dev.fyloz.musicplayer.modules.spotify.SpotifyTrack
import kotlinx.serialization.Serializable
@Serializable