Adds first fully working version of code
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
package net.halfbinary.prettyplayerapi
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class PrettyplayerapiApplication
|
||||
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<PrettyplayerapiApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.halfbinary.prettyplayerapi.controller
|
||||
|
||||
import net.halfbinary.prettyplayerapi.model.AlbumInfo
|
||||
import net.halfbinary.prettyplayerapi.service.AlbumService
|
||||
import org.springframework.web.bind.annotation.CrossOrigin
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@CrossOrigin
|
||||
class AlbumController(private val albumService: AlbumService) {
|
||||
@GetMapping("albums")
|
||||
fun listFolders(): List<AlbumInfo> {
|
||||
return albumService.getAlbumList()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.halfbinary.prettyplayerapi.controller
|
||||
|
||||
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.RestController
|
||||
|
||||
@RestController
|
||||
@CrossOrigin
|
||||
class CacheController(private val albumCacheService: AlbumCacheService) {
|
||||
@GetMapping("cache")
|
||||
fun refreshAlbumCache() {
|
||||
albumCacheService.refreshAlbumCache()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.halfbinary.prettyplayerapi.controller
|
||||
|
||||
import net.halfbinary.prettyplayerapi.service.ImageService
|
||||
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.PathVariable
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@CrossOrigin
|
||||
class ImageController(private val imageService: ImageService) {
|
||||
@GetMapping(value = ["image/{id}"], produces = [MediaType.IMAGE_JPEG_VALUE])
|
||||
fun getImage(@PathVariable("id") id: String): ByteArray {
|
||||
return imageService.getImage(id).readBytes()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.halfbinary.prettyplayerapi.controller
|
||||
|
||||
import net.halfbinary.prettyplayerapi.service.MusicService
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
|
||||
|
||||
@RestController
|
||||
@RequestMapping("music")
|
||||
@CrossOrigin
|
||||
class MusicController(private val musicService: MusicService) {
|
||||
|
||||
@GetMapping(value = ["/album/{albumHash}/{trackNumber}"], produces = ["audio/mpeg3"])
|
||||
fun getAlbum(@PathVariable albumHash: String, @PathVariable trackNumber: Int): ResponseEntity<StreamingResponseBody> {
|
||||
val track = musicService.getMusic(albumHash, trackNumber)
|
||||
val responseBody = StreamingResponseBody { stream -> stream.write(track.readBytes()) }
|
||||
return ResponseEntity.ok().body(responseBody)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package net.halfbinary.prettyplayerapi.exception
|
||||
|
||||
class AlbumHashNotFoundException(message: String) : RuntimeException(message) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package net.halfbinary.prettyplayerapi.exception
|
||||
|
||||
class EnvironmentVariableNotFoundException(message: String) : RuntimeException(message) {
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.halfbinary.prettyplayerapi.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
|
||||
@ControllerAdvice
|
||||
class GlobalExceptionHandler {
|
||||
@ExceptionHandler
|
||||
fun handleEnvironmentVariableNotFoundException(e: EnvironmentVariableNotFoundException): ResponseEntity<ErrorResponse> {
|
||||
return ResponseEntity(
|
||||
ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||
e.message ?:""), HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
@ExceptionHandler
|
||||
fun handleException(e: RuntimeException): ResponseEntity<ErrorResponse> {
|
||||
return ResponseEntity(
|
||||
ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||
e.message?:""), HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class ErrorResponse(
|
||||
private val responseCode: Int,
|
||||
private val message: String
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
package net.halfbinary.prettyplayerapi.model
|
||||
|
||||
data class AlbumInfo(val albumTitle: String, val albumFolder: String, val hash: String)
|
||||
@@ -0,0 +1,9 @@
|
||||
package net.halfbinary.prettyplayerapi.repository
|
||||
|
||||
import net.halfbinary.prettyplayerapi.model.AlbumInfo
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
class AlbumRepository {
|
||||
var albumCache: LinkedHashMap<String, AlbumInfo> = linkedMapOf()
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package net.halfbinary.prettyplayerapi.service
|
||||
|
||||
import com.mpatric.mp3agic.Mp3File
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.halfbinary.prettyplayerapi.exception.EnvironmentVariableNotFoundException
|
||||
import net.halfbinary.prettyplayerapi.model.AlbumInfo
|
||||
import net.halfbinary.prettyplayerapi.repository.AlbumRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import java.awt.Image
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.io.path.name
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class AlbumCacheService(private val albumRepository: AlbumRepository) {
|
||||
|
||||
fun refreshAlbumCache() {
|
||||
runBlocking{
|
||||
launch { cacheAlbums(true)}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAlbumCache(): LinkedHashMap<String, AlbumInfo> {
|
||||
return albumRepository.albumCache
|
||||
}
|
||||
|
||||
suspend fun cacheAlbums(refresh: Boolean = false) {
|
||||
createAlbumArtRootDir()
|
||||
val list = getAlbumRootDir()
|
||||
.walkTopDown()
|
||||
.filter {
|
||||
it.isFile && it.extension == "mp3" && it.name.startsWith("01.")
|
||||
}
|
||||
.toList()
|
||||
.map { Pair(it, Mp3File(it)) }
|
||||
.sortedWith(getMusicComparator())
|
||||
.map {
|
||||
val folder = it.first.parentFile.toPath()
|
||||
val folderHash = folder.name.hashCode().toString()
|
||||
runBlocking { launch {getAndWriteAlbumImageFile(it.first, folderHash, folder, refresh)} }
|
||||
Pair(folderHash, AlbumInfo(it.second.id3v2Tag.album, it.first.parent, folderHash))
|
||||
}
|
||||
|
||||
albumRepository.albumCache = convertAlbumListToMapForCache(list)
|
||||
logger.info { "Albums cached: ${albumRepository.albumCache.size}" }
|
||||
}
|
||||
|
||||
fun createAlbumArtRootDir() {
|
||||
val root = File(ALBUM_ART_ROOT_DIR)
|
||||
if(!root.exists()) {
|
||||
root.mkdirs()
|
||||
}
|
||||
logger.info { "Album Art Directory: $root" }
|
||||
}
|
||||
|
||||
fun getAlbumRootDir(): File {
|
||||
val root = File(ALBUM_ROOT_DIR)
|
||||
logger.info { "Albums Directory: $root" }
|
||||
return root
|
||||
}
|
||||
|
||||
fun getMusicComparator(): Comparator<Pair<File, Mp3File>> {
|
||||
return compareBy({ it.second.id3v2Tag.albumArtist }, {it.second.id3v2Tag.album}, {it.second.id3v2Tag.year})
|
||||
}
|
||||
|
||||
fun convertAlbumListToMapForCache(list: List<Pair<String, AlbumInfo>>): java.util.LinkedHashMap<String, AlbumInfo> {
|
||||
val array = list.toTypedArray()
|
||||
return linkedMapOf(*array)
|
||||
}
|
||||
|
||||
suspend fun getAndWriteAlbumImageFile(file: File, folderHash: String, folder: Path, refresh: Boolean = false) {
|
||||
val hashedFile = File("$ALBUM_ART_ROOT_DIR/$folderHash.jpg")
|
||||
logger.info { "Looking at $hashedFile" }
|
||||
if(refresh || !hashedFile.exists()) {
|
||||
logger.debug { "No image for $folder, making one..." }
|
||||
val imageByteStream = Mp3File(file).id3v2Tag.albumImage.inputStream()
|
||||
val bufferedImage = ImageIO.read(imageByteStream)
|
||||
val scaledImage = bufferedImage.getScaledInstance(300, 300, Image.SCALE_DEFAULT)
|
||||
val outputImage = BufferedImage(300, 300, BufferedImage.TYPE_INT_RGB)
|
||||
outputImage.graphics.drawImage(scaledImage, 0, 0, null)
|
||||
ImageIO.write(outputImage, "jpg", hashedFile)
|
||||
logger.debug { "Wrote new image file." }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ALBUM_ROOT_DIR = System.getenv("ALBUM_ROOT_DIR")
|
||||
?: throw EnvironmentVariableNotFoundException("Environment variable `ALBUM_ROOT_DIR` does not exist, please specify.") //"q:\\CDs"
|
||||
val ALBUM_ART_ROOT_DIR = System.getenv("ALBUM_ART_ROOT_DIR")
|
||||
?: throw EnvironmentVariableNotFoundException("Environment variable `ALBUM_ART_ROOT_DIR` does not exist, please specify.") //"src/main/resources/images/"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package net.halfbinary.prettyplayerapi.service
|
||||
|
||||
import net.halfbinary.prettyplayerapi.model.AlbumInfo
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class AlbumService(private val albumCacheService: AlbumCacheService) {
|
||||
fun getAlbumList(): List<AlbumInfo> {
|
||||
return albumCacheService.getAlbumCache().map { (_,v) -> v }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package net.halfbinary.prettyplayerapi.service
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.File
|
||||
|
||||
@Service
|
||||
class ImageService {
|
||||
fun getImage(id: String): File {
|
||||
return File("${AlbumCacheService.ALBUM_ART_ROOT_DIR}/$id.jpg")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.halfbinary.prettyplayerapi.service
|
||||
|
||||
import net.halfbinary.prettyplayerapi.exception.AlbumHashNotFoundException
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.File
|
||||
|
||||
@Service
|
||||
class MusicService(private val albumCacheService: AlbumCacheService) {
|
||||
fun getMusic(albumHash: String, trackNumber: Int): File {
|
||||
val albumInfo = albumCacheService.getAlbumCache()[albumHash]
|
||||
return if(albumInfo != null) {
|
||||
File(albumInfo.albumFolder).listFiles()[trackNumber-1]
|
||||
} else {
|
||||
throw AlbumHashNotFoundException("Album hash $albumHash not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.halfbinary.prettyplayerapi.service
|
||||
|
||||
import jakarta.annotation.PostConstruct
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
|
||||
@Component
|
||||
class StartupService(private val albumCacheService: AlbumCacheService) {
|
||||
|
||||
@PostConstruct
|
||||
fun onStartup() {
|
||||
albumCacheService.refreshAlbumCache()
|
||||
}
|
||||
|
||||
}
|
||||
1
src/main/resources/application.properties
Normal file
1
src/main/resources/application.properties
Normal file
@@ -0,0 +1 @@
|
||||
spring.application.name=prettyplayerapi
|
||||
@@ -0,0 +1,13 @@
|
||||
package net.halfbinary.prettyplayerapi
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class PrettyplayerapiApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user