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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@ DATABASE_PASSWORD=typetype
DRAGONFLY_URL=redis://dragonfly:6379
DOWNLOADER_SERVICE_URL=http://typetype-downloader:18093
YOUTUBE_SESSION_ENCRYPTION_KEY=replace-with-at-least-32-random-characters
YOUTUBE_REMOTE_LOGIN_SERVICE_URL=http://typetype-token:8081
YOUTUBE_REMOTE_LOGIN_CALLBACK_BASE_URL=http://typetype-server:8080
YOUTUBE_REMOTE_LOGIN_INTERNAL_TOKEN=replace-with-shared-internal-token
YOUTUBE_REMOTE_LOGIN_TTL_MS=480000
YOUTUBE_REMOTE_LOGIN_MAX_SESSIONS=2

ALLOWED_ORIGINS=http://localhost:5173
9 changes: 4 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
plugins {
kotlin("jvm") version "2.3.21"
kotlin("plugin.serialization") version "2.3.21"
kotlin("jvm") version "2.4.0"
kotlin("plugin.serialization") version "2.4.0"
id("io.ktor.plugin") version "3.5.0"
id("jacoco")
}

apply(from = "gradle/openapi-validation.gradle.kts")

group = "dev.typetype"
version = "0.0.1"

application {
mainClass.set("dev.typetype.server.ApplicationKt")
}
Expand All @@ -27,11 +25,12 @@ dependencies {
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation("io.ktor:ktor-server-cors-jvm")
implementation("io.ktor:ktor-server-compression-jvm")
implementation("io.ktor:ktor-server-websockets-jvm")
implementation("io.ktor:ktor-server-status-pages-jvm")
implementation("io.ktor:ktor-server-call-logging-jvm")
implementation("io.ktor:ktor-server-rate-limit-jvm")
implementation("ch.qos.logback:logback-classic:1.5.34")
implementation("com.github.InfinityLoop1308.PipePipeExtractor:extractor:3c8feae0548ba7e30bce92f881a1924121c3dc83")
implementation("com.github.InfinityLoop1308.PipePipeExtractor:extractor:2a4a333c0e64a96343d35981fd5e75fd122526c2")
implementation("org.json:json:20250517")
implementation("com.squareup.okhttp3:okhttp:5.3.2")
implementation("io.lettuce:lettuce-core:7.6.0.RELEASE")
Expand Down
17 changes: 15 additions & 2 deletions docker-compose.dev-mirror.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ services:
ALLOWED_ORIGINS: http://localhost:28082,http://127.0.0.1:28082
SUBTITLE_SERVICE_URL: http://typetype-token:8081
DOWNLOADER_SERVICE_URL: http://typetype-downloader:18093
YOUTUBE_SESSION_ENCRYPTION_KEY: beta-youtube-session-key-change-before-prod
YOUTUBE_REMOTE_LOGIN_SERVICE_URL: http://typetype-token:8081
YOUTUBE_REMOTE_LOGIN_CALLBACK_BASE_URL: http://typetype-server:8080
YOUTUBE_REMOTE_LOGIN_INTERNAL_TOKEN: beta-remote-login-internal-token
YOUTUBE_REMOTE_LOGIN_TTL_MS: "480000"
YOUTUBE_REMOTE_LOGIN_MAX_SESSIONS: "2"
depends_on:
postgres-init: { condition: service_completed_successfully }
dragonfly: { condition: service_started }
Expand Down Expand Up @@ -56,12 +62,19 @@ services:
typetype-token: { condition: service_started }
restart: unless-stopped
typetype-token:
image: ghcr.io/priveetee/typetype-token:latest
image: ghcr.io/priveetee/typetype-token-beta:beta
pull_policy: always
ports: ["28081:8081"]
init: true
ipc: host
environment: ["NODE_ENV=production"]
environment:
NODE_ENV: production
YOUTUBE_REMOTE_LOGIN_ENABLED: "true"
YOUTUBE_REMOTE_LOGIN_INTERNAL_TOKEN: beta-remote-login-internal-token
YOUTUBE_REMOTE_LOGIN_CALLBACK_ORIGIN: http://typetype-server:8080
YOUTUBE_REMOTE_LOGIN_MAX_SESSIONS: "2"
YOUTUBE_REMOTE_LOGIN_FRAME_FPS: "10"
YOUTUBE_REMOTE_LOGIN_MAX_FRAME_BYTES: "524288"
restart: unless-stopped
postgres:
image: postgres:17
Expand Down
15 changes: 15 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ tags:
- name: health
- name: extraction
- name: downloader
- name: youtube-session
paths:
/health:
$ref: ./openapi/paths/health.yaml#/Health
Expand Down Expand Up @@ -51,6 +52,14 @@ paths:
$ref: ./openapi/paths/downloader.yaml#/JobArtifact
/downloader/jobs/{id}/cancel:
$ref: ./openapi/paths/downloader.yaml#/JobCancel
/youtube-session/browser/start:
$ref: ./openapi/paths/youtube-session.yaml#/BrowserStart
/youtube-session/browser/{sessionId}:
$ref: ./openapi/paths/youtube-session.yaml#/BrowserSession
/internal/youtube-remote-login/callback:
$ref: ./openapi/paths/youtube-session.yaml#/InternalBrowserComplete
/internal/youtube-session/browser/complete:
$ref: ./openapi/paths/youtube-session.yaml#/InternalBrowserComplete
components:
schemas:
ErrorResponse:
Expand Down Expand Up @@ -87,3 +96,9 @@ components:
$ref: ./openapi/components/media.yaml#/CommentsPageResponse
DownloadJob:
$ref: ./openapi/components/downloader.yaml#/DownloadJob
YoutubeRemoteBrowserStartRequest:
$ref: ./openapi/components/youtube-session.yaml#/YoutubeRemoteBrowserStartRequest
YoutubeRemoteBrowserStartResponse:
$ref: ./openapi/components/youtube-session.yaml#/YoutubeRemoteBrowserStartResponse
YoutubeRemoteBrowserCompleteRequest:
$ref: ./openapi/components/youtube-session.yaml#/YoutubeRemoteBrowserCompleteRequest
26 changes: 26 additions & 0 deletions openapi/components/youtube-session.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
YoutubeRemoteBrowserStartRequest:
type: object
properties:
returnTo:
type: string
nullable: true
maxLength: 2048
YoutubeRemoteBrowserStartResponse:
type: object
required: [sessionId, wsUrl, expiresAt]
properties:
sessionId: { type: string }
wsUrl: { type: string }
expiresAt: { type: integer, format: int64 }
YoutubeRemoteBrowserCompleteRequest:
type: object
required: [sessionId, tokenSessionId, status, cookies, poToken, capturedAt]
properties:
sessionId: { type: string }
tokenSessionId: { type: string }
status:
type: string
enum: [completed]
cookies: { type: string }
poToken: { type: string }
capturedAt: { type: integer, format: int64 }
109 changes: 109 additions & 0 deletions openapi/paths/youtube-session.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
BrowserStart:
post:
tags: [youtube-session]
summary: Start a temporary remote Chromium login session
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
returnTo:
type: string
nullable: true
maxLength: 2048
responses:
'201':
description: Remote browser session created.
content:
application/json:
schema:
type: object
required: [sessionId, wsUrl, expiresAt]
properties:
sessionId: { type: string }
wsUrl: { type: string }
expiresAt: { type: integer, format: int64 }
'401': { $ref: '#/components/responses/JsonError' }
'403': { $ref: '#/components/responses/JsonError' }
'409': { $ref: '#/components/responses/JsonError' }
'429': { $ref: '#/components/responses/JsonError' }
'503': { $ref: '#/components/responses/JsonError' }
BrowserSession:
parameters:
- name: sessionId
in: path
required: true
schema: { type: string }
get:
tags: [youtube-session]
summary: WebSocket endpoint for remote browser frames and input
parameters:
- name: token
in: query
required: true
schema: { type: string }
responses:
'101': { description: WebSocket upgrade accepted. }
'401': { $ref: '#/components/responses/JsonError' }
delete:
tags: [youtube-session]
summary: Cancel a temporary remote browser login session
responses:
'204': { description: Remote browser session cancelled. }
'401': { $ref: '#/components/responses/JsonError' }
'404': { $ref: '#/components/responses/JsonError' }
InternalBrowserComplete:
post:
tags: [youtube-session]
summary: Internal callback used by TypeType-Token after capture
parameters:
- name: X-Internal-Token
in: header
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [sessionId, tokenSessionId, status, cookies, poToken, capturedAt]
properties:
sessionId: { type: string }
tokenSessionId: { type: string }
status:
type: string
enum: [completed]
cookies: { type: string }
poToken: { type: string }
capturedAt: { type: integer, format: int64 }
responses:
'204': { description: YouTube credentials stored. }
'400': { $ref: '#/components/responses/JsonError' }
'401': { $ref: '#/components/responses/JsonError' }
'404': { $ref: '#/components/responses/JsonError' }
components:
schemas:
ErrorResponse:
type: object
required: [error]
properties:
error: { type: string }
code: { type: string, default: error }
requestId: { type: string, nullable: true }
headers:
RequestIdHeader:
description: Request correlation id returned by TypeType-Server.
schema: { type: string }
responses:
JsonError:
description: Request failed.
headers:
X-Request-ID:
$ref: '#/components/headers/RequestIdHeader'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
97 changes: 34 additions & 63 deletions src/main/kotlin/dev/typetype/server/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,6 @@ package dev.typetype.server
import dev.typetype.server.cache.DragonflyService
import dev.typetype.server.db.DatabaseFactory
import dev.typetype.server.downloader.OkHttpDownloader
import dev.typetype.server.routes.avatarRoutes
import dev.typetype.server.routes.bulletCommentRoutes
import dev.typetype.server.routes.channelRoutes
import dev.typetype.server.routes.commentRoutes
import dev.typetype.server.routes.downloaderGatewayRoutes
import dev.typetype.server.routes.internalObservabilityRoutes
import dev.typetype.server.routes.manifestRoutes
import dev.typetype.server.routes.nicoVideoProxyRoutes
import dev.typetype.server.routes.oidcAuthRoutes
import dev.typetype.server.routes.podcastRoutes
import dev.typetype.server.routes.proxyRoutes
import dev.typetype.server.routes.publicPlaylistRoutes
import dev.typetype.server.routes.storyboardProxyRoutes
import dev.typetype.server.routes.searchRoutes
import dev.typetype.server.routes.streamRoutes
import dev.typetype.server.routes.suggestionRoutes
import dev.typetype.server.routes.adminSessionRoutes
import dev.typetype.server.routes.adminRoutes
import dev.typetype.server.routes.adminBugReportRoutes
import dev.typetype.server.routes.authRoutes
import dev.typetype.server.routes.trendingRoutes
import dev.typetype.server.routes.publicMetadataRoutes
import dev.typetype.server.routes.sessionActivityRoutes
import dev.typetype.server.routes.userDataRoutes
import dev.typetype.server.services.ActiveSessionService
import dev.typetype.server.services.AuthService
import dev.typetype.server.services.AdminSettingsService
Expand All @@ -40,11 +16,12 @@ import dev.typetype.server.services.InstanceService
import dev.typetype.server.services.InternalHealthService
import dev.typetype.server.services.OidcAuthService
import dev.typetype.server.services.OidcConfigLoader
import dev.typetype.server.services.OkHttpYoutubeRemoteBrowserClient
import dev.typetype.server.services.UserAdminService
import dev.typetype.server.services.YoutubeRemoteBrowserConfig
import dev.typetype.server.services.YoutubeRemoteBrowserService
import io.ktor.server.application.Application
import io.ktor.server.netty.EngineMain
import io.ktor.server.plugins.ratelimit.rateLimit
import io.ktor.server.routing.routing
import org.schabi.newpipe.extractor.NewPipe
import java.util.UUID

Expand All @@ -67,53 +44,47 @@ fun Application.module() {
val gitHubIssueService = GitHubIssueService()
val adminSettingsService = AdminSettingsService()
val activeSessionService = ActiveSessionService(adminSettingsService)
val instanceService = InstanceService(authService, adminSettingsService, oidcAuthService::publicConfig)
val restoreService = PipePipeBackupImporterService()
val cacheUrl = System.getenv("DRAGONFLY_URL") ?: "redis://localhost:6379"
val subtitleServiceUrl = System.getenv("SUBTITLE_SERVICE_URL") ?: "http://typetype-token:8081"
val youtubeRemoteBrowserConfig = YoutubeRemoteBrowserConfig.fromEnvironment(subtitleServiceUrl)
val instanceService = InstanceService(
authService,
adminSettingsService,
youtubeRemoteLoginAvailable = { youtubeRemoteBrowserConfig.isConfigured },
oidcConfigProvider = oidcAuthService::publicConfig,
)
val downloaderServiceUrl = System.getenv("DOWNLOADER_SERVICE_URL") ?: "http://typetype-downloader:18093"
val youtubeSessionEncryptionKey = System.getenv("YOUTUBE_SESSION_ENCRYPTION_KEY")
?: error("YOUTUBE_SESSION_ENCRYPTION_KEY is required")
val cache = DragonflyService(cacheUrl)
val svc = ServiceRegistry(cache, subtitleServiceUrl, youtubeSessionEncryptionKey)
val youtubeRemoteBrowserService = YoutubeRemoteBrowserService(
youtubeRemoteBrowserConfig,
adminSettingsService,
svc.youtubeSessionService,
OkHttpYoutubeRemoteBrowserClient(youtubeRemoteBrowserConfig.serviceUrl),
)
val downloaderGatewayService = DownloaderGatewayService(downloaderServiceUrl)
val openMojiProxyService = OpenMojiProxyService(cache)
val internalHealthService = InternalHealthService(cache, downloaderGatewayService, subtitleServiceUrl)
configurePlugins(authService)
routing {
internalObservabilityRoutes(internalHealthService::check)
publicMetadataRoutes(instanceService::getInstance)
rateLimit(STREAMS_ZONE) {
streamRoutes(svc.streamService, authService, svc.youtubeSessionStreamService::getStreamInfo)
manifestRoutes(svc.manifestService, svc.nativeManifestService, svc.hlsManifestService)
}
rateLimit(EXTRACTION_ZONE) {
searchRoutes(svc.searchService)
suggestionRoutes(svc.suggestionService)
trendingRoutes(svc.trendingService)
publicPlaylistRoutes(svc.publicPlaylistService)
commentRoutes(svc.commentService)
bulletCommentRoutes(svc.bulletCommentService)
}
rateLimit(CHANNEL_ZONE) {
channelRoutes(svc.channelService)
podcastRoutes(svc.podcastService)
}
rateLimit(PROXY_ZONE) {
proxyRoutes(svc.proxyService)
nicoVideoProxyRoutes(svc.nicoVideoProxyService)
}
rateLimit(PROXY_STORYBOARD_ZONE) {
storyboardProxyRoutes(svc.proxyService)
}
downloaderGatewayRoutes(downloaderGatewayService)
oidcAuthRoutes(oidcAuthService, adminSettingsService)
authRoutes(authService, passwordResetService, profileService, adminSettingsService, svc.homeRecommendationWarmupService)
adminRoutes(authService, userAdminService, passwordResetService, adminSettingsService)
adminSessionRoutes(authService, activeSessionService)
sessionActivityRoutes(authService, activeSessionService)
adminBugReportRoutes(authService, svc.bugReportService, gitHubIssueService)
avatarRoutes(avatarService, openMojiProxyService)
rateLimit(USER_DATA_ZONE) { userDataRoutes(svc, authService, profileService, avatarService, svc.bugReportService, restoreService) }
}
installApplicationRoutes(
svc = svc,
authService = authService,
adminSettingsService = adminSettingsService,
activeSessionService = activeSessionService,
downloaderGatewayService = downloaderGatewayService,
gitHubIssueService = gitHubIssueService,
instanceService = instanceService,
oidcAuthService = oidcAuthService,
passwordResetService = passwordResetService,
profileService = profileService,
userAdminService = userAdminService,
avatarService = avatarService,
openMojiProxyService = openMojiProxyService,
internalHealthService = internalHealthService,
restoreService = restoreService,
youtubeRemoteBrowserService = youtubeRemoteBrowserService,
)
}
Loading
Loading