feat(train): smaller copies of lossless tracks, transcoded by Jellyfin to Opus (ADR 0030, D52)
Smaller copies, on by default on the music screen, has the music job fetch a file above 256 kbps as Opus at 128 kbps in WebM, but for the lossy codecs (MP3, AAC, Vorbis, Opus) at any rate. Each track keeps which copy its file is; switching fetches the other copy without working out the tempo again, the file kept until the new one is whole. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
409a2519f9
commit
2a7cd927c4
3 changed files with 379 additions and 39 deletions
|
|
@ -9,9 +9,10 @@ import java.net.URL
|
|||
/**
|
||||
* Downloads the tracks the app listed (ADR 0030, D52), for the music job: each from Jellyfin
|
||||
* into the person's music folder, through a part file renamed once whole, its tempo worked out
|
||||
* where asked, and a line of JSON each appended to the folder's [FILE] (`id`, `bytes`, `tempo`,
|
||||
* `analysed`), which the app takes in. What a line names already is skipped, as the app may not
|
||||
* have taken it in yet. Never logs a track's name.
|
||||
* where asked, and a line of JSON each appended to the folder's [FILE] (`id`, `copy`, `bytes`,
|
||||
* `tempo`, `analysed`), which the app takes in. A track whose copy a line names already is
|
||||
* skipped, as the app may not have taken it in yet. A new copy replaces the file kept only once
|
||||
* it is whole. Never logs a track's name.
|
||||
*/
|
||||
class MusicFetcher(
|
||||
private val token: (String) -> String?,
|
||||
|
|
@ -29,8 +30,11 @@ class MusicFetcher(
|
|||
val tracks: List<Track>,
|
||||
)
|
||||
|
||||
/** A track to fetch: its ID, the path and query under the server's address, and whether its tempo is wanted. */
|
||||
class Track(val id: String, val path: String, val analyse: Boolean = true)
|
||||
/**
|
||||
* A track to fetch: its ID, the copy wanted (`original` or `smaller`, D52, handed back in its
|
||||
* line), the path and query under the server's address, and whether its tempo is wanted.
|
||||
*/
|
||||
class Track(val id: String, val path: String, val analyse: Boolean = true, val copy: String = "original")
|
||||
|
||||
enum class Outcome {
|
||||
/** Everything fetched, or skipped for its own error. */
|
||||
|
|
@ -52,7 +56,9 @@ class MusicFetcher(
|
|||
val done = fetchedIds(lines)
|
||||
for (track in job.tracks) {
|
||||
if (stopped()) return Outcome.STOPPED
|
||||
if (track.id in done || !ID.matches(track.id) || !track.path.startsWith("/Audio/")) continue
|
||||
if ("${track.id}/${track.copy}" in done || !ID.matches(track.id) || !track.path.startsWith("/Audio/")) {
|
||||
continue
|
||||
}
|
||||
val file = File(job.folder, track.id)
|
||||
val part = File(job.folder, "${track.id}.part")
|
||||
val connection = URL(job.address.trimEnd('/') + track.path).openConnection() as HttpURLConnection
|
||||
|
|
@ -78,13 +84,13 @@ class MusicFetcher(
|
|||
}
|
||||
val beats = if (track.analyse) tempo(file) else null
|
||||
val line =
|
||||
mutableMapOf<String, Any?>("id" to track.id, "bytes" to file.length())
|
||||
mutableMapOf<String, Any?>("id" to track.id, "copy" to track.copy, "bytes" to file.length())
|
||||
.apply { if (track.analyse) putAll(mapOf("tempo" to beats, "analysed" to true)) }
|
||||
FileOutputStream(lines, true).use { out ->
|
||||
out.write((JsonText.of(line) + "\n").toByteArray())
|
||||
out.fd.sync()
|
||||
}
|
||||
done += track.id
|
||||
done += "${track.id}/${track.copy}"
|
||||
fetched()
|
||||
}
|
||||
return Outcome.DONE
|
||||
|
|
@ -96,10 +102,15 @@ class MusicFetcher(
|
|||
|
||||
private const val TIMEOUT_MS = 30_000
|
||||
private val ID = Regex("[A-Za-z0-9-]+")
|
||||
private val LINE_ID = Regex("^\\{\"id\":\"([A-Za-z0-9-]+)\"")
|
||||
private val LINE = Regex("^\\{\"id\":\"([A-Za-z0-9-]+)\",\"copy\":\"(\\w+)\"")
|
||||
|
||||
/** What the lines name, as `<id>/<copy>`. */
|
||||
private fun fetchedIds(lines: File): MutableSet<String> =
|
||||
if (!lines.exists()) mutableSetOf()
|
||||
else lines.readLines().mapNotNull { LINE_ID.find(it)?.groupValues?.get(1) }.toMutableSet()
|
||||
else
|
||||
lines
|
||||
.readLines()
|
||||
.mapNotNull { LINE.find(it)?.groupValues?.let { (_, id, copy) -> "$id/$copy" } }
|
||||
.toMutableSet()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,12 @@ class MusicWorker(context: Context, parameters: WorkerParameters) : CoroutineWor
|
|||
tracks =
|
||||
(0 until tracks.length()).map {
|
||||
val track = tracks.getJSONObject(it)
|
||||
MusicFetcher.Track(track.getString("id"), track.getString("path"), track.optBoolean("analyse", true))
|
||||
MusicFetcher.Track(
|
||||
track.getString("id"),
|
||||
track.getString("path"),
|
||||
track.optBoolean("analyse", true),
|
||||
track.optString("copy", "original"),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@ class MusicFetcherTest {
|
|||
assertEquals("audio of t1", File(folder, "t1").readText())
|
||||
assertEquals(
|
||||
listOf(
|
||||
"""{"id":"t1","bytes":11,"tempo":128.0,"analysed":true}""",
|
||||
"""{"id":"t2","bytes":11,"tempo":null,"analysed":true}""",
|
||||
"""{"id":"t1","copy":"original","bytes":11,"tempo":128.0,"analysed":true}""",
|
||||
"""{"id":"t2","copy":"original","bytes":11,"tempo":null,"analysed":true}""",
|
||||
),
|
||||
lines(),
|
||||
)
|
||||
|
|
@ -114,7 +114,27 @@ class MusicFetcherTest {
|
|||
fun `a track whose tempo is known is fetched without working it out`() {
|
||||
fetcher().fetch(job("t1", analyse = false))
|
||||
|
||||
assertEquals(listOf("""{"id":"t1","bytes":11}"""), lines())
|
||||
assertEquals(listOf("""{"id":"t1","copy":"original","bytes":11}"""), lines())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a smaller copy replaces the file kept once whole, though a line names the original`() {
|
||||
fetcher().fetch(job("t1"))
|
||||
val smaller =
|
||||
MusicFetcher.Job(
|
||||
person = "ada",
|
||||
folder = folder,
|
||||
address = address,
|
||||
client = "MediaBrowser",
|
||||
tracks = listOf(MusicFetcher.Track("t1", "/Audio/t1/stream.webm?audioCodec=opus", false, "smaller")),
|
||||
)
|
||||
fetcher().fetch(smaller)
|
||||
fetcher().fetch(smaller)
|
||||
|
||||
assertEquals("/jellyfin/Audio/t1/stream.webm?audioCodec=opus", asked.last())
|
||||
assertEquals(2, asked.size)
|
||||
assertEquals("""{"id":"t1","copy":"smaller","bytes":11}""", lines().last())
|
||||
assertTrue(File(folder, "t1").exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Reference in a new issue