Skip to content

Commit 82d8eac

Browse files
committed
feat: add playback privacy settings
1 parent 34cb125 commit 82d8eac

6 files changed

Lines changed: 161 additions & 0 deletions

File tree

src/main/kotlin/dev/typetype/server/db/DatabaseFactory.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ object DatabaseFactory {
6969
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS default_audio_language TEXT NOT NULL DEFAULT ''")
7070
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS prefer_original_language BOOLEAN NOT NULL DEFAULT false")
7171
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS enable_high_quality_playback BOOLEAN NOT NULL DEFAULT false")
72+
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS sponsor_block_mode TEXT NOT NULL DEFAULT 'auto_skip'")
73+
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS hide_home_recommendations BOOLEAN NOT NULL DEFAULT false")
74+
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS hide_related_videos BOOLEAN NOT NULL DEFAULT false")
75+
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS hide_comments BOOLEAN NOT NULL DEFAULT false")
76+
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS hide_shorts BOOLEAN NOT NULL DEFAULT false")
7277
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS subscription_sync_interval INTEGER NOT NULL DEFAULT 0")
7378
exec("ALTER TABLE history ADD COLUMN IF NOT EXISTS channel_avatar TEXT NOT NULL DEFAULT ''")
7479
exec("ALTER TABLE history ADD COLUMN IF NOT EXISTS user_id TEXT NOT NULL DEFAULT ''")

src/main/kotlin/dev/typetype/server/db/tables/SettingsTable.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,10 @@ object SettingsTable : Table("settings") {
1414
val defaultAudioLanguage = text("default_audio_language").default("")
1515
val preferOriginalLanguage = bool("prefer_original_language").default(false)
1616
val enableHighQualityPlayback = bool("enable_high_quality_playback").default(false)
17+
val sponsorBlockMode = text("sponsor_block_mode").default("auto_skip")
18+
val hideHomeRecommendations = bool("hide_home_recommendations").default(false)
19+
val hideRelatedVideos = bool("hide_related_videos").default(false)
20+
val hideComments = bool("hide_comments").default(false)
21+
val hideShorts = bool("hide_shorts").default(false)
1722
override val primaryKey = PrimaryKey(userId)
1823
}

src/main/kotlin/dev/typetype/server/models/SettingsItem.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,9 @@ data class SettingsItem(
1414
val defaultAudioLanguage: String = "",
1515
val preferOriginalLanguage: Boolean = false,
1616
val enableHighQualityPlayback: Boolean = false,
17+
val sponsorBlockMode: SponsorBlockMode = SponsorBlockMode.AUTO_SKIP,
18+
val hideHomeRecommendations: Boolean = false,
19+
val hideRelatedVideos: Boolean = false,
20+
val hideComments: Boolean = false,
21+
val hideShorts: Boolean = false,
1722
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package dev.typetype.server.models
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
enum class SponsorBlockMode(val storageValue: String) {
8+
@SerialName("auto_skip")
9+
AUTO_SKIP("auto_skip"),
10+
11+
@SerialName("mark_only")
12+
MARK_ONLY("mark_only"),
13+
14+
@SerialName("disabled")
15+
DISABLED("disabled"),
16+
}
17+
18+
fun String.toSponsorBlockMode(): SponsorBlockMode =
19+
SponsorBlockMode.entries.firstOrNull { it.storageValue == this } ?: SponsorBlockMode.AUTO_SKIP

src/main/kotlin/dev/typetype/server/services/SettingsService.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dev.typetype.server.services
33
import dev.typetype.server.db.DatabaseFactory
44
import dev.typetype.server.db.tables.SettingsTable
55
import dev.typetype.server.models.SettingsItem
6+
import dev.typetype.server.models.toSponsorBlockMode
67
import org.jetbrains.exposed.v1.core.eq
78
import org.jetbrains.exposed.v1.jdbc.insert
89
import org.jetbrains.exposed.v1.jdbc.selectAll
@@ -23,6 +24,11 @@ class SettingsService {
2324
defaultAudioLanguage = it[SettingsTable.defaultAudioLanguage],
2425
preferOriginalLanguage = it[SettingsTable.preferOriginalLanguage],
2526
enableHighQualityPlayback = it[SettingsTable.enableHighQualityPlayback],
27+
sponsorBlockMode = it[SettingsTable.sponsorBlockMode].toSponsorBlockMode(),
28+
hideHomeRecommendations = it[SettingsTable.hideHomeRecommendations],
29+
hideRelatedVideos = it[SettingsTable.hideRelatedVideos],
30+
hideComments = it[SettingsTable.hideComments],
31+
hideShorts = it[SettingsTable.hideShorts],
2632
)
2733
} ?: SettingsItem()
2834
}
@@ -40,6 +46,11 @@ class SettingsService {
4046
it[defaultAudioLanguage] = settings.defaultAudioLanguage
4147
it[preferOriginalLanguage] = settings.preferOriginalLanguage
4248
it[enableHighQualityPlayback] = settings.enableHighQualityPlayback
49+
it[sponsorBlockMode] = settings.sponsorBlockMode.storageValue
50+
it[hideHomeRecommendations] = settings.hideHomeRecommendations
51+
it[hideRelatedVideos] = settings.hideRelatedVideos
52+
it[hideComments] = settings.hideComments
53+
it[hideShorts] = settings.hideShorts
4354
}
4455
if (updated == 0) {
4556
SettingsTable.insert {
@@ -54,6 +65,11 @@ class SettingsService {
5465
it[defaultAudioLanguage] = settings.defaultAudioLanguage
5566
it[preferOriginalLanguage] = settings.preferOriginalLanguage
5667
it[enableHighQualityPlayback] = settings.enableHighQualityPlayback
68+
it[sponsorBlockMode] = settings.sponsorBlockMode.storageValue
69+
it[hideHomeRecommendations] = settings.hideHomeRecommendations
70+
it[hideRelatedVideos] = settings.hideRelatedVideos
71+
it[hideComments] = settings.hideComments
72+
it[hideShorts] = settings.hideShorts
5773
}
5874
}
5975
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package dev.typetype.server
2+
3+
import dev.typetype.server.routes.settingsRoutes
4+
import dev.typetype.server.services.AuthService
5+
import dev.typetype.server.services.SettingsService
6+
import io.ktor.client.request.get
7+
import io.ktor.client.request.headers
8+
import io.ktor.client.request.put
9+
import io.ktor.client.request.setBody
10+
import io.ktor.client.statement.bodyAsText
11+
import io.ktor.http.ContentType
12+
import io.ktor.http.HttpHeaders
13+
import io.ktor.http.HttpStatusCode
14+
import io.ktor.serialization.kotlinx.json.json
15+
import io.ktor.server.application.install
16+
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
17+
import io.ktor.server.routing.routing
18+
import io.ktor.server.testing.ApplicationTestBuilder
19+
import io.ktor.server.testing.testApplication
20+
import kotlinx.serialization.json.Json
21+
import org.junit.jupiter.api.Assertions.assertEquals
22+
import org.junit.jupiter.api.Assertions.assertTrue
23+
import org.junit.jupiter.api.BeforeAll
24+
import org.junit.jupiter.api.BeforeEach
25+
import org.junit.jupiter.api.Test
26+
27+
class SettingsPrivacyControlsRoutesTest {
28+
private val service = SettingsService()
29+
private val auth = AuthService.fixed(TEST_USER_ID)
30+
31+
companion object {
32+
@BeforeAll
33+
@JvmStatic
34+
fun initDb() {
35+
TestDatabase.setup()
36+
}
37+
}
38+
39+
@BeforeEach
40+
fun clean() {
41+
TestDatabase.truncateAll()
42+
}
43+
44+
private fun withApp(block: suspend ApplicationTestBuilder.() -> Unit) = testApplication {
45+
application {
46+
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true; encodeDefaults = true }) }
47+
routing { settingsRoutes(service, auth) }
48+
}
49+
block()
50+
}
51+
52+
@Test
53+
fun `GET settings returns SponsorBlock and privacy defaults`() = withApp {
54+
val body = client.get("/settings") {
55+
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
56+
}.bodyAsText()
57+
58+
assertContainsAll(
59+
body,
60+
listOf(
61+
"\"sponsorBlockMode\":\"auto_skip\"",
62+
"\"hideHomeRecommendations\":false",
63+
"\"hideRelatedVideos\":false",
64+
"\"hideComments\":false",
65+
"\"hideShorts\":false",
66+
),
67+
)
68+
}
69+
70+
@Test
71+
fun `PUT settings persists SponsorBlock and privacy controls`() = withApp {
72+
val put = client.put("/settings") {
73+
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
74+
headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
75+
setBody(settingsBody())
76+
}
77+
assertEquals(HttpStatusCode.OK, put.status)
78+
79+
val body = client.get("/settings") {
80+
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
81+
}.bodyAsText()
82+
assertContainsAll(
83+
body,
84+
listOf(
85+
"\"sponsorBlockMode\":\"mark_only\"",
86+
"\"hideHomeRecommendations\":true",
87+
"\"hideRelatedVideos\":true",
88+
"\"hideComments\":true",
89+
"\"hideShorts\":true",
90+
),
91+
)
92+
}
93+
94+
@Test
95+
fun `PUT settings rejects invalid SponsorBlock mode`() = withApp {
96+
val response = client.put("/settings") {
97+
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
98+
headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
99+
setBody(settingsBody(sponsorBlockMode = "skip_everything"))
100+
}
101+
102+
assertEquals(HttpStatusCode.BadRequest, response.status)
103+
}
104+
105+
private fun assertContainsAll(body: String, values: List<String>) =
106+
values.forEach { assertTrue(body.contains(it)) }
107+
108+
private fun settingsBody(sponsorBlockMode: String = "mark_only"): String = """
109+
{"defaultService":0,"defaultQuality":"1080p","autoplay":true,"volume":1.0,"muted":false,"sponsorBlockMode":"$sponsorBlockMode","hideHomeRecommendations":true,"hideRelatedVideos":true,"hideComments":true,"hideShorts":true}
110+
""".trimIndent()
111+
}

0 commit comments

Comments
 (0)