Compare commits

...

10 Commits

22 changed files with 284 additions and 146 deletions

3
.gitignore vendored
View File

@ -38,3 +38,6 @@ out/
### Kotlin ### ### Kotlin ###
.kotlin .kotlin
### misc ###
/data/

View File

@ -6,3 +6,12 @@ Kotlin Spring Boot based REST service for the Pretty Player
* `ALBUM_ROOT_DIR` = Directory where the albums of mp3s can be found * `ALBUM_ROOT_DIR` = Directory where the albums of mp3s can be found
* `ALBUM_ART_ROOT_DIR` = Directory where the album art can be cached for later * `ALBUM_ART_ROOT_DIR` = Directory where the album art can be cached for later
# TODO
* Add in DB like SQLite where the DB can be written to disk easily, and the DB can run in memory.
* Allows for instant ready, while having new albums added async.
# API Documentation
* http://localhost:8080/swagger-ui/index.html

View File

@ -3,12 +3,13 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id("org.springframework.boot") version "3.2.5" id("org.springframework.boot") version "3.2.5"
id("io.spring.dependency-management") version "1.1.4" id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.23" id("org.jetbrains.kotlin.plugin.jpa") version "2.0.0"
kotlin("plugin.spring") version "1.9.23" kotlin("jvm") version "2.0.0"
kotlin("plugin.spring") version "2.0.0"
} }
group = "net.halfbinary" group = "net.halfbinary"
version = "0.0.11-SNAPSHOT" version = "0.0.13-SNAPSHOT"
java { java {
sourceCompatibility = JavaVersion.VERSION_21 sourceCompatibility = JavaVersion.VERSION_21
@ -33,6 +34,7 @@ val coroutinesVersion = "1.8.1"
dependencies { dependencies {
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-data-jpa:3.3.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-reflect")
@ -44,6 +46,8 @@ dependencies {
implementation("org.slf4j:slf4j-api:$slf4jApiVersion") implementation("org.slf4j:slf4j-api:$slf4jApiVersion")
implementation("ch.qos.logback:logback-core:$logbackVersion") implementation("ch.qos.logback:logback-core:$logbackVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0")
runtimeOnly("com.h2database:h2")
developmentOnly("org.springframework.boot:spring-boot-devtools") developmentOnly("org.springframework.boot:spring-boot-devtools")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")

View File

@ -0,0 +1,14 @@
package net.halfbinary.prettyplayerapi.config
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.EnableWebMvc
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
}
}

View File

@ -4,7 +4,10 @@ import net.halfbinary.prettyplayerapi.model.AlbumInfo
import net.halfbinary.prettyplayerapi.model.AlbumMetadata import net.halfbinary.prettyplayerapi.model.AlbumMetadata
import net.halfbinary.prettyplayerapi.model.TrackInfo import net.halfbinary.prettyplayerapi.model.TrackInfo
import net.halfbinary.prettyplayerapi.service.AlbumService import net.halfbinary.prettyplayerapi.service.AlbumService
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("album") @RequestMapping("album")

View File

@ -1,15 +1,17 @@
package net.halfbinary.prettyplayerapi.controller package net.halfbinary.prettyplayerapi.controller
import net.halfbinary.prettyplayerapi.model.CacheType
import net.halfbinary.prettyplayerapi.service.AlbumCacheService import net.halfbinary.prettyplayerapi.service.AlbumCacheService
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@CrossOrigin @RequestMapping("cache")
class CacheController(private val albumCacheService: AlbumCacheService) { class CacheController(private val albumCacheService: AlbumCacheService) {
@GetMapping("cache") @GetMapping
suspend fun refreshAlbumCache() { suspend fun refreshAlbumCache(@RequestParam type: CacheType) {
albumCacheService.refreshAlbumCache() albumCacheService.cacheAlbums(type)
} }
} }

View File

@ -1,15 +0,0 @@
package net.halfbinary.prettyplayerapi.controller
import net.halfbinary.prettyplayerapi.service.FolderService
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@CrossOrigin
class FolderController(private val folderService: FolderService) {
@GetMapping("folders")
fun listFolders(): List<String> {
return folderService.getFolderList()
}
}

View File

@ -2,15 +2,15 @@ package net.halfbinary.prettyplayerapi.controller
import net.halfbinary.prettyplayerapi.service.ImageService import net.halfbinary.prettyplayerapi.service.ImageService
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@CrossOrigin @RequestMapping("image")
class ImageController(private val imageService: ImageService) { class ImageController(private val imageService: ImageService) {
@GetMapping(value = ["image/{id}"], produces = [MediaType.IMAGE_JPEG_VALUE]) @GetMapping(value = ["/{id}"], produces = [MediaType.IMAGE_JPEG_VALUE])
fun getImage(@PathVariable("id") id: String): ByteArray { fun getImage(@PathVariable("id") id: String): ByteArray {
return imageService.getImage(id).readBytes() return imageService.getImage(id).readBytes()
} }

View File

@ -2,16 +2,21 @@ package net.halfbinary.prettyplayerapi.controller
import net.halfbinary.prettyplayerapi.service.MusicService import net.halfbinary.prettyplayerapi.service.MusicService
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
@RestController @RestController
@RequestMapping("music") @RequestMapping("music")
@CrossOrigin
class MusicController(private val musicService: MusicService) { class MusicController(private val musicService: MusicService) {
@GetMapping(value = ["/album/{albumHash}/{trackNumber}"], produces = ["audio/mpeg3"]) @GetMapping(value = ["/album/{albumHash}/track/{trackNumber}"], produces = ["audio/mpeg3"])
fun getAlbum(@PathVariable albumHash: String, @PathVariable trackNumber: Int): ResponseEntity<StreamingResponseBody> { fun getAlbum(
@PathVariable albumHash: String,
@PathVariable trackNumber: Int
): ResponseEntity<StreamingResponseBody> {
val track = musicService.getMusic(albumHash, trackNumber) val track = musicService.getMusic(albumHash, trackNumber)
val responseBody = StreamingResponseBody { stream -> stream.write(track.readBytes()) } val responseBody = StreamingResponseBody { stream -> stream.write(track.readBytes()) }
return ResponseEntity.ok().body(responseBody) return ResponseEntity.ok().body(responseBody)

View File

@ -1,27 +0,0 @@
package net.halfbinary.prettyplayerapi.controller
import com.mpatric.mp3agic.Mp3File
import io.github.oshai.kotlinlogging.KotlinLogging
import net.halfbinary.prettyplayerapi.model.TrackInfo
import net.halfbinary.prettyplayerapi.service.MusicService
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
private val logger = KotlinLogging.logger {}
@RestController
@CrossOrigin
class TrackController(private val musicService: MusicService) {
@GetMapping("track/{albumHash}/{trackNumber}")
fun getTrackInfo(@PathVariable albumHash: String, @PathVariable trackNumber: Int): TrackInfo {
val file = musicService.getMusic(albumHash, trackNumber)
val trackFile = Mp3File(file)
trackFile.id3v2Tag.artist
trackFile.id3v2Tag.apply {
logger.debug { "Getting track $trackNumber. $title by $artist on $album" }
return TrackInfo(album, artist, title)
}
}
}

View File

@ -0,0 +1,4 @@
package net.halfbinary.prettyplayerapi.exception
class AlbumFolderNotFoundException(folder: String) :
RuntimeException("Album folder $folder does not exist")

View File

@ -1,4 +1,3 @@
package net.halfbinary.prettyplayerapi.exception package net.halfbinary.prettyplayerapi.exception
class AlbumHashNotFoundException(message: String) : RuntimeException(message) { class AlbumHashNotFoundException(hash: String) : RuntimeException("Album hash $hash not found")
}

View File

@ -1,4 +1,4 @@
package net.halfbinary.prettyplayerapi.exception package net.halfbinary.prettyplayerapi.exception
class EnvironmentVariableNotFoundException(message: String) : RuntimeException(message) { class EnvironmentVariableNotFoundException(envVar: String) :
} RuntimeException("Environment variable $envVar does not exist, please specify")

View File

@ -10,21 +10,41 @@ class GlobalExceptionHandler {
@ExceptionHandler @ExceptionHandler
fun handleEnvironmentVariableNotFoundException(e: EnvironmentVariableNotFoundException): ResponseEntity<ErrorResponse> { fun handleEnvironmentVariableNotFoundException(e: EnvironmentVariableNotFoundException): ResponseEntity<ErrorResponse> {
return ResponseEntity( return ResponseEntity(
ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), ErrorResponse(
e.message ?:""), HttpStatus.INTERNAL_SERVER_ERROR) HttpStatus.INTERNAL_SERVER_ERROR.value(),
e.message ?: ""
), HttpStatus.INTERNAL_SERVER_ERROR
)
} }
@ExceptionHandler @ExceptionHandler
fun handleAlbumHashNotFoundException(e: AlbumHashNotFoundException): ResponseEntity<ErrorResponse> { fun handleAlbumHashNotFoundException(e: AlbumHashNotFoundException): ResponseEntity<ErrorResponse> {
return ResponseEntity( return ResponseEntity(
ErrorResponse(HttpStatus.BAD_REQUEST.value(), ErrorResponse(
e.message ?:""), HttpStatus.BAD_REQUEST) HttpStatus.BAD_REQUEST.value(),
e.message ?: ""
), HttpStatus.BAD_REQUEST
)
} }
@ExceptionHandler
fun handleAlbumFolderNotFoundException(e: AlbumFolderNotFoundException): ResponseEntity<ErrorResponse> {
return ResponseEntity(
ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
e.message ?: ""
), HttpStatus.INTERNAL_SERVER_ERROR
)
}
@ExceptionHandler @ExceptionHandler
fun handleException(e: RuntimeException): ResponseEntity<ErrorResponse> { fun handleException(e: RuntimeException): ResponseEntity<ErrorResponse> {
return ResponseEntity( return ResponseEntity(
ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), ErrorResponse(
e.message?:""), HttpStatus.INTERNAL_SERVER_ERROR) HttpStatus.INTERNAL_SERVER_ERROR.value(),
e.message ?: ""
), HttpStatus.INTERNAL_SERVER_ERROR
)
} }
} }

View File

@ -0,0 +1,7 @@
package net.halfbinary.prettyplayerapi.model
enum class CacheType {
ART,
NEW,
ALL
}

View File

@ -1,9 +1,21 @@
package net.halfbinary.prettyplayerapi.repository package net.halfbinary.prettyplayerapi.repository
import net.halfbinary.prettyplayerapi.model.AlbumMetadata import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
class AlbumRepository { interface AlbumRepository : JpaRepository<AlbumRecord, String>
var albumCache: LinkedHashMap<String, AlbumMetadata> = linkedMapOf()
} @Entity
@Table(name = "album")
data class AlbumRecord(
@Id
val id: String,
val albumTitle: String,
val albumFolder: String,
val albumArtist: String,
val albumYear: Int
)

View File

@ -4,8 +4,10 @@ import com.mpatric.mp3agic.Mp3File
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.* import kotlinx.coroutines.*
import net.halfbinary.prettyplayerapi.exception.EnvironmentVariableNotFoundException import net.halfbinary.prettyplayerapi.exception.EnvironmentVariableNotFoundException
import net.halfbinary.prettyplayerapi.model.AlbumMetadata import net.halfbinary.prettyplayerapi.model.CacheType
import net.halfbinary.prettyplayerapi.repository.AlbumRecord
import net.halfbinary.prettyplayerapi.repository.AlbumRepository import net.halfbinary.prettyplayerapi.repository.AlbumRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.awt.Image import java.awt.Image
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
@ -17,48 +19,76 @@ import kotlin.io.path.name
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@Service @Service
class AlbumCacheService(private val albumRepository: AlbumRepository, class AlbumCacheService(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { private val albumRepository: AlbumRepository,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun doAlbumCache() { suspend fun cacheAlbums(type: CacheType = CacheType.ART) {
coroutineScope { logger.debug { "Starting the cache process with type ${type.name}" }
launch { cacheAlbums(false) }
}
}
suspend fun refreshAlbumCache() {
coroutineScope{
launch { cacheAlbums(true)}
}
}
fun getAlbumCache(): LinkedHashMap<String, AlbumMetadata> {
return albumRepository.albumCache
}
suspend fun cacheAlbums(refresh: Boolean = false) {
logger.debug { "Starting the cache process with refresh set to $refresh" }
createAlbumArtRootDir() createAlbumArtRootDir()
val list = getAlbumRootDir() val fileList = getAlbumRootDir()
.walkTopDown() .walkTopDown()
.filter { .filter {
it.isFile && it.extension == "mp3" && it.name.startsWith("01.") it.isFile && it.extension == "mp3" && it.name.startsWith("01.")
} }
.toList() .toList()
.map { Pair(it, Mp3File(it)) } when (type) {
.sortedWith(getMusicComparator()) CacheType.ART -> cacheArt(fileList)
.map { CacheType.NEW -> cacheNew(fileList)
val folder = it.first.parentFile.toPath() CacheType.ALL -> cacheAll(fileList)
val folderHash = folder.name.hashCode().toString()
coroutineScope {
launch {getAndWriteAlbumImageFile(it.first, folderHash, folder, refresh)}
} }
Pair(folderHash, AlbumMetadata(it.second.id3v2Tag.album, it.first.parent, folderHash))
} }
albumRepository.albumCache = convertAlbumListToMapForCache(list) suspend fun cacheAll(fileList: List<File>) {
//TODO: Write cache to file for later reading...or get a DB going? fileList.forEach {
logger.info { "Albums cached: ${albumRepository.albumCache.size}" } val mp3File = Mp3File(it)
val folderPath = it.parentFile.toPath()
val folderHash = folderPath.name.hashCode().toString()
coroutineScope {
launch { getAndWriteAlbumImageFile(it, folderHash, folderPath) }
}
albumRepository.save(
AlbumRecord(
folderHash,
mp3File.id3v2Tag.album,
it.parent,
mp3File.id3v2Tag.albumArtist,
mp3File.id3v2Tag.year.toInt()
)
)
}
}
suspend fun cacheNew(fileList: List<File>) {
fileList.forEach {
val folderPath = it.parentFile.toPath()
val folderHash = folderPath.name.hashCode().toString()
if (albumRepository.findByIdOrNull(folderHash) == null) {
coroutineScope {
launch { getAndWriteAlbumImageFile(it, folderHash, folderPath) }
}
val mp3File = Mp3File(it)
albumRepository.save(
AlbumRecord(
folderHash,
mp3File.id3v2Tag.album,
it.parent,
mp3File.id3v2Tag.albumArtist,
mp3File.id3v2Tag.year.toInt()
)
)
}
}
}
suspend fun cacheArt(fileList: List<File>) {
fileList.forEach {
val folderPath = it.parentFile.toPath()
val folderHash = folderPath.name.hashCode().toString()
coroutineScope {
launch { getAndWriteAlbumImageFile(it, folderHash, folderPath) }
}
}
} }
fun createAlbumArtRootDir() { fun createAlbumArtRootDir() {
@ -75,20 +105,15 @@ class AlbumCacheService(private val albumRepository: AlbumRepository,
return root return root
} }
fun getMusicComparator(): Comparator<Pair<File, Mp3File>> { suspend fun getAndWriteAlbumImageFile(
return compareBy({ it.second.id3v2Tag.albumArtist }, {it.second.id3v2Tag.album}, {it.second.id3v2Tag.year}) file: File,
} folderHash: String,
folder: Path
fun convertAlbumListToMapForCache(list: List<Pair<String, AlbumMetadata>>): java.util.LinkedHashMap<String, AlbumMetadata> { ) {
val array = list.toTypedArray()
return linkedMapOf(*array)
}
suspend fun getAndWriteAlbumImageFile(file: File, folderHash: String, folder: Path, refresh: Boolean = false) {
logger.info { "Checking album $folder with hash $folderHash" } logger.info { "Checking album $folder with hash $folderHash" }
val hashedFile = File("$ALBUM_ART_ROOT_DIR/$folderHash.jpg") val hashedFile = File("$ALBUM_ART_ROOT_DIR/$folderHash.jpg")
if(refresh || !hashedFile.exists()) { if (!hashedFile.exists()) {
logger.debug { "No image for $folder, making one..." } logger.debug { "Making image for $folder with hash $folderHash..." }
val imageByteStream = Mp3File(file).id3v2Tag.albumImage.inputStream() val imageByteStream = Mp3File(file).id3v2Tag.albumImage.inputStream()
val bufferedImage = withContext(ioDispatcher) { val bufferedImage = withContext(ioDispatcher) {
ImageIO.read(imageByteStream) ImageIO.read(imageByteStream)
@ -108,6 +133,7 @@ class AlbumCacheService(private val albumRepository: AlbumRepository,
return System.getenv(envName) return System.getenv(envName)
?: throw EnvironmentVariableNotFoundException(envName) ?: throw EnvironmentVariableNotFoundException(envName)
} }
val ALBUM_ROOT_DIR = getEnv("ALBUM_ROOT_DIR") //"q:\\CDs" val ALBUM_ROOT_DIR = getEnv("ALBUM_ROOT_DIR") //"q:\\CDs"
val ALBUM_ART_ROOT_DIR = getEnv("ALBUM_ART_ROOT_DIR") //"src/main/resources/images/" val ALBUM_ART_ROOT_DIR = getEnv("ALBUM_ART_ROOT_DIR") //"src/main/resources/images/"
} }

View File

@ -1,11 +1,64 @@
package net.halfbinary.prettyplayerapi.service package net.halfbinary.prettyplayerapi.service
import com.mpatric.mp3agic.Mp3File
import io.github.oshai.kotlinlogging.KotlinLogging
import net.halfbinary.prettyplayerapi.exception.AlbumFolderNotFoundException
import net.halfbinary.prettyplayerapi.exception.AlbumHashNotFoundException
import net.halfbinary.prettyplayerapi.model.AlbumInfo import net.halfbinary.prettyplayerapi.model.AlbumInfo
import net.halfbinary.prettyplayerapi.model.AlbumMetadata
import net.halfbinary.prettyplayerapi.model.AlbumTrackInfo
import net.halfbinary.prettyplayerapi.model.TrackInfo
import net.halfbinary.prettyplayerapi.repository.AlbumRepository
import org.springframework.data.domain.Sort
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.File
private val logger = KotlinLogging.logger {}
@Service @Service
class AlbumService(private val albumCacheService: AlbumCacheService) { class AlbumService(private val albumRepository: AlbumRepository) {
fun getAlbumList(): List<AlbumInfo> { fun getAlbumList(): List<AlbumMetadata> {
return albumCacheService.getAlbumCache().map { (_,v) -> v } return albumRepository
.findAll(Sort.by(Sort.Direction.ASC, "albumArtist", "albumYear", "albumTitle"))
.map { AlbumMetadata(it.albumTitle, it.albumFolder, it.id) }
}
fun getAlbumInfo(albumHash: String): AlbumInfo {
val albumRecordResponse = albumRepository.findByIdOrNull(albumHash)
val trackFileList = if (albumRecordResponse != null) {
File(albumRecordResponse.albumFolder).listFiles()
?.filter { it.name.endsWith(".mp3") }
?.sortedBy { it.name }
?: throw AlbumFolderNotFoundException(albumRecordResponse.albumFolder)
} else {
throw AlbumHashNotFoundException(albumHash)
}
val trackList = trackFileList.map {
val id3 = Mp3File(it).id3v2Tag
id3.run {
AlbumTrackInfo(track.toInt(), artist, title)
}
}
val id3 = Mp3File(trackFileList[0]).id3v2Tag
return AlbumInfo(id3.album, id3.albumArtist, trackList)
}
fun getTrackInfo(albumHash: String, trackNumber: Int): TrackInfo {
val albumInfo = albumRepository.findByIdOrNull(albumHash)
val file = if (albumInfo != null) {
File(albumInfo.albumFolder).listFiles()
?.filter { it.name.endsWith(".mp3") }
?.sortedBy { it.name }
?.get(trackNumber)
} else {
throw AlbumHashNotFoundException(albumHash)
}
val trackFile = Mp3File(file)
trackFile.id3v2Tag.artist
trackFile.id3v2Tag.apply {
logger.debug { "Getting track $trackNumber: $track. $title by $artist on $album" }
return TrackInfo(album, artist, title)
}
} }
} }

View File

@ -1,10 +0,0 @@
package net.halfbinary.prettyplayerapi.service
import org.springframework.stereotype.Service
@Service
class FolderService(private val albumCacheService: AlbumCacheService) {
fun getFolderList(): List<String> {
return albumCacheService.getAlbumCache().map { (_,v) -> v.albumFolder }
}
}

View File

@ -1,17 +1,25 @@
package net.halfbinary.prettyplayerapi.service package net.halfbinary.prettyplayerapi.service
import net.halfbinary.prettyplayerapi.exception.AlbumFolderNotFoundException
import net.halfbinary.prettyplayerapi.exception.AlbumHashNotFoundException import net.halfbinary.prettyplayerapi.exception.AlbumHashNotFoundException
import net.halfbinary.prettyplayerapi.repository.AlbumRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.File import java.io.File
@Service @Service
class MusicService(private val albumCacheService: AlbumCacheService) { class MusicService(private val albumRepository: AlbumRepository) {
fun getMusic(albumHash: String, trackNumber: Int): File { fun getMusic(albumHash: String, trackNumber: Int): File {
val albumInfo = albumCacheService.getAlbumCache()[albumHash] val albumInfo = albumRepository.findByIdOrNull(albumHash)
return if (albumInfo != null) { return if (albumInfo != null) {
File(albumInfo.albumFolder).listFiles().sortedBy { it.name }[trackNumber-1] File(albumInfo.albumFolder)
.listFiles()
?.filter { it.name.endsWith(".mp3") }
?.sortedBy { it.name }
?.get(trackNumber)
?: throw AlbumFolderNotFoundException(albumInfo.albumFolder)
} else { } else {
throw AlbumHashNotFoundException("Album hash $albumHash not found") throw AlbumHashNotFoundException(albumHash)
} }
} }
} }

View File

@ -1,23 +1,39 @@
package net.halfbinary.prettyplayerapi.service package net.halfbinary.prettyplayerapi.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.launch import net.halfbinary.prettyplayerapi.model.CacheType
import net.halfbinary.prettyplayerapi.repository.AlbumRepository
import org.springframework.boot.context.event.ApplicationStartedEvent import org.springframework.boot.context.event.ApplicationStartedEvent
import org.springframework.context.event.EventListener import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Async import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@Component @Component
class StartupService(private val albumCacheService: AlbumCacheService) { class StartupService(
private val albumRepository: AlbumRepository,
private val albumCacheService: AlbumCacheService,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
@Async @Async
@EventListener(ApplicationStartedEvent::class) @EventListener(ApplicationStartedEvent::class)
suspend fun onStartup() { suspend fun onStartup() {
if (withContext(ioDispatcher) {
albumRepository.count()
} == 0L) {
coroutineScope { coroutineScope {
launch { launch {
albumCacheService.doAlbumCache() albumCacheService.cacheAlbums(CacheType.ALL)
}
}
} else {
coroutineScope {
launch {
albumCacheService.cacheAlbums(CacheType.NEW)
}
} }
} }
} }

View File

@ -1 +1,6 @@
spring.application.name=prettyplayerapi spring.application.name=prettyplayerapi
spring.datasource.url=jdbc:h2:file:./data/prettyplayerdata
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=pp
spring.datasource.password=pppass
spring.jpa.hibernate.ddl-auto=update