Adds first fully working version of code

This commit is contained in:
2024-06-02 23:28:35 -05:00
parent bc979b94db
commit 8b077be945
25 changed files with 726 additions and 0 deletions

View File

@@ -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)
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,4 @@
package net.halfbinary.prettyplayerapi.exception
class AlbumHashNotFoundException(message: String) : RuntimeException(message) {
}

View File

@@ -0,0 +1,4 @@
package net.halfbinary.prettyplayerapi.exception
class EnvironmentVariableNotFoundException(message: String) : RuntimeException(message) {
}

View File

@@ -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
)

View File

@@ -0,0 +1,3 @@
package net.halfbinary.prettyplayerapi.model
data class AlbumInfo(val albumTitle: String, val albumFolder: String, val hash: String)

View File

@@ -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()
}

View File

@@ -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/"
}
}

View File

@@ -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 }
}
}

View File

@@ -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 }
}
}

View File

@@ -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")
}
}

View File

@@ -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")
}
}
}

View File

@@ -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()
}
}

View File

@@ -0,0 +1 @@
spring.application.name=prettyplayerapi

View File

@@ -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() {
}
}