Compare commits
No commits in common. "8802755300d41a72a5b09c20ec21214892515844" and "88d07b0fa0ee1593364f679725f242828b7857c6" have entirely different histories.
8802755300
...
88d07b0fa0
3
.gitignore
vendored
3
.gitignore
vendored
@ -38,6 +38,3 @@ out/
|
|||||||
|
|
||||||
### Kotlin ###
|
### Kotlin ###
|
||||||
.kotlin
|
.kotlin
|
||||||
|
|
||||||
### misc ###
|
|
||||||
/data/
|
|
||||||
|
|||||||
@ -6,12 +6,3 @@ 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
|
|
||||||
@ -3,13 +3,12 @@ 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"
|
||||||
id("org.jetbrains.kotlin.plugin.jpa") version "2.0.0"
|
kotlin("jvm") version "1.9.23"
|
||||||
kotlin("jvm") version "2.0.0"
|
kotlin("plugin.spring") version "1.9.23"
|
||||||
kotlin("plugin.spring") version "2.0.0"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "net.halfbinary"
|
group = "net.halfbinary"
|
||||||
version = "0.0.13-SNAPSHOT"
|
version = "0.0.11-SNAPSHOT"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
@ -34,7 +33,6 @@ 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")
|
||||||
@ -46,8 +44,6 @@ 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")
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
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("/**")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,10 +4,7 @@ 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.GetMapping
|
import org.springframework.web.bind.annotation.*
|
||||||
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")
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
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
|
||||||
@RequestMapping("cache")
|
@CrossOrigin
|
||||||
class CacheController(private val albumCacheService: AlbumCacheService) {
|
class CacheController(private val albumCacheService: AlbumCacheService) {
|
||||||
@GetMapping
|
@GetMapping("cache")
|
||||||
suspend fun refreshAlbumCache(@RequestParam type: CacheType) {
|
suspend fun refreshAlbumCache() {
|
||||||
albumCacheService.cacheAlbums(type)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
@RequestMapping("image")
|
@CrossOrigin
|
||||||
class ImageController(private val imageService: ImageService) {
|
class ImageController(private val imageService: ImageService) {
|
||||||
@GetMapping(value = ["/{id}"], produces = [MediaType.IMAGE_JPEG_VALUE])
|
@GetMapping(value = ["image/{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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,21 +2,16 @@ 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.GetMapping
|
import org.springframework.web.bind.annotation.*
|
||||||
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}/track/{trackNumber}"], produces = ["audio/mpeg3"])
|
@GetMapping(value = ["/album/{albumHash}/{trackNumber}"], produces = ["audio/mpeg3"])
|
||||||
fun getAlbum(
|
fun getAlbum(@PathVariable albumHash: String, @PathVariable trackNumber: Int): ResponseEntity<StreamingResponseBody> {
|
||||||
@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)
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +0,0 @@
|
|||||||
package net.halfbinary.prettyplayerapi.exception
|
|
||||||
|
|
||||||
class AlbumFolderNotFoundException(folder: String) :
|
|
||||||
RuntimeException("Album folder $folder does not exist")
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
package net.halfbinary.prettyplayerapi.exception
|
package net.halfbinary.prettyplayerapi.exception
|
||||||
|
|
||||||
class AlbumHashNotFoundException(hash: String) : RuntimeException("Album hash $hash not found")
|
class AlbumHashNotFoundException(message: String) : RuntimeException(message) {
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package net.halfbinary.prettyplayerapi.exception
|
package net.halfbinary.prettyplayerapi.exception
|
||||||
|
|
||||||
class EnvironmentVariableNotFoundException(envVar: String) :
|
class EnvironmentVariableNotFoundException(message: String) : RuntimeException(message) {
|
||||||
RuntimeException("Environment variable $envVar does not exist, please specify")
|
}
|
||||||
@ -10,41 +10,21 @@ class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler
|
@ExceptionHandler
|
||||||
fun handleEnvironmentVariableNotFoundException(e: EnvironmentVariableNotFoundException): ResponseEntity<ErrorResponse> {
|
fun handleEnvironmentVariableNotFoundException(e: EnvironmentVariableNotFoundException): ResponseEntity<ErrorResponse> {
|
||||||
return ResponseEntity(
|
return ResponseEntity(
|
||||||
ErrorResponse(
|
ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
e.message ?:""), HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
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(
|
ErrorResponse(HttpStatus.BAD_REQUEST.value(),
|
||||||
HttpStatus.BAD_REQUEST.value(),
|
e.message ?:""), HttpStatus.BAD_REQUEST)
|
||||||
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(
|
ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
e.message?:""), HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
e.message ?: ""
|
|
||||||
), HttpStatus.INTERNAL_SERVER_ERROR
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
package net.halfbinary.prettyplayerapi.model
|
|
||||||
|
|
||||||
enum class CacheType {
|
|
||||||
ART,
|
|
||||||
NEW,
|
|
||||||
ALL
|
|
||||||
}
|
|
||||||
@ -1,21 +1,9 @@
|
|||||||
package net.halfbinary.prettyplayerapi.repository
|
package net.halfbinary.prettyplayerapi.repository
|
||||||
|
|
||||||
import jakarta.persistence.Entity
|
import net.halfbinary.prettyplayerapi.model.AlbumMetadata
|
||||||
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
|
||||||
interface AlbumRepository : JpaRepository<AlbumRecord, String>
|
class AlbumRepository {
|
||||||
|
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
|
|
||||||
)
|
|
||||||
@ -4,10 +4,8 @@ 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.CacheType
|
import net.halfbinary.prettyplayerapi.model.AlbumMetadata
|
||||||
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
|
||||||
@ -19,76 +17,48 @@ import kotlin.io.path.name
|
|||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class AlbumCacheService(
|
class AlbumCacheService(private val albumRepository: AlbumRepository,
|
||||||
private val albumRepository: AlbumRepository,
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
|
||||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
|
||||||
) {
|
|
||||||
suspend fun cacheAlbums(type: CacheType = CacheType.ART) {
|
suspend fun doAlbumCache() {
|
||||||
logger.debug { "Starting the cache process with type ${type.name}" }
|
coroutineScope {
|
||||||
|
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 fileList = getAlbumRootDir()
|
val list = 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()
|
||||||
when (type) {
|
.map { Pair(it, Mp3File(it)) }
|
||||||
CacheType.ART -> cacheArt(fileList)
|
.sortedWith(getMusicComparator())
|
||||||
CacheType.NEW -> cacheNew(fileList)
|
.map {
|
||||||
CacheType.ALL -> cacheAll(fileList)
|
val folder = it.first.parentFile.toPath()
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun cacheAll(fileList: List<File>) {
|
albumRepository.albumCache = convertAlbumListToMapForCache(list)
|
||||||
fileList.forEach {
|
//TODO: Write cache to file for later reading...or get a DB going?
|
||||||
val mp3File = Mp3File(it)
|
logger.info { "Albums cached: ${albumRepository.albumCache.size}" }
|
||||||
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() {
|
||||||
@ -105,15 +75,20 @@ class AlbumCacheService(
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAndWriteAlbumImageFile(
|
fun getMusicComparator(): Comparator<Pair<File, Mp3File>> {
|
||||||
file: File,
|
return compareBy({ it.second.id3v2Tag.albumArtist }, {it.second.id3v2Tag.album}, {it.second.id3v2Tag.year})
|
||||||
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 (!hashedFile.exists()) {
|
if(refresh || !hashedFile.exists()) {
|
||||||
logger.debug { "Making image for $folder with hash $folderHash..." }
|
logger.debug { "No image for $folder, making one..." }
|
||||||
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)
|
||||||
@ -133,7 +108,6 @@ class AlbumCacheService(
|
|||||||
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/"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,64 +1,11 @@
|
|||||||
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 albumRepository: AlbumRepository) {
|
class AlbumService(private val albumCacheService: AlbumCacheService) {
|
||||||
fun getAlbumList(): List<AlbumMetadata> {
|
fun getAlbumList(): List<AlbumInfo> {
|
||||||
return albumRepository
|
return albumCacheService.getAlbumCache().map { (_,v) -> v }
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,25 +1,17 @@
|
|||||||
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 albumRepository: AlbumRepository) {
|
class MusicService(private val albumCacheService: AlbumCacheService) {
|
||||||
fun getMusic(albumHash: String, trackNumber: Int): File {
|
fun getMusic(albumHash: String, trackNumber: Int): File {
|
||||||
val albumInfo = albumRepository.findByIdOrNull(albumHash)
|
val albumInfo = albumCacheService.getAlbumCache()[albumHash]
|
||||||
return if(albumInfo != null) {
|
return if(albumInfo != null) {
|
||||||
File(albumInfo.albumFolder)
|
File(albumInfo.albumFolder).listFiles().sortedBy { it.name }[trackNumber-1]
|
||||||
.listFiles()
|
|
||||||
?.filter { it.name.endsWith(".mp3") }
|
|
||||||
?.sortedBy { it.name }
|
|
||||||
?.get(trackNumber)
|
|
||||||
?: throw AlbumFolderNotFoundException(albumInfo.albumFolder)
|
|
||||||
} else {
|
} else {
|
||||||
throw AlbumHashNotFoundException(albumHash)
|
throw AlbumHashNotFoundException("Album hash $albumHash not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,39 +1,23 @@
|
|||||||
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.*
|
import kotlinx.coroutines.coroutineScope
|
||||||
import net.halfbinary.prettyplayerapi.model.CacheType
|
import kotlinx.coroutines.launch
|
||||||
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(
|
class StartupService(private val albumCacheService: AlbumCacheService) {
|
||||||
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.cacheAlbums(CacheType.ALL)
|
albumCacheService.doAlbumCache()
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
coroutineScope {
|
|
||||||
launch {
|
|
||||||
albumCacheService.cacheAlbums(CacheType.NEW)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1 @@
|
|||||||
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
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user