This commit is contained in:
william 2022-12-17 21:26:30 -05:00
parent 1e9be2c7c2
commit c0c7a56f96
20 changed files with 273 additions and 120 deletions

View File

@ -32,13 +32,14 @@ dependencies {
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
implementation("io.ktor:ktor-client-core:$ktor_version") implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-server-auth:$ktor_version") implementation("io.ktor:ktor-server-auth:$ktor_version")
implementation("io.ktor:ktor-server-auth-jvm:$ktor_version")
implementation("io.ktor:ktor-server-locations-jvm:$ktor_version")
implementation("io.ktor:ktor-client-core-jvm:$ktor_version")
implementation("io.ktor:ktor-client-apache-jvm:$ktor_version")
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
implementation("io.insert-koin:koin-ktor:$koin_version") implementation("io.insert-koin:koin-ktor:$koin_version")
implementation("io.insert-koin:koin-logger-slf4j:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
implementation("ch.qos.logback:logback-classic:$logback_version") implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-server-auth-jvm:2.1.3")
implementation("io.ktor:ktor-server-locations-jvm:2.1.3")
implementation("io.ktor:ktor-client-core-jvm:2.1.3")
implementation("io.ktor:ktor-client-apache-jvm:2.1.3")
testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")

View File

@ -1,27 +1,24 @@
package dev.fyloz.musicplayer.core package dev.fyloz.musicplayer.core
import com.typesafe.config.ConfigFactory
import dev.fyloz.musicplayer.core.data.RepositoryInjection import dev.fyloz.musicplayer.core.data.RepositoryInjection
import dev.fyloz.musicplayer.core.factory.TrackFactoryProxy import dev.fyloz.musicplayer.core.factory.TrackFactoryProxy
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.http.configureTrackRoutes import dev.fyloz.musicplayer.core.http.configureTrackRoutes
import dev.fyloz.musicplayer.core.logic.TrackLogic import dev.fyloz.musicplayer.core.logic.TrackLogic
import dev.fyloz.musicplayer.modules.spotify.SpotifyInjection import dev.fyloz.musicplayer.modules.Module
import dev.fyloz.musicplayer.modules.spotify.SpotifyModule
import dev.fyloz.musicplayer.modules.spotify.SpotifyTrackFactory
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.apache.* import io.ktor.client.plugins.logging.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.* import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.config.*
import io.ktor.server.engine.* import io.ktor.server.engine.*
import io.ktor.server.netty.* import io.ktor.server.netty.*
import io.ktor.server.plugins.callloging.* import io.ktor.server.plugins.callloging.*
import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.ktor.ext.inject
import org.koin.ktor.plugin.Koin import org.koin.ktor.plugin.Koin
import org.koin.logger.slf4jLogger import org.koin.logger.slf4jLogger
import org.slf4j.event.Level import org.slf4j.event.Level
@ -32,6 +29,11 @@ fun main() {
} }
fun Application.module() { fun Application.module() {
val config = HoconApplicationConfig(ConfigFactory.load())
log.info("Loading modules...")
registerModules(config)
install(CallLogging) { install(CallLogging) {
level = Level.DEBUG level = Level.DEBUG
// filter { call -> call.request.path().startsWith("/") } // filter { call -> call.request.path().startsWith("/") }
@ -46,24 +48,72 @@ fun Application.module() {
slf4jLogger() slf4jLogger()
modules( modules(
RepositoryInjection.koinBeans, RepositoryInjection.koinBeans,
SpotifyInjection.koinBeans,
module { module {
single { TrackFactoryProxy() } single { TrackFactoryProxy() }
single { TrackLogic() } single { TrackLogic() }
single {
HttpClient {
install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
install(Logging) {
level = LogLevel.ALL
// filter { call -> call.request.path().startsWith("/") }
}
}
}
registeredModules.values.forEach { it.setupDependencyInjection(this, config) }
} }
) )
} }
} }
val spotifyModule = SpotifyModule() registeredModules.values.forEach { it.configure(this) }
spotifyModule.configure(this)
install(Routing) { install(Routing) {
spotifyModule.configureRoutes(this) registeredModules.values.forEach { it.configureRoutes(this) }
route("/api/v1") { route("/api/v1") {
configureTrackRoutes() configureTrackRoutes()
} }
} }
} }
private val registeredModules = mutableMapOf<String, Module>()
fun ApplicationCall.getAuthorizationData(): AuthorizationData {
val auth = AuthorizationData()
registeredModules.forEach { (moduleName, module) ->
val data = module.getAuthorizationData(this)
if (data != null) {
auth.addModuleData(data, moduleName)
}
}
return auth
}
private fun Application.registerModules(config: ApplicationConfig) {
val enabledModules = config.property("modules.enabled").getList()
enabledModules.forEach { registerModule(it, config) }
}
private fun Application.registerModule(moduleName: String, config: ApplicationConfig) {
log.debug("Found module '$moduleName'")
val moduleClass = config.property("modules.$moduleName.class").getString()
val module = loadModule(moduleClass)
registeredModules[moduleName] = module
log.info("Registered module '$moduleName'")
}
private fun Application.loadModule(moduleClass: String): Module {
log.debug("Loading module class '$moduleClass'...")
return Class.forName(moduleClass).getDeclaredConstructor().newInstance() as Module
}

View File

@ -0,0 +1,3 @@
package dev.fyloz.musicplayer.core
typealias KoinModule = org.koin.core.module.Module

View File

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

View File

@ -1,5 +1,6 @@
package dev.fyloz.musicplayer.core.factory package dev.fyloz.musicplayer.core.factory
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.model.Track import dev.fyloz.musicplayer.core.model.Track
class TrackFactoryProxy { class TrackFactoryProxy {
@ -9,9 +10,11 @@ class TrackFactoryProxy {
factories[type] = factory; factories[type] = factory;
} }
suspend fun createTrack(type: String, id: String, trackId: String): Track { suspend fun search(query: String, auth: AuthorizationData) =
return getFactory(type).createTrack(id, trackId) 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]!!; private fun getFactory(type: String) = factories[type]!!;
} }

View File

@ -1,5 +1,6 @@
package dev.fyloz.musicplayer.core.http 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.http.requests.CreateTrackRequest
import dev.fyloz.musicplayer.core.logic.TrackLogic import dev.fyloz.musicplayer.core.logic.TrackLogic
import io.ktor.server.application.* import io.ktor.server.application.*
@ -16,9 +17,15 @@ fun Route.configureTrackRoutes() {
call.respond(logic.getAll().toList()) call.respond(logic.getAll().toList())
} }
get("/search") {
val query = call.request.queryParameters["q"]!!
val tracks = logic.search(query, call.getAuthorizationData())
call.respond(tracks)
}
post { post {
val request = call.receive<CreateTrackRequest>() val request = call.receive<CreateTrackRequest>()
val track = logic.save(request.type, request.trackId) val track = logic.save(request.type, request.trackId, call.getAuthorizationData())
call.respond(track) call.respond(track)
} }
} }

View File

@ -0,0 +1,14 @@
package dev.fyloz.musicplayer.core.http.auth
class AuthorizationData {
private val modulesData = mutableMapOf<String, Any>()
fun <T : Any> addModuleData(data: T, moduleName: String) {
modulesData[moduleName] = data;
}
@Suppress("UNCHECKED_CAST")
fun <T : Any> getModuleData(moduleName: String): T {
return modulesData[moduleName] as T
}
}

View File

@ -2,8 +2,8 @@ package dev.fyloz.musicplayer.core.logic
import dev.fyloz.musicplayer.core.data.TrackRepository import dev.fyloz.musicplayer.core.data.TrackRepository
import dev.fyloz.musicplayer.core.factory.TrackFactoryProxy import dev.fyloz.musicplayer.core.factory.TrackFactoryProxy
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.model.Track import dev.fyloz.musicplayer.core.model.Track
import dev.fyloz.musicplayer.modules.spotify.SpotifyTrackFactory
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.util.UUID import java.util.UUID
@ -15,9 +15,13 @@ class TrackLogic : KoinComponent {
fun getAll() = repository.findAll() fun getAll() = repository.findAll()
fun getById(id: String) = repository.findById(id) fun getById(id: String) = repository.findById(id)
suspend fun save(type: String, trackId: String): Track { 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 id = generateId()
val track = trackFactory.createTrack(type, id, trackId) val track = trackFactory.createTrack(type, id, trackId, auth)
repository.save(track) repository.save(track)
return track return track
} }

View File

@ -1,13 +0,0 @@
package dev.fyloz.musicplayer.core.plugins
import io.ktor.server.plugins.callloging.*
import org.slf4j.event.*
import io.ktor.server.request.*
import io.ktor.server.application.*
fun Application.configureMonitoring() {
install(CallLogging) {
level = Level.INFO
filter { call -> call.request.path().startsWith("/") }
}
}

View File

@ -1,12 +1,17 @@
package dev.fyloz.musicplayer.modules package dev.fyloz.musicplayer.modules
import dev.fyloz.musicplayer.core.KoinModule
import dev.fyloz.musicplayer.core.factory.TrackFactory import dev.fyloz.musicplayer.core.factory.TrackFactory
import dev.fyloz.musicplayer.core.factory.TrackFactoryProxy import dev.fyloz.musicplayer.core.factory.TrackFactoryProxy
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.config.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import org.koin.ktor.ext.inject import org.koin.ktor.ext.inject
abstract class Module(private val moduleName: String) { abstract class Module(private val moduleName: String) {
open fun setupDependencyInjection(module: KoinModule, config: ApplicationConfig) {
}
open fun configure(app: Application) { open fun configure(app: Application) {
with(app) { with(app) {
val trackFactoryProxy by inject<TrackFactoryProxy>() val trackFactoryProxy by inject<TrackFactoryProxy>()
@ -26,6 +31,8 @@ abstract class Module(private val moduleName: String) {
} }
} }
open fun getAuthorizationData(call: ApplicationCall): Any? = null
protected open fun Route.configureApiRoutes() { protected open fun Route.configureApiRoutes() {
} }

View File

@ -0,0 +1,46 @@
package dev.fyloz.musicplayer.modules.spotify
import dev.fyloz.musicplayer.modules.spotify.api.SearchRequest
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)
url {
parameters.append("q", query)
parameters.append("type", type)
parameters.append("include_external", "audio")
parameters.append("limit", "50")
parameters.append("offset", "0")
}
}.body<SearchResponse>()
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()
}
}

View File

@ -0,0 +1,3 @@
package dev.fyloz.musicplayer.modules.spotify
data class SpotifyAuthorizationData(val accessToken: String)

View File

@ -1,34 +0,0 @@
package dev.fyloz.musicplayer.modules.spotify
import dev.fyloz.musicplayer.modules.spotify.api.Track
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
class SpotifyHttpProvider {
private val client = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
suspend fun getTrackById(trackId: String, token: String): Track {
val tmpToken =
"Bearer BQAtodg3W_kHLVJhGLFD0YEOh5AFfxSsrvhk1n9c0ELrlQdoEkbyOXYncN1k3umPfqqR3sesLI8qpJMxJ6-98nc4Z7KenedLBGyHfvBPzWg_U8OtwWy6jEWl7MbeFJYO50GNj-XRLPv6EAA1hPrrSIOhJBvOygs6hcDAYfD-_moA"
val response = client.get("https://api.spotify.com/v1/tracks/$trackId") {
accept(ContentType.Application.Json)
headers {
append(HttpHeaders.Authorization, tmpToken)
}
}
return response.body()
}
}

View File

@ -1,58 +1,76 @@
package dev.fyloz.musicplayer.modules.spotify package dev.fyloz.musicplayer.modules.spotify
import dev.fyloz.musicplayer.core.KoinModule
import dev.fyloz.musicplayer.modules.Module import dev.fyloz.musicplayer.modules.Module
import dev.fyloz.musicplayer.modules.spotify.config.SpotifyConfiguration
import io.ktor.client.* import io.ktor.client.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.config.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import org.koin.ktor.ext.inject
class SpotifyModule : Module(moduleName) {
override fun setupDependencyInjection(module: KoinModule, config: ApplicationConfig) {
with(module) {
single { SpotifyConfiguration.fromEnvironment(config) }
}
}
class SpotifyModule : Module("spotify") {
override fun configure(app: Application) { override fun configure(app: Application) {
super.configure(app) super.configure(app)
val httpClient by app.inject<HttpClient>()
val config by app.inject<SpotifyConfiguration>()
with(app) { with(app) {
authentication { authentication {
oauth("auth-oauth-spotify") { oauth(oauthName) {
urlProvider = { "http://localhost:8080/module/spotify/login-callback" } urlProvider = { "http://localhost:8080/module/spotify/login-callback" }
providerLookup = { providerLookup = {
OAuthServerSettings.OAuth2ServerSettings( OAuthServerSettings.OAuth2ServerSettings(
name = "spotify", name = moduleName,
authorizeUrl = "https://accounts.spotify.com/authorize", authorizeUrl = "https://accounts.spotify.com/authorize",
accessTokenUrl = "https://accounts.spotify.com/api/token", accessTokenUrl = "https://accounts.spotify.com/api/token",
requestMethod = HttpMethod.Post, requestMethod = HttpMethod.Post,
clientId = "1372bd3ebcad4f889994f9a3f675472b", clientId = config.clientId,
clientSecret = "26ac249dc5ca4a309aa08f8cfcec9a60" clientSecret = config.clientSecret,
defaultScopes = listOf("user-read-private user-read-email")
) )
} }
client = HttpClient() client = httpClient
} }
} }
} }
} }
override fun getAuthorizationData(call: ApplicationCall) = SpotifyAuthorizationData(
call.request.cookies["Spotify-Access-Token"]!!
)
override fun Route.configureModuleRoutes() { override fun Route.configureModuleRoutes() {
authenticate("auth-oauth-spotify") { authenticate(oauthName) {
get("/login") { get("/login") {
call.respondRedirect("https://accounts.spotify.com/authorize") call.respondRedirect("https://accounts.spotify.com/authorize")
} }
get("/login-callback") { get("/login-callback") {
val principal: OAuthAccessTokenResponse.OAuth2? = call.principal() call.principal<OAuthAccessTokenResponse.OAuth2>()?.let {
if (principal != null) { with(call.response.cookies) {
call.response.cookies.append("Spotify-Access-Token", principal.accessToken) append("Spotify-Access-Token", it.accessToken, maxAge = it.expiresIn)
append("Spotify-Refresh-Token", it.refreshToken!!)
if (principal.refreshToken != null) {
call.response.cookies.append("Spotify-Refresh-Token", principal.refreshToken!!)
} }
// SpotifyTokenLogic(http)
} else {
call.respond(HttpStatusCode.Unauthorized)
} }
} }
} }
} }
override fun getTrackFactory() = SpotifyTrackFactory() override fun getTrackFactory() = SpotifyTrackFactory()
companion object {
const val moduleName = "spotify"
const val oauthName = "auth-oauth-spotify"
}
} }

View File

@ -1,29 +0,0 @@
package dev.fyloz.musicplayer.modules.spotify
import dev.fyloz.musicplayer.modules.spotify.api.AccessTokenRequest
import dev.fyloz.musicplayer.modules.spotify.api.AccessTokenResponse
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.util.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class SpotifyTokenLogic : KoinComponent {
private val httpClient by inject<HttpClient>()
suspend fun getAccessToken(authorizationCode: String) {
val authorizationHeader = "1372bd3ebcad4f889994f9a3f675472b:26ac249dc5ca4a309aa08f8cfcec9a60".encodeBase64()
val call = httpClient.post("https://accounts.spotify.com/api/token") {
contentType(ContentType.Application.FormUrlEncoded)
accept(ContentType.Application.Json)
setBody(AccessTokenRequest(authorizationCode, "http://localhost:8080/module/spotify/login-callback"))
header("Authorization", "Basic $authorizationHeader")
}
val response = call.body<AccessTokenResponse>()
println(response.accessToken)
}
}

View File

@ -1,14 +1,22 @@
package dev.fyloz.musicplayer.modules.spotify package dev.fyloz.musicplayer.modules.spotify
import dev.fyloz.musicplayer.core.factory.TrackFactory import dev.fyloz.musicplayer.core.factory.TrackFactory
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.model.Track import dev.fyloz.musicplayer.core.model.Track
class SpotifyTrackFactory : TrackFactory { class SpotifyTrackFactory : TrackFactory {
private val httpProvider = SpotifyHttpProvider() private val apiProvider = SpotifyApiProvider()
override suspend fun createTrack(id: String, trackId: String): Track { override suspend fun searchTrack(query: String, auth: AuthorizationData): Collection<Track> {
val spotifyApiTrack = httpProvider.getTrackById(trackId, "bla"); 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) return SpotifyTrack(id, trackId, spotifyApiTrack.name)
} }
private val AuthorizationData.accessToken
get() = getModuleData<SpotifyAuthorizationData>(SpotifyModule.moduleName).accessToken
} }

View File

@ -0,0 +1,19 @@
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

@ -0,0 +1,20 @@
package dev.fyloz.musicplayer.modules.spotify.api
import dev.fyloz.musicplayer.modules.spotify.SpotifyTrack
import kotlinx.serialization.Serializable
@Serializable
data class SearchResponse(
val tracks: TrackSearchResponse
)
@Serializable
data class TrackSearchResponse(
val href: String,
val items: Collection<Track>,
val limit: Int,
val next: String,
val offset: Int,
val previous: String?,
val total: Int
)

View File

@ -0,0 +1,15 @@
package dev.fyloz.musicplayer.modules.spotify.config
import io.ktor.server.config.*
data class SpotifyConfiguration(
val clientId: String,
val clientSecret: String
) {
companion object {
fun fromEnvironment(config: ApplicationConfig) = SpotifyConfiguration(
config.property("modules.spotify.clientId").getString(),
config.property("modules.spotify.clientSecret").getString()
)
}
}

View File

@ -0,0 +1,9 @@
modules {
enabled = [ "spotify" ]
spotify {
class = "dev.fyloz.musicplayer.modules.spotify.SpotifyModule"
clientId = "1372bd3ebcad4f889994f9a3f675472b"
clientSecret = "26ac249dc5ca4a309aa08f8cfcec9a60"
}
}