Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/main/kotlin/dev/typetype/server/db/DatabaseFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ object DatabaseFactory {
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS default_audio_language TEXT NOT NULL DEFAULT ''")
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS prefer_original_language BOOLEAN NOT NULL DEFAULT false")
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS enable_high_quality_playback BOOLEAN NOT NULL DEFAULT false")
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS sponsor_block_mode TEXT NOT NULL DEFAULT 'auto_skip'")
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS hide_home_recommendations BOOLEAN NOT NULL DEFAULT false")
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS hide_related_videos BOOLEAN NOT NULL DEFAULT false")
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS hide_comments BOOLEAN NOT NULL DEFAULT false")
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS hide_shorts BOOLEAN NOT NULL DEFAULT false")
exec("ALTER TABLE settings ADD COLUMN IF NOT EXISTS subscription_sync_interval INTEGER NOT NULL DEFAULT 0")
exec("ALTER TABLE history ADD COLUMN IF NOT EXISTS channel_avatar TEXT NOT NULL DEFAULT ''")
exec("ALTER TABLE history ADD COLUMN IF NOT EXISTS user_id TEXT NOT NULL DEFAULT ''")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@ object SettingsTable : Table("settings") {
val defaultAudioLanguage = text("default_audio_language").default("")
val preferOriginalLanguage = bool("prefer_original_language").default(false)
val enableHighQualityPlayback = bool("enable_high_quality_playback").default(false)
val sponsorBlockMode = text("sponsor_block_mode").default("auto_skip")
val hideHomeRecommendations = bool("hide_home_recommendations").default(false)
val hideRelatedVideos = bool("hide_related_videos").default(false)
val hideComments = bool("hide_comments").default(false)
val hideShorts = bool("hide_shorts").default(false)
override val primaryKey = PrimaryKey(userId)
}
5 changes: 5 additions & 0 deletions src/main/kotlin/dev/typetype/server/models/SettingsItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ data class SettingsItem(
val defaultAudioLanguage: String = "",
val preferOriginalLanguage: Boolean = false,
val enableHighQualityPlayback: Boolean = false,
val sponsorBlockMode: SponsorBlockMode = SponsorBlockMode.AUTO_SKIP,
val hideHomeRecommendations: Boolean = false,
val hideRelatedVideos: Boolean = false,
val hideComments: Boolean = false,
val hideShorts: Boolean = false,
)
19 changes: 19 additions & 0 deletions src/main/kotlin/dev/typetype/server/models/SponsorBlockMode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.typetype.server.models

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
enum class SponsorBlockMode(val storageValue: String) {
@SerialName("auto_skip")
AUTO_SKIP("auto_skip"),

@SerialName("mark_only")
MARK_ONLY("mark_only"),

@SerialName("disabled")
DISABLED("disabled"),
}

fun String.toSponsorBlockMode(): SponsorBlockMode =
SponsorBlockMode.entries.firstOrNull { it.storageValue == this } ?: SponsorBlockMode.AUTO_SKIP
16 changes: 16 additions & 0 deletions src/main/kotlin/dev/typetype/server/services/SettingsService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dev.typetype.server.services
import dev.typetype.server.db.DatabaseFactory
import dev.typetype.server.db.tables.SettingsTable
import dev.typetype.server.models.SettingsItem
import dev.typetype.server.models.toSponsorBlockMode
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
Expand All @@ -23,6 +24,11 @@ class SettingsService {
defaultAudioLanguage = it[SettingsTable.defaultAudioLanguage],
preferOriginalLanguage = it[SettingsTable.preferOriginalLanguage],
enableHighQualityPlayback = it[SettingsTable.enableHighQualityPlayback],
sponsorBlockMode = it[SettingsTable.sponsorBlockMode].toSponsorBlockMode(),
hideHomeRecommendations = it[SettingsTable.hideHomeRecommendations],
hideRelatedVideos = it[SettingsTable.hideRelatedVideos],
hideComments = it[SettingsTable.hideComments],
hideShorts = it[SettingsTable.hideShorts],
)
} ?: SettingsItem()
}
Expand All @@ -40,6 +46,11 @@ class SettingsService {
it[defaultAudioLanguage] = settings.defaultAudioLanguage
it[preferOriginalLanguage] = settings.preferOriginalLanguage
it[enableHighQualityPlayback] = settings.enableHighQualityPlayback
it[sponsorBlockMode] = settings.sponsorBlockMode.storageValue
it[hideHomeRecommendations] = settings.hideHomeRecommendations
it[hideRelatedVideos] = settings.hideRelatedVideos
it[hideComments] = settings.hideComments
it[hideShorts] = settings.hideShorts
}
if (updated == 0) {
SettingsTable.insert {
Expand All @@ -54,6 +65,11 @@ class SettingsService {
it[defaultAudioLanguage] = settings.defaultAudioLanguage
it[preferOriginalLanguage] = settings.preferOriginalLanguage
it[enableHighQualityPlayback] = settings.enableHighQualityPlayback
it[sponsorBlockMode] = settings.sponsorBlockMode.storageValue
it[hideHomeRecommendations] = settings.hideHomeRecommendations
it[hideRelatedVideos] = settings.hideRelatedVideos
it[hideComments] = settings.hideComments
it[hideShorts] = settings.hideShorts
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package dev.typetype.server

import dev.typetype.server.routes.settingsRoutes
import dev.typetype.server.services.AuthService
import dev.typetype.server.services.SettingsService
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.request.put
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.routing
import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.server.testing.testApplication
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class SettingsPrivacyControlsRoutesTest {
private val service = SettingsService()
private val auth = AuthService.fixed(TEST_USER_ID)

companion object {
@BeforeAll
@JvmStatic
fun initDb() {
TestDatabase.setup()
}
}

@BeforeEach
fun clean() {
TestDatabase.truncateAll()
}

private fun withApp(block: suspend ApplicationTestBuilder.() -> Unit) = testApplication {
application {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true; encodeDefaults = true }) }
routing { settingsRoutes(service, auth) }
}
block()
}

@Test
fun `GET settings returns SponsorBlock and privacy defaults`() = withApp {
val body = client.get("/settings") {
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
}.bodyAsText()

assertContainsAll(
body,
listOf(
"\"sponsorBlockMode\":\"auto_skip\"",
"\"hideHomeRecommendations\":false",
"\"hideRelatedVideos\":false",
"\"hideComments\":false",
"\"hideShorts\":false",
),
)
}

@Test
fun `PUT settings persists SponsorBlock and privacy controls`() = withApp {
val put = client.put("/settings") {
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(settingsBody())
}
assertEquals(HttpStatusCode.OK, put.status)

val body = client.get("/settings") {
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
}.bodyAsText()
assertContainsAll(
body,
listOf(
"\"sponsorBlockMode\":\"mark_only\"",
"\"hideHomeRecommendations\":true",
"\"hideRelatedVideos\":true",
"\"hideComments\":true",
"\"hideShorts\":true",
),
)
}

@Test
fun `PUT settings rejects invalid SponsorBlock mode`() = withApp {
val response = client.put("/settings") {
headers.append(HttpHeaders.Authorization, "Bearer test-jwt")
headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(settingsBody(sponsorBlockMode = "skip_everything"))
}

assertEquals(HttpStatusCode.BadRequest, response.status)
}

private fun assertContainsAll(body: String, values: List<String>) =
values.forEach { assertTrue(body.contains(it)) }

private fun settingsBody(sponsorBlockMode: String = "mark_only"): String = """
{"defaultService":0,"defaultQuality":"1080p","autoplay":true,"volume":1.0,"muted":false,"sponsorBlockMode":"$sponsorBlockMode","hideHomeRecommendations":true,"hideRelatedVideos":true,"hideComments":true,"hideShorts":true}
""".trimIndent()
}
Loading