Skip to content

Commit

Permalink
Name downloaded files by song title (#173)
Browse files Browse the repository at this point in the history
  • Loading branch information
toasterofbread committed Nov 24, 2023
1 parent 20a9f4a commit a350af0
Show file tree
Hide file tree
Showing 39 changed files with 344 additions and 209 deletions.
2 changes: 1 addition & 1 deletion ComposeKit
1 change: 1 addition & 0 deletions desktopApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ kotlin {
implementation(project(":shared"))
implementation(project(":ComposeKit:lib"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.4")
implementation("io.github.humbleui:jwm:0.4.15")
}
}
}
Expand Down
4 changes: 1 addition & 3 deletions desktopApp/src/jvmMain/kotlin/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import com.toasterofbread.spmp.platform.AppContext
import com.toasterofbread.composekit.platform.composable.onWindowBackPressed
import com.toasterofbread.spmp.platform.AppContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
Expand Down Expand Up @@ -54,11 +54,9 @@ fun main() {
) {
var initialised by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
context.updateScreenSize()
initialised = true

while (true) {
context.updateScreenSize()
delay(SCREEN_SIZE_UPDATE_INTERVAL)
}
}
Expand Down
5 changes: 3 additions & 2 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ kotlin {
implementation("com.github.catppuccin:java:v1.0.0")
implementation("com.github.paramsen:noise:2.0.0")
implementation("org.kobjects.ktxml:core:0.2.3")
implementation("com.github.Adonai:jaudiotagger:2.3.14")
}
kotlin.srcDir(buildConfigDir)
}
Expand All @@ -161,6 +162,8 @@ kotlin {
api("androidx.activity:activity-compose:1.7.2")
api("androidx.core:core-ktx:1.12.0")
api("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.palette:palette:1.0.0")
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")

val media3_version = "1.1.1"
implementation("androidx.media3:media3-exoplayer:$media3_version")
Expand All @@ -170,8 +173,6 @@ kotlin {
implementation("com.google.accompanist:accompanist-pager:0.21.2-beta")
implementation("com.google.accompanist:accompanist-pager-indicators:0.21.2-beta")
implementation("com.google.accompanist:accompanist-systemuicontroller:0.21.2-beta")
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
implementation("androidx.palette:palette:1.0.0")
//noinspection GradleDependency
implementation("com.github.andob:android-awt:1.0.0")
implementation("com.github.toasterofbread:KizzyRPC:84e79614b4")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,18 @@ import com.toasterofbread.spmp.model.settings.category.StreamingSettings
import com.toasterofbread.spmp.platform.AppContext
import com.toasterofbread.spmp.platform.PlatformBinder
import com.toasterofbread.spmp.platform.PlatformServiceImpl
import com.toasterofbread.spmp.platform.PlayerDownloadManager
import com.toasterofbread.spmp.platform.PlayerDownloadManager.DownloadStatus
import com.toasterofbread.spmp.platform.getLocalLyricsFile
import com.toasterofbread.spmp.platform.getLocalSongFile
import com.toasterofbread.spmp.platform.download.LocalSongMetadataProcessor
import com.toasterofbread.spmp.platform.download.PlayerDownloadManager
import com.toasterofbread.spmp.platform.download.PlayerDownloadManager.DownloadStatus
import com.toasterofbread.spmp.platform.download.getLocalLyricsFile
import com.toasterofbread.spmp.platform.download.getLocalSongFile
import com.toasterofbread.spmp.platform.getUiLanguage
import com.toasterofbread.spmp.resources.getString
import com.toasterofbread.spmp.resources.initResources
import com.toasterofbread.spmp.youtubeapi.YoutubeVideoFormat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.InputStream
Expand All @@ -63,11 +66,11 @@ class PlayerDownloadService: PlatformServiceImpl() {
var silent: Boolean,
val instance: Int,
) {
var song_file: PlatformFile? = song.getLocalSongFile(context, allow_partial = true)
var song_file: PlatformFile? = runBlocking { song.getLocalSongFile(context, allow_partial = true) } // This is fine :)
var lyrics_file: PlatformFile? = song.getLocalLyricsFile(context, allow_partial = true)

var status: DownloadStatus.Status =
if (song_file?.let { fileMatchesDownload(it.name, song)} == true) DownloadStatus.Status.ALREADY_FINISHED
if (song_file?.let { isFileDownloadInProgressForSong(it, song) } == false) DownloadStatus.Status.ALREADY_FINISHED
else DownloadStatus.Status.IDLE
set(value) {
if (field != value) {
Expand Down Expand Up @@ -103,7 +106,7 @@ class PlayerDownloadService: PlatformServiceImpl() {
}

fun generatePath(extension: String, in_progress: Boolean): String {
return getDownloadPath(song, quality, extension, in_progress)
return getDownloadPath(song, extension, in_progress, context)
}

fun broadcastResult(result: Result<PlatformFile?>?, instance: Int) {
Expand Down Expand Up @@ -171,30 +174,20 @@ class PlayerDownloadService: PlatformServiceImpl() {
)

companion object {
fun getFilenameData(filename: String): FilenameData {
val downloading = filename.endsWith(FILE_DOWNLOADING_SUFFIX)
val split = (if (downloading) filename.dropLast(FILE_DOWNLOADING_SUFFIX.length) else filename).split('.', limit = 2)
require(split.size == 2)

return FilenameData(
split[0],
split[1],
downloading
)
}

// Filename format: id.quality.mediatype(.part)
// Return values: true = match, false = match (partial file), null = no match
fun fileMatchesDownload(filename: String, song: Song): Boolean? {
if (!filename.startsWith("${song.id}.")) {
return null
fun getDownloadPath(song: Song, extension: String, in_progress: Boolean, context: AppContext): String {
if (in_progress) {
return "${song.id}.$extension$FILE_DOWNLOADING_SUFFIX"
}
else {
return "${song.getActiveTitle(context.database)}.$extension"
}
return !filename.endsWith(FILE_DOWNLOADING_SUFFIX)
}

fun getDownloadPath(song: Song, quality: SongAudioQuality, extension: String, in_progress: Boolean): String {
return "${song.id}.$extension${ if (in_progress) FILE_DOWNLOADING_SUFFIX else ""}"
}
fun isFileDownloadInProgressForSong(file: PlatformFile, song: Song): Boolean =
file.name.endsWith(FILE_DOWNLOADING_SUFFIX) && file.name.startsWith("${song.id}.")

fun getSongIdOfInProgressDownload(file: PlatformFile): String? =
if (file.name.endsWith(FILE_DOWNLOADING_SUFFIX)) file.name.split('.', limit = 2).first() else null
}

private var notification_builder: NotificationCompat.Builder? = null
Expand Down Expand Up @@ -403,12 +396,12 @@ class PlayerDownloadService: PlatformServiceImpl() {
}

private suspend fun performDownload(download: Download): Result<PlatformFile?> = withContext(Dispatchers.IO) {
val format = getSongFormatByQuality(download.song.id, download.quality, context).fold(
val format: YoutubeVideoFormat = getSongFormatByQuality(download.song.id, download.quality, context).fold(
{ it },
{ return@withContext Result.failure(it) }
)

val connection = URL(format.url).openConnection() as HttpURLConnection
val connection: HttpURLConnection = URL(format.url).openConnection() as HttpURLConnection
connection.connectTimeout = 3000
connection.setRequestProperty("Range", "bytes=${download.downloaded}-")

Expand All @@ -428,16 +421,18 @@ class PlayerDownloadService: PlatformServiceImpl() {
var file: PlatformFile? = download.song_file
check(song_download_dir.mkdirs()) { song_download_dir.toString() }

val file_extension: String = when (connection.contentType) {
"audio/webm" -> "webm"
"audio/mp4" -> "mp4"
else -> return@withContext Result.failure(NotImplementedError(connection.contentType))
}

if (file == null) {
val extension = when (connection.contentType) {
"audio/webm" -> "webm"
"audio/mp4" -> "mp4"
else -> return@withContext Result.failure(NotImplementedError(connection.contentType))
}
file = song_download_dir.resolve(download.generatePath(extension, true))
file = song_download_dir.resolve(download.generatePath(file_extension, true))
}

check(file.name.endsWith(FILE_DOWNLOADING_SUFFIX))
val target_filename: String = download.generatePath(file_extension, false)

val data: ByteArray = ByteArray(4096)
val output: OutputStream = file.outputStream(true)
Expand Down Expand Up @@ -482,31 +477,37 @@ class PlayerDownloadService: PlatformServiceImpl() {
onDownloadProgress()
}

val metadata_retriever = MediaMetadataRetriever()
val metadata_retriever: MediaMetadataRetriever = MediaMetadataRetriever()
metadata_retriever.setDataSource(context.ctx, Uri.parse(file.uri))

val duration_ms = metadata_retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
SongRef(download.song.id).Duration.setNotNull(duration_ms, context.database)
val duration_ms: Long? = metadata_retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
download.song.Duration.setNotNull(duration_ms, context.database)

var lyrics_file: PlatformFile? = download.lyrics_file
if (lyrics_file == null) {
lyrics_file = MediaItemLibrary.getLocalLyricsFile(download.song, context)
runBlocking {
launch {
LocalSongMetadataProcessor.addMetadataToLocalSong(download.song, file, target_filename, context)
}
launch {
if (download.lyrics_file == null) {
val lyrics_file: PlatformFile = MediaItemLibrary.getLocalLyricsFile(download.song, context)

SongLyricsLoader.loadBySong(download.song, context)?.onSuccess { lyrics ->
with (LyricsFileConverter) {
lyrics.saveToFile(lyrics_file, context)
SongLyricsLoader.loadBySong(download.song, context)?.onSuccess { lyrics ->
with (LyricsFileConverter) {
lyrics.saveToFile(lyrics_file, context)
}
}
}
}
}

close(DownloadStatus.Status.FINISHED)
}
catch (_: Throwable) {
catch (e: Throwable) {
e.printStackTrace()
close(DownloadStatus.Status.CANCELLED)
}

val renamed = file.renameTo(file.name.dropLast(FILE_DOWNLOADING_SUFFIX.length))

val renamed: PlatformFile = file.renameTo(target_filename)
download.song_file = renamed
download.status = DownloadStatus.Status.FINISHED

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import com.toasterofbread.composekit.settings.ui.Theme
import com.toasterofbread.db.Database
import com.toasterofbread.spmp.model.settings.category.YTApiSettings
import com.toasterofbread.spmp.model.settings.getEnum
import com.toasterofbread.spmp.platform.download.PlayerDownloadManager
import com.toasterofbread.spmp.youtubeapi.YoutubeApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

actual class AppContext(
context: Context,
private val coroutine_scope: CoroutineScope,
val coroutine_scope: CoroutineScope,
application_context: ApplicationContext? = null
): PlatformContext(context, coroutine_scope, application_context) {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import com.toasterofbread.spmp.model.mediaitem.db.getPlayCount
import com.toasterofbread.spmp.model.mediaitem.song.SongRef
import com.toasterofbread.spmp.model.mediaitem.song.getSongStreamFormat
import com.toasterofbread.spmp.model.settings.category.StreamingSettings
import com.toasterofbread.spmp.platform.download.PlayerDownloadManager
import com.toasterofbread.spmp.platform.download.getLocalSongFile
import com.toasterofbread.spmp.platform.playerservice.AUTO_DOWNLOAD_SOFT_TIMEOUT
import com.toasterofbread.spmp.youtubeapi.YoutubeVideoFormat
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

@UnstableApi
Expand All @@ -33,7 +36,7 @@ internal suspend fun processMediaDataSpec(data_spec: DataSpec, context: AppConte
val initial_status: PlayerDownloadManager.DownloadStatus? = download_manager.getDownload(song)
when (initial_status?.status) {
PlayerDownloadManager.DownloadStatus.Status.IDLE, PlayerDownloadManager.DownloadStatus.Status.CANCELLED, PlayerDownloadManager.DownloadStatus.Status.PAUSED, null -> {
download_manager.startDownload(song.id, true) { status ->
download_manager.startDownload(song, true) { status ->
local_file = status.file
done = true
}
Expand All @@ -55,8 +58,10 @@ internal suspend fun processMediaDataSpec(data_spec: DataSpec, context: AppConte
done = true
}
PlayerDownloadManager.DownloadStatus.Status.FINISHED, PlayerDownloadManager.DownloadStatus.Status.ALREADY_FINISHED -> {
local_file = song.getLocalSongFile(context)
done = true
launch {
local_file = song.getLocalSongFile(context)
done = true
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.toasterofbread.spmp.platform.download

import com.toasterofbread.spmp.model.mediaitem.song.Song
import com.toasterofbread.spmp.platform.AppContext
import com.toasterofbread.composekit.platform.PlatformFile
import com.toasterofbread.spmp.model.mediaitem.MediaItem
import com.toasterofbread.spmp.model.mediaitem.MediaItemData
import com.toasterofbread.spmp.model.mediaitem.artist.Artist.Companion.getForItemId
import com.toasterofbread.spmp.model.mediaitem.artist.ArtistData
import com.toasterofbread.spmp.model.mediaitem.artist.ArtistRef
import com.toasterofbread.spmp.model.mediaitem.playlist.PlaylistData
import com.toasterofbread.spmp.model.mediaitem.playlist.RemotePlaylist
import com.toasterofbread.spmp.model.mediaitem.playlist.RemotePlaylistData
import com.toasterofbread.spmp.model.mediaitem.song.SongData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.jaudiotagger.audio.AudioFile
import org.jaudiotagger.audio.AudioFileIO
import org.jaudiotagger.tag.FieldKey
import org.jaudiotagger.tag.Tag
import org.jaudiotagger.tag.mp4.Mp4Tag
import java.io.File

private val CUSTOM_METADATA_KEY: FieldKey = FieldKey.CUSTOM1

object LocalSongMetadataProcessor {
@Serializable
private data class CustomMetadata(
val song_id: String?, val artist_id: String?, val album_id: String?
)

private fun <T: MediaItemData> MediaItem.getItemWithOrForTitle(item_id: String?, item_title: String?, createItem: (String) -> T): T? {
val item: T
if (item_id != null) {
item = createItem(item_id)
}
else if (item_title != null) {
// TODO
item = createItem(getForItemId(this))
}
else {
return null
}

item.title = item_title
return item
}

suspend fun addMetadataToLocalSong(song: Song, file: PlatformFile, final_filename: String, context: AppContext) = withContext(Dispatchers.IO) {
val tag: Tag = Mp4Tag().apply {
fun set(key: FieldKey, value: String?) {
if (value == null) {
deleteField(key)
}
else {
setField(key, value)
}
}

val artist: ArtistRef? = song.Artist.get(context.database)
val album: RemotePlaylist? = song.Album.get(context.database)

set(FieldKey.TITLE, song.getActiveTitle(context.database))
set(FieldKey.ARTIST, artist?.getActiveTitle(context.database))
set(FieldKey.ALBUM, album?.getActiveTitle(context.database))
set(FieldKey.URL_OFFICIAL_ARTIST_SITE, song.Album.get(context.database)?.getURL(context))
set(FieldKey.URL_LYRICS_SITE, song.Lyrics.get(context.database)?.getUrl())

val custom_metadata: CustomMetadata =
CustomMetadata(
song.id,
artist?.id,
album?.id
)
set(CUSTOM_METADATA_KEY, Json.encodeToString(custom_metadata))
}

val dot_index: Int = final_filename.lastIndexOf('.')
val extension: String = if (dot_index == -1) final_filename else final_filename.substring(dot_index + 1)

val audio_file: AudioFile = AudioFileIO.readAs(File(file.absolute_path), extension)
audio_file.tag = tag
audio_file.commit()
}

suspend fun readLocalSongMetadata(file: PlatformFile, match_id: String? = null, load_data: Boolean = true): SongData? = withContext(Dispatchers.IO) {
val tag: Tag = AudioFileIO.read(File(file.absolute_path)).tag

val custom_metadata: CustomMetadata = Json.decodeFromString(tag.getFirst(CUSTOM_METADATA_KEY))
if (custom_metadata.song_id == null || (match_id != null && custom_metadata.song_id != match_id)) {
return@withContext null
}

return@withContext SongData(custom_metadata.song_id).apply {
if (!load_data) {
return@apply
}
title = tag.getFirst(FieldKey.TITLE)
artist = getItemWithOrForTitle(custom_metadata.artist_id, tag.getFirst(FieldKey.ARTIST)) { ArtistData(it) }
album = getItemWithOrForTitle(custom_metadata.album_id, tag.getFirst(FieldKey.ALBUM)) { RemotePlaylistData(it) }
}
}
}
Loading

0 comments on commit a350af0

Please sign in to comment.