From ecce1acfebf2ac22d029356175a915a9a60508a9 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Sun, 10 May 2026 16:18:08 +0200 Subject: [PATCH] feat: enrich playlist video metadata --- .../kotlin/dev/typetype/server/db/DatabaseFactory.kt | 4 ++++ .../typetype/server/db/tables/PlaylistVideosTable.kt | 4 ++++ .../dev/typetype/server/models/PlaylistVideoItem.kt | 11 +++++++++++ .../dev/typetype/server/services/PlaylistService.kt | 8 ++++++++ .../kotlin/dev/typetype/server/PlaylistRoutesTest.kt | 10 +++++----- .../kotlin/dev/typetype/server/PlaylistServiceTest.kt | 6 +++++- 6 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/dev/typetype/server/db/DatabaseFactory.kt b/src/main/kotlin/dev/typetype/server/db/DatabaseFactory.kt index a3b98af..7dbe4e0 100644 --- a/src/main/kotlin/dev/typetype/server/db/DatabaseFactory.kt +++ b/src/main/kotlin/dev/typetype/server/db/DatabaseFactory.kt @@ -78,6 +78,10 @@ object DatabaseFactory { exec("ALTER TABLE search_history ADD COLUMN IF NOT EXISTS user_id TEXT NOT NULL DEFAULT ''") exec("ALTER TABLE playlists ADD COLUMN IF NOT EXISTS user_id TEXT NOT NULL DEFAULT ''") exec("ALTER TABLE playlist_videos ADD COLUMN IF NOT EXISTS user_id TEXT NOT NULL DEFAULT ''") + exec("ALTER TABLE playlist_videos ADD COLUMN IF NOT EXISTS channel_name TEXT NOT NULL DEFAULT ''") + exec("ALTER TABLE playlist_videos ADD COLUMN IF NOT EXISTS channel_url TEXT NOT NULL DEFAULT ''") + exec("ALTER TABLE playlist_videos ADD COLUMN IF NOT EXISTS channel_avatar TEXT NOT NULL DEFAULT ''") + exec("ALTER TABLE playlist_videos ADD COLUMN IF NOT EXISTS view_count BIGINT NOT NULL DEFAULT 0") exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS user_id TEXT NOT NULL DEFAULT ''") exec("ALTER TABLE blocked_channels ADD COLUMN IF NOT EXISTS user_id TEXT NOT NULL DEFAULT ''") exec("ALTER TABLE blocked_channels ADD COLUMN IF NOT EXISTS scope TEXT NOT NULL DEFAULT 'user'") diff --git a/src/main/kotlin/dev/typetype/server/db/tables/PlaylistVideosTable.kt b/src/main/kotlin/dev/typetype/server/db/tables/PlaylistVideosTable.kt index 2328122..641b429 100644 --- a/src/main/kotlin/dev/typetype/server/db/tables/PlaylistVideosTable.kt +++ b/src/main/kotlin/dev/typetype/server/db/tables/PlaylistVideosTable.kt @@ -12,5 +12,9 @@ object PlaylistVideosTable : Table("playlist_videos") { val thumbnail = text("thumbnail") val duration = long("duration") val position = integer("position") + val channelName = text("channel_name").default("") + val channelUrl = text("channel_url").default("") + val channelAvatar = text("channel_avatar").default("") + val viewCount = long("view_count").default(0L) override val primaryKey = PrimaryKey(id) } diff --git a/src/main/kotlin/dev/typetype/server/models/PlaylistVideoItem.kt b/src/main/kotlin/dev/typetype/server/models/PlaylistVideoItem.kt index 90a3cf2..495d44c 100644 --- a/src/main/kotlin/dev/typetype/server/models/PlaylistVideoItem.kt +++ b/src/main/kotlin/dev/typetype/server/models/PlaylistVideoItem.kt @@ -1,13 +1,24 @@ package dev.typetype.server.models +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames +@OptIn(ExperimentalSerializationApi::class) @Serializable data class PlaylistVideoItem( val id: String = "", val url: String, val title: String, + @JsonNames("thumbnailUrl") val thumbnail: String, val duration: Long, val position: Int = 0, + @JsonNames("uploaderName") + val channelName: String = "", + @JsonNames("uploaderUrl") + val channelUrl: String = "", + @JsonNames("channelAvatarUrl", "uploaderAvatarUrl") + val channelAvatar: String = "", + val viewCount: Long = 0L, ) diff --git a/src/main/kotlin/dev/typetype/server/services/PlaylistService.kt b/src/main/kotlin/dev/typetype/server/services/PlaylistService.kt index 9b5a689..8d9fe44 100644 --- a/src/main/kotlin/dev/typetype/server/services/PlaylistService.kt +++ b/src/main/kotlin/dev/typetype/server/services/PlaylistService.kt @@ -81,6 +81,10 @@ class PlaylistService { it[thumbnail] = video.thumbnail it[duration] = video.duration it[position] = pos + it[channelName] = video.channelName + it[channelUrl] = video.channelUrl + it[channelAvatar] = video.channelAvatar + it[viewCount] = video.viewCount } } return video.copy(id = videoId, position = pos) @@ -97,5 +101,9 @@ class PlaylistService { thumbnail = this[PlaylistVideosTable.thumbnail], duration = this[PlaylistVideosTable.duration], position = this[PlaylistVideosTable.position], + channelName = this[PlaylistVideosTable.channelName], + channelUrl = this[PlaylistVideosTable.channelUrl], + channelAvatar = this[PlaylistVideosTable.channelAvatar], + viewCount = this[PlaylistVideosTable.viewCount], ) } diff --git a/src/test/kotlin/dev/typetype/server/PlaylistRoutesTest.kt b/src/test/kotlin/dev/typetype/server/PlaylistRoutesTest.kt index 1c11405..bc2298a 100644 --- a/src/test/kotlin/dev/typetype/server/PlaylistRoutesTest.kt +++ b/src/test/kotlin/dev/typetype/server/PlaylistRoutesTest.kt @@ -27,7 +27,6 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class PlaylistRoutesTest { - private val service = PlaylistService() private val auth = AuthService.fixed(TEST_USER_ID) @@ -49,7 +48,7 @@ class PlaylistRoutesTest { } private val playlistBody = """{"name":"My Playlist","description":""}""" - private val videoBody = """{"url":"https://yt.com","title":"T","thumbnail":"","duration":100}""" + private val videoBody = """{"url":"https://yt.com","title":"T","thumbnailUrl":"thumb","duration":100,"uploaderName":"C","uploaderUrl":"https://c","uploaderAvatarUrl":"avatar","viewCount":123}""" @Test fun `GET playlists without token returns 401`() = withApp { @@ -73,7 +72,6 @@ class PlaylistRoutesTest { assertEquals(HttpStatusCode.Created, response.status) assertTrue(response.bodyAsText().contains("\"name\":\"My Playlist\"")) } - @Test fun `GET playlists by id returns 200 when found`() = withApp { val playlist = service.create(TEST_USER_ID, PlaylistItem(name = "My Playlist")) @@ -104,11 +102,13 @@ class PlaylistRoutesTest { @Test fun `POST playlists videos returns 201`() = withApp { val playlist = service.create(TEST_USER_ID, PlaylistItem(name = "My Playlist")) - assertEquals(HttpStatusCode.Created, client.post("/playlists/${playlist.id}/videos") { + val response = client.post("/playlists/${playlist.id}/videos") { headers.append(HttpHeaders.Authorization, "Bearer test-jwt") headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) setBody(videoBody) - }.status) + } + assertEquals(HttpStatusCode.Created, response.status) + assertTrue(response.bodyAsText().contains("\"channelName\":\"C\"")) } @Test diff --git a/src/test/kotlin/dev/typetype/server/PlaylistServiceTest.kt b/src/test/kotlin/dev/typetype/server/PlaylistServiceTest.kt index b33bc73..01f2e39 100644 --- a/src/test/kotlin/dev/typetype/server/PlaylistServiceTest.kt +++ b/src/test/kotlin/dev/typetype/server/PlaylistServiceTest.kt @@ -40,11 +40,15 @@ class PlaylistServiceTest { @Test fun `getAll returns created playlists with videos populated`() = runBlocking { val playlist = service.create(TEST_USER_ID, PlaylistItem(name = "Test")) - service.addVideo(TEST_USER_ID, playlist.id, PlaylistVideoItem(url = "https://yt.com", title = "T", thumbnail = "", duration = 100L)) + service.addVideo(TEST_USER_ID, playlist.id, PlaylistVideoItem(url = "https://yt.com", title = "T", thumbnail = "", duration = 100L, channelName = "C", channelUrl = "https://c", channelAvatar = "avatar", viewCount = 123L)) val all = service.getAll(TEST_USER_ID) assertEquals(1, all.size) assertEquals(1, all[0].videos.size) assertEquals("https://yt.com", all[0].videos[0].url) + assertEquals("C", all[0].videos[0].channelName) + assertEquals("https://c", all[0].videos[0].channelUrl) + assertEquals("avatar", all[0].videos[0].channelAvatar) + assertEquals(123L, all[0].videos[0].viewCount) } @Test