This commit is contained in:
william 2023-01-24 22:39:09 -05:00
parent a73ba4cd77
commit 71cb4abae9
28 changed files with 274 additions and 196 deletions

View File

@ -2,10 +2,10 @@ package dev.fyloz.musicplayer.core
import com.typesafe.config.ConfigFactory 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.SongFactoryProxy import dev.fyloz.musicplayer.core.factory.TrackFactoryProxy
import dev.fyloz.musicplayer.core.http.auth.AuthorizationData import dev.fyloz.musicplayer.core.http.auth.AuthorizationData
import dev.fyloz.musicplayer.core.http.configureSongRoutes import dev.fyloz.musicplayer.core.http.configureTrackRoutes
import dev.fyloz.musicplayer.core.logic.SongLogic import dev.fyloz.musicplayer.core.logic.TrackLogic
import dev.fyloz.musicplayer.modules.Module import dev.fyloz.musicplayer.modules.Module
import io.ktor.client.* import io.ktor.client.*
import io.ktor.http.* import io.ktor.http.*
@ -51,8 +51,8 @@ fun Application.module() {
RepositoryInjection.koinBeans, RepositoryInjection.koinBeans,
module { module {
single { SongFactoryProxy() } single { TrackFactoryProxy() }
single { SongLogic() } single { TrackLogic() }
single { single {
HttpClient { HttpClient {
install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) {
@ -80,18 +80,19 @@ fun Application.module() {
registeredModules.values.forEach { it.configureRoutes(this) } registeredModules.values.forEach { it.configureRoutes(this) }
route("/api/v1") { route("/api/v1") {
configureSongRoutes() configureTrackRoutes()
} }
} }
install(CORS) { install(CORS) {
allowMethod(HttpMethod.Options) allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Put) allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete) allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch) allowHeader(HttpHeaders.AccessControlAllowOrigin)
allowHeader(HttpHeaders.Authorization) allowHeader(HttpHeaders.ContentType)
allowHeader("MyCustomHeader")
anyHost() // @TODO: Don't do this in production if possible. Try to limit it. anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
allowCredentials = true
} }
} }

View File

@ -0,0 +1,5 @@
package dev.fyloz.musicplayer.core
fun <K, V> MutableMap<K, V>.removeIf(predicate: (V) -> Boolean) {
this.filterValues(predicate).map { it.key }.forEach(this::remove)
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
package dev.fyloz.musicplayer.core.data
import dev.fyloz.musicplayer.core.model.Track
interface TrackRepository : Repository<Track> {
fun deleteByTrackId(trackId: String, source: String)
}

View File

@ -3,7 +3,7 @@ package dev.fyloz.musicplayer.core.data.memory
import dev.fyloz.musicplayer.core.data.Repository import dev.fyloz.musicplayer.core.data.Repository
abstract class BaseMemoryRepository<T> : Repository<T> { abstract class BaseMemoryRepository<T> : Repository<T> {
private val memoryCache = mutableMapOf<String, T>() protected val memoryCache = mutableMapOf<String, T>()
override fun findAll() = memoryCache.values override fun findAll() = memoryCache.values
override fun findById(id: String) = memoryCache[id] override fun findById(id: String) = memoryCache[id]

View File

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

@ -0,0 +1,13 @@
package dev.fyloz.musicplayer.core.data.memory
import dev.fyloz.musicplayer.core.data.TrackRepository
import dev.fyloz.musicplayer.core.model.Track
import dev.fyloz.musicplayer.core.removeIf
class TrackMemoryRepository : BaseMemoryRepository<Track>(), TrackRepository {
override fun deleteByTrackId(trackId: String, source: String) {
memoryCache.removeIf { it.source == source && it.trackId == trackId }
}
override fun getId(t: Track) = t.id
}

View File

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

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

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

View File

@ -0,0 +1,27 @@
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.ExternalTrack
import dev.fyloz.musicplayer.core.model.Track
class TrackFactoryProxy {
private val factories = mutableMapOf<String, TrackFactory>()
fun registerFactory(source: String, factory: TrackFactory) {
factories[source] = factory;
}
suspend fun search(query: String, auth: AuthorizationData): SearchResult {
val results = mutableMapOf<String, Collection<ExternalTrack>>()
factories.forEach { (source, factory) ->
results[source] = factory.search(query, auth)
}
return results.toMap()
}
suspend fun create(source: String, id: String, trackId: String, auth: AuthorizationData): Track =
getFactory(source).create(id, trackId, auth)
private fun getFactory(source: String) = factories[source]!!;
}

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.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

@ -0,0 +1,49 @@
package dev.fyloz.musicplayer.core.http
import dev.fyloz.musicplayer.core.getAuthorizationData
import dev.fyloz.musicplayer.core.http.requests.ImportTrackRequest
import dev.fyloz.musicplayer.core.logic.TrackLogic
import io.ktor.http.*
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("/tracks") {
get("/") {
call.respond(logic.getAll().toList())
}
get("/trackIds/") {
call.respond(logic.getAllTrackIdsBySource())
}
get("/search") {
val query = call.request.queryParameters["q"]!!
val tracks = logic.search(query, call.getAuthorizationData())
call.respond(tracks)
}
post("/") {
val request = call.receive<ImportTrackRequest>()
val track = logic.save(request.source, request.trackId, call.getAuthorizationData())
call.respond(HttpStatusCode.Created, track)
}
delete("/trackId/{source}/{trackId}") {
val source = call.parameters["source"]
val trackId = call.parameters["trackId"]
if (source != null && trackId != null) {
logic.deleteByTrackId(trackId, source)
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.BadRequest)
}
}
}
}

View File

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

View File

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

@ -0,0 +1,39 @@
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.SearchResult
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 getAllTrackIdsBySource() = getAll()
.groupBy { it.source }
.mapValues { it.value.map { track -> track.trackId } }
fun getById(id: String) = repository.findById(id)
suspend fun search(query: String, auth: AuthorizationData): SearchResult {
return trackFactory.search(query, auth)
}
suspend fun save(source: String, trackId: String, auth: AuthorizationData): Track {
val id = generateId()
val track = trackFactory.create(source, id, trackId, auth)
repository.save(track)
return track
}
fun deleteByTrackId(trackId: String, source: String) {
repository.deleteByTrackId(trackId, source)
}
private fun generateId() = UUID.randomUUID().toString()
}

View File

@ -2,11 +2,14 @@ package dev.fyloz.musicplayer.core.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
typealias SearchResult = Map<String, Collection<SearchResultItem>> typealias SearchResult = Map<String, Collection<ExternalTrack>>
@Serializable @Serializable
data class SearchResultItem( data class ExternalTrack(
val songId: String, val trackId: String,
val name: String, val name: String,
val authors: Collection<String> val albumName: String,
val authors: Collection<String>,
val thumbnailUrl: String,
val previewUrl: String
) )

View File

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

@ -0,0 +1,38 @@
package dev.fyloz.musicplayer.core.model
import kotlinx.serialization.Serializable
/**
* 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
/** The name of the authors of the track. **/
abstract val authors: Collection<String>
/** The name of the source module of the track. **/
abstract val source: String
/** The URLs to the track images. **/
abstract val imagesUrls: TrackImagesUrls
}
@Serializable
data class TrackImagesUrls(
/** The URL of the track icon. **/
val icon: String,
/** The URL of the track thumbnail. **/
val thumbnail: String,
/** The URL of the track's full resolution image. **/
val fullRes: String
)

View File

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

View File

@ -16,6 +16,6 @@ class SpotifyApiProvider : HttpProvider("https://api.spotify.com/v1") {
} }
}.tracks.items }.tracks.items
suspend fun getSongById(songId: String, accessToken: String): Track = suspend fun getTrackById(trackId: String, accessToken: String): Track =
get("tracks/$songId", accessToken) get("tracks/$trackId", accessToken)
} }

View File

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

View File

@ -47,7 +47,8 @@ class SpotifyModule : Module(moduleName) {
} }
override fun getAuthorizationData(call: ApplicationCall) = SpotifyAuthorizationData( override fun getAuthorizationData(call: ApplicationCall) = SpotifyAuthorizationData(
call.request.cookies["Spotify-Access-Token"]!! // call.request.cookies["Spotify-Access-Token"]!!
"BQA5jMFjiPuGgPJ8ogizGh3lR82HIGnkGAZQWFcUSOzIeoGonRleiJ1VUueUlfRpBPBjczBzNZY3EWEHC8kU21SzSyz__DPcZouA2Geyy1obEmbTar8OhQn640JU8VoszpsDFoZTyEAQATbKHVlq6n7Vb51S_nqFHcYww_rUfwObuVRumydpU8rHqWqr"
) )
override fun Route.configureModuleRoutes() { override fun Route.configureModuleRoutes() {
@ -67,7 +68,7 @@ class SpotifyModule : Module(moduleName) {
} }
} }
override fun getSongFactory() = SpotifySongFactory() override fun getTrackFactory() = SpotifyTrackFactory()
companion object { companion object {
const val moduleName = "spotify" const val moduleName = "spotify"

View File

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

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

@ -0,0 +1,16 @@
package dev.fyloz.musicplayer.modules.spotify
import dev.fyloz.musicplayer.core.model.Track
import dev.fyloz.musicplayer.core.model.TrackImagesUrls
import kotlinx.serialization.Serializable
@Serializable
data class SpotifyTrack(
override val id: String,
override val trackId: String,
override val name: String,
override val authors: Collection<String>,
override val imagesUrls: TrackImagesUrls
) : Track() {
override val source = SpotifyModule.moduleName
}

View File

@ -0,0 +1,37 @@
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.ExternalTrack
import dev.fyloz.musicplayer.core.model.Track
import dev.fyloz.musicplayer.core.model.TrackImagesUrls
class SpotifyTrackFactory : TrackFactory {
private val apiProvider = SpotifyApiProvider()
override suspend fun search(query: String, auth: AuthorizationData): Collection<ExternalTrack> {
val tracks = apiProvider.search(query, "track", auth.accessToken)
return tracks.map {
val artists = it.artists.map { artist -> artist.name }
val thumbnailUrl = it.album.images.first { image -> image.width == 300 }.url
ExternalTrack(it.id, it.name, it.album.name, artists, thumbnailUrl, it.previewUrl)
}
}
override suspend fun create(id: String, trackId: String, auth: AuthorizationData): Track {
val spotifyApiTrack = apiProvider.getTrackById(trackId, auth.accessToken)
val artists = spotifyApiTrack.artists.map { it.name }
val imagesUrls = TrackImagesUrls(
spotifyApiTrack.album.images.first { it.width == 64 }.url,
spotifyApiTrack.album.images.first { it.width == 300 }.url,
spotifyApiTrack.album.images.first { it.width == 640 }.url
)
return SpotifyTrack(id, trackId, spotifyApiTrack.name, artists, imagesUrls)
}
private val AuthorizationData.accessToken
get() = getModuleData<SpotifyAuthorizationData>(SpotifyModule.moduleName).accessToken
}