Newer
Older
bremer / src / main / kotlin / service / AudioService.kt
/*
 * Copyright (c) 2023. yo-saito. All Rights Reserved.
 */

package net.piedpiper.bremer.service

import net.piedpiper.bremer.dao.*
import net.piedpiper.bremer.entity.AudioEntity
import net.piedpiper.bremer.entity.AudioPlayHistoryEntity
import net.piedpiper.bremer.exception.NotFoundException
import net.piedpiper.bremer.model.api.AudioListResponse
import net.piedpiper.bremer.model.api.AudioRequest
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.io.File
import java.time.LocalDateTime
import java.time.ZoneId

@Service("bremer.service.AudioService")
class AudioService(
    @Qualifier("bremer.dao.AudioDao")
    private val audioDao: AudioDao,
    @Qualifier("bremer.dao.ArtistDao")
    private val artistDao: ArtistDao,
    @Qualifier("bremer.dao.AlbumDao")
    private val albumDao: AlbumDao,
    @Qualifier("bremer.dao.AudioPlayHistoryDao")
    private val audioPlayHistoryDao: AudioPlayHistoryDao,
    @Qualifier("bremer.dao.TagDao")
    private val tagDao: TagDao
) {

    /** 音楽ファイル取得 */
    @Transactional
    fun getAudioFile(slug: String): File {
        val audio = audioDao.findOneBySlug(slug) ?: throw NotFoundException()
        val file = File(audio.path)
        if (!file.exists()) {
            throw NotFoundException()
        }
        return file
    }

    /** キーワード検索 */
    @Transactional
    fun getByKeywords(
        audioNamesLike: List<String>?,
        artistNamesLike: List<String>?,
        albumNamesLike: List<String>?,
        tagNamesLike: List<String>?,
        pageSize: Int
    ): AudioListResponse {
        val set = mutableSetOf<AudioEntity>()
        if (audioNamesLike?.isNotEmpty() == true) {
            set.addAll(audioDao.findAllByNameLikesLimit(audioNamesLike, pageSize))
        }
        if (artistNamesLike?.isNotEmpty() == true && set.size <= pageSize) {
            set.addAll(findAllByArtistNamesLikesLimit(artistNamesLike, pageSize))
        }
        if (albumNamesLike?.isNotEmpty() == true && set.size <= pageSize) {
            set.addAll(findAllByAlbumNamesLikeLimit(albumNamesLike, pageSize))
        }
        if (tagNamesLike?.isNotEmpty() == true && set.size <= pageSize) {
            set.addAll(findAllByTagNamesLikeLimit(tagNamesLike, pageSize))
        }
        return AudioListResponse(
            set.sortedWith(compareBy({ it.albumId }, { it.sequence }))
                .take(pageSize)
                .toList()
        )
    }

    /** 最近再生した曲一覧を取得 */
    @Transactional
    fun getLeastRecentlyAccessedAudio(pageSize: Int): AudioListResponse {
        val historyMap = audioPlayHistoryDao.findAllOrderLastPlayedAtDescLimit(pageSize, 0L)
            .associateBy { it.audioId }
        return if (historyMap.isNotEmpty())
            audioDao.findAllByIdIn(historyMap.keys.map { it })
                .sortedByDescending {
                    historyMap[it.id]?.lastPlayedAt ?: LocalDateTime.MIN
                }.let {
                    AudioListResponse(it)
                }
        else AudioListResponse()
    }

    /** 更新 */
    @Transactional
    fun update(slug: String, request: AudioRequest) = audioDao.findOneBySlugWithLock(slug)
        ?.let {
            request.name?.apply {
                it.name = request.name
                audioDao.updateOne(it)
            }
            albumDao.findOneByIdWithLock(it.albumId)?.let { album ->
                request.album?.apply {
                    album.name = request.album
                    albumDao.updateOne(album)
                }
                request.artist?.apply {
                    artistDao.findOneByIdWithLock(album.artistId)?.let { artist ->
                        artist.name = request.artist
                        artistDao.updateOne(artist)
                    }
                }
            }
        } ?: throw NotFoundException()

    /** 音楽削除 */
    @Transactional
    fun delete(slug: String) = audioDao.findOneBySlugWithLock(slug)
        ?.let {
            audioDao.deleteOneBySlug(it.slug)
            if (audioDao.findAllByAlbumIdIn(listOf(it.albumId))
                    .isEmpty()
            ) {
                albumDao.deleteOneById(it.albumId);
            }
        } ?: throw NotFoundException()

    /** 履歴更新 */
    @Transactional
    fun updateHistory(slug: String, pageSize: Int) {
        audioDao.findOneBySlug(slug)?.let { it ->
            audioPlayHistoryDao.insertOrUpdateOne(
                AudioPlayHistoryEntity(
                    audioId = it.id,
                    lastPlayedAt = LocalDateTime.now(ZoneId.of("Asia/Tokyo"))
                )
            )
            while (true) {
                audioPlayHistoryDao.findAllOrderLastPlayedAtDescLimit(pageSize, pageSize.toLong())
                    ?.let { result ->
                        if (result.isEmpty()) {
                            return
                        }
                        audioPlayHistoryDao.deleteAllByIds(result.map { history -> history.id })
                    }
            }
        }
    }

    private fun findAllByArtistNamesLikesLimit(
        artistNameLikes: List<String>,
        limit: Int
    ): List<AudioEntity> {
        val artists = artistDao.findAllByNamesLikeLimit(artistNameLikes, limit)
        if (artists.isEmpty()) {
            return emptyList()
        }
        val albums = albumDao.findAllByArtistIdInLimit(artists.map { it.id }.distinct(), limit)
        if (albums.isEmpty()) {
            return emptyList()
        }
        return audioDao.findAllByAlbumIdIn(albums.map { it.id }.distinct())
    }

    private fun findAllByAlbumNamesLikeLimit(
        albumNamesLike: List<String>,
        limit: Int
    ): List<AudioEntity> {
        val albums = albumDao.findAllByNamesLikeLimit(albumNamesLike, limit)
        return if (albums.isNotEmpty()) audioDao
            .findAllByAlbumIdIn(albums.map { it.id }) else emptyList()
    }

    private fun findAllByTagNamesLikeLimit(
        tagNamesLike: List<String>,
        limit: Int
    ): List<AudioEntity> {
        val tags = tagDao.findAllByNamesLikeLimit(tagNamesLike, limit)
        return if (tags.isNotEmpty()) audioDao.findAllByTagIdIn(tags.map { it.id }) else emptyList()
    }
}