From 56951a2e68ca8e653cc87aa31a26325ac95870f8 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Thu, 9 Apr 2026 12:45:49 +0200 Subject: [PATCH 1/3] fix: quote exact itag selectors for yt-dlp --- .../typetype/downloader/services/YtDlpOptionResolver.kt | 8 ++++---- .../downloader/services/YtDlpOptionResolverTest.kt | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/dev/typetype/downloader/services/YtDlpOptionResolver.kt b/src/main/kotlin/dev/typetype/downloader/services/YtDlpOptionResolver.kt index ad02eee..547f572 100644 --- a/src/main/kotlin/dev/typetype/downloader/services/YtDlpOptionResolver.kt +++ b/src/main/kotlin/dev/typetype/downloader/services/YtDlpOptionResolver.kt @@ -10,7 +10,7 @@ object YtDlpOptionResolver { fun audioSelector(options: JobOptions): String { val audioItag = options.audioItag if (audioItag.isNotBlank()) { - val strict = "ba[format_id=$audioItag]/b[format_id=$audioItag]" + val strict = "ba[format_id='$audioItag']/b[format_id='$audioItag']" return if (options.allowQualityFallback) "$strict/bestaudio/best" else strict } return if (options.quality.lowercase() == "worst") "worstaudio/worst" else "bestaudio/best" @@ -33,8 +33,8 @@ object YtDlpOptionResolver { private fun exactVideoSelector(options: JobOptions): String { val videoFilters = mutableListOf() val audioFilters = mutableListOf() - options.videoItag.takeIf { it.isNotBlank() }?.let { videoFilters += "[format_id=$it]" } - options.audioItag.takeIf { it.isNotBlank() }?.let { audioFilters += "[format_id=$it]" } + options.videoItag.takeIf { it.isNotBlank() }?.let { videoFilters += "[format_id='$it']" } + options.audioItag.takeIf { it.isNotBlank() }?.let { audioFilters += "[format_id='$it']" } options.height?.let { videoFilters += "[height=$it]" } options.fps?.let { videoFilters += "[fps=$it]" } options.videoCodec.takeIf { it.isNotBlank() }?.let { videoFilters += "[vcodec^=$it]" } @@ -43,7 +43,7 @@ object YtDlpOptionResolver { val video = "bv*${videoFilters.joinToString("")}" val audio = "ba${audioFilters.joinToString("")}" return if (options.videoItag.isNotBlank() && options.audioItag.isBlank()) { - "$video+$audio/b[format_id=${options.videoItag}]" + "$video+$audio/b[format_id='${options.videoItag}']" } else { "$video+$audio" } diff --git a/src/test/kotlin/dev/typetype/downloader/services/YtDlpOptionResolverTest.kt b/src/test/kotlin/dev/typetype/downloader/services/YtDlpOptionResolverTest.kt index 4494800..d7fabbe 100644 --- a/src/test/kotlin/dev/typetype/downloader/services/YtDlpOptionResolverTest.kt +++ b/src/test/kotlin/dev/typetype/downloader/services/YtDlpOptionResolverTest.kt @@ -11,7 +11,7 @@ class YtDlpOptionResolverTest { assertEquals("worstaudio/worst", YtDlpOptionResolver.audioSelector(JobOptions(mode = DownloadMode.AUDIO, quality = "worst"))) assertEquals("bestaudio/best", YtDlpOptionResolver.audioSelector(JobOptions(mode = DownloadMode.AUDIO, quality = "best"))) assertEquals( - "ba[format_id=251]/b[format_id=251]", + "ba[format_id='251']/b[format_id='251']", YtDlpOptionResolver.audioSelector(JobOptions(mode = DownloadMode.AUDIO, audioItag = "251")), ) assertEquals("mp3", YtDlpOptionResolver.audioFormat("avi")) @@ -30,7 +30,7 @@ class YtDlpOptionResolverTest { ) assertEquals("bv*+ba/b", YtDlpOptionResolver.videoSelector(JobOptions(mode = DownloadMode.VIDEO, quality = "best"))) assertEquals( - "bv*[format_id=137]+ba[format_id=140]", + "bv*[format_id='137']+ba[format_id='140']", YtDlpOptionResolver.videoSelector(JobOptions(mode = DownloadMode.VIDEO, videoItag = "137", audioItag = "140")), ) assertEquals("mp4", YtDlpOptionResolver.videoFormat("unknown")) From d736ba9ad70dd5fbfceec1e387c6d01d17fbb932 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Thu, 9 Apr 2026 12:58:52 +0200 Subject: [PATCH 2/3] fix: skip po token on exact format selection --- .../typetype/downloader/services/JobWorker.kt | 9 ++- .../services/JobWorkerTokenSelectionTest.kt | 62 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/dev/typetype/downloader/services/JobWorkerTokenSelectionTest.kt diff --git a/src/main/kotlin/dev/typetype/downloader/services/JobWorker.kt b/src/main/kotlin/dev/typetype/downloader/services/JobWorker.kt index ed54340..b6dac23 100644 --- a/src/main/kotlin/dev/typetype/downloader/services/JobWorker.kt +++ b/src/main/kotlin/dev/typetype/downloader/services/JobWorker.kt @@ -38,7 +38,7 @@ class JobWorker( redis.setex(redisJobKey(id), config.jobTtlSeconds, "running") try { val startedAt = System.nanoTime() - val token = tokenServiceClient.fetchForUrl(job.url) + val token = if (shouldUseTokenFor(options)) tokenServiceClient.fetchForUrl(job.url) else null val result = ytDlpService.download(job.url, token, options) { jobsRepository.getById(id)?.status != JobStatus.RUNNING } @@ -84,6 +84,13 @@ class JobWorker( return runCatching { JobOptionsCodec.decode(row.optionsJson) }.map(JobOptionsNormalizer::normalize).getOrElse { JobOptions() } } + private fun shouldUseTokenFor(options: JobOptions): Boolean { + val hasExactSelection = options.videoItag.isNotBlank() || options.audioItag.isNotBlank() || + options.height != null || options.fps != null || options.videoCodec.isNotBlank() || + options.audioCodec.isNotBlank() || options.bitrate != null + return !hasExactSelection + } + private fun uploadArtifact(cacheKey: String, filePath: java.nio.file.Path): StorageArtifact { val expiresAt = Instant.now().plusSeconds(config.s3ArtifactTtlSeconds) val extension = filePath.fileName.toString().substringAfterLast('.', "bin") diff --git a/src/test/kotlin/dev/typetype/downloader/services/JobWorkerTokenSelectionTest.kt b/src/test/kotlin/dev/typetype/downloader/services/JobWorkerTokenSelectionTest.kt new file mode 100644 index 0000000..9584e44 --- /dev/null +++ b/src/test/kotlin/dev/typetype/downloader/services/JobWorkerTokenSelectionTest.kt @@ -0,0 +1,62 @@ +package dev.typetype.downloader.services + +import dev.typetype.downloader.models.JobOptions +import kotlin.reflect.full.declaredFunctions +import kotlin.reflect.jvm.isAccessible +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class JobWorkerTokenSelectionTest { + @Test + fun `uses token for non exact jobs`() { + assertTrue(shouldUseToken(JobOptions())) + } + + @Test + fun `skips token for exact selector inputs`() { + assertFalse(shouldUseToken(JobOptions(videoItag = "137"))) + assertFalse(shouldUseToken(JobOptions(audioItag = "251"))) + assertFalse(shouldUseToken(JobOptions(height = 720))) + assertFalse(shouldUseToken(JobOptions(fps = 25))) + assertFalse(shouldUseToken(JobOptions(videoCodec = "avc1.640028"))) + assertFalse(shouldUseToken(JobOptions(audioCodec = "opus"))) + assertFalse(shouldUseToken(JobOptions(bitrate = 160))) + } + + private fun shouldUseToken(options: JobOptions): Boolean { + val method = JobWorker::class.declaredFunctions.first { it.name == "shouldUseTokenFor" } + method.isAccessible = true + return method.call(worker(), options) as Boolean + } + + private fun worker(): JobWorker = JobWorker( + jobsRepository = io.mockk.mockk(relaxed = true), + redis = io.mockk.mockk(relaxed = true), + ytDlpService = io.mockk.mockk(relaxed = true), + tokenServiceClient = io.mockk.mockk(relaxed = true), + storageService = io.mockk.mockk(relaxed = true), + config = dev.typetype.downloader.config.AppConfig( + httpPort = 18093, + dbUrl = "jdbc:postgresql://localhost:55432/typetype_downloader", + dbUser = "typetype", + dbPassword = "typetype", + redisHost = "localhost", + redisPort = 56379, + redisQueueKey = "downloader:queue", + maxConcurrentWorkers = 1, + maxQueueSize = 10, + jobTtlSeconds = 600, + ytdlpBin = "yt-dlp", + ytdlpTimeoutSeconds = 120, + enableTranscode = false, + s3Endpoint = "http://localhost:3900", + s3Region = "garage", + s3Bucket = "typetype-downloads", + s3AccessKey = "demo", + s3SecretKey = "demo", + s3ArtifactTtlSeconds = 7200, + tokenServiceUrl = "http://localhost:8081", + ), + ) +} From 4edb767c82d2edb252d7ebf4d95c6c8f9e48baa3 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Thu, 9 Apr 2026 13:05:46 +0200 Subject: [PATCH 3/3] fix: preserve codec case in exact selector inputs --- .../typetype/downloader/services/JobOptionsNormalizer.kt | 2 +- .../downloader/services/JobOptionsNormalizerTest.kt | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/dev/typetype/downloader/services/JobOptionsNormalizer.kt b/src/main/kotlin/dev/typetype/downloader/services/JobOptionsNormalizer.kt index f5bc6b6..1f0b7fa 100644 --- a/src/main/kotlin/dev/typetype/downloader/services/JobOptionsNormalizer.kt +++ b/src/main/kotlin/dev/typetype/downloader/services/JobOptionsNormalizer.kt @@ -87,7 +87,7 @@ object JobOptionsNormalizer { } private fun normalizeCodec(raw: String): String { - val value = raw.trim().lowercase() + val value = raw.trim() if (value.isBlank()) return "" return if (value.all { it.isLetterOrDigit() || it == '.' || it == '_' || it == '-' }) value else "" } diff --git a/src/test/kotlin/dev/typetype/downloader/services/JobOptionsNormalizerTest.kt b/src/test/kotlin/dev/typetype/downloader/services/JobOptionsNormalizerTest.kt index 4c094e8..853dae8 100644 --- a/src/test/kotlin/dev/typetype/downloader/services/JobOptionsNormalizerTest.kt +++ b/src/test/kotlin/dev/typetype/downloader/services/JobOptionsNormalizerTest.kt @@ -98,4 +98,12 @@ class JobOptionsNormalizerTest { assertEquals("mp4a.40.2", normalized.audioCodec) assertEquals(null, normalized.bitrate) } + + @Test + fun `keeps codec case for av1 profile strings`() { + val normalized = JobOptionsNormalizer.normalize( + JobOptions(videoCodec = "av01.0.08M.08"), + ) + assertEquals("av01.0.08M.08", normalized.videoCodec) + } }