Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5bc66b3
chore: prepare server 1.3.1 beta
Priveetee Jul 30, 2026
75fad74
fix: release replaced SABR sessions
Priveetee Jul 30, 2026
1c8bcbf
perf: bound SABR memory caches
Priveetee Jul 30, 2026
a856b08
fix: invalidate rejected SABR playback context
Priveetee Jul 31, 2026
0b42a14
fix: refresh rejected SABR token context
Priveetee Jul 31, 2026
91be9b3
chore: align PipePipeExtractor token providers
Priveetee Aug 1, 2026
e197f92
fix: proxy YouTube subtitle tracks
Priveetee Aug 1, 2026
7a49e97
test: cover YouTube subtitle proxying
Priveetee Aug 1, 2026
bf75639
chore: update PipePipeExtractor revision
Priveetee Aug 1, 2026
a886f2b
fix: enforce blocked content on stream routes
Priveetee Aug 1, 2026
f2cf545
fix: preserve typed subtitle throttling errors
Priveetee Aug 1, 2026
d61748c
feat: add grouped search filter API
Priveetee Aug 1, 2026
fc86339
test: cover grouped search filters
Priveetee Aug 1, 2026
de5ec9e
fix: keep YouTube requests on configured proxy
Priveetee Aug 1, 2026
0b4efe5
fix: bootstrap SABR before restoring playback position
Priveetee Aug 1, 2026
64aa66f
fix: recover rejected SABR attestations
Priveetee Aug 1, 2026
1675137
feat: define YouTube subtitle delivery contract
Priveetee Aug 1, 2026
e59c533
feat: cache YouTube subtitle delivery
Priveetee Aug 1, 2026
49152b5
feat: serve YouTube subtitles through Server
Priveetee Aug 1, 2026
548992d
test: cover YouTube subtitle resolution
Priveetee Aug 1, 2026
19d0839
test: cover YouTube subtitle delivery
Priveetee Aug 1, 2026
7cdb25c
fix: isolate YouTube subtitle extraction
Priveetee Aug 1, 2026
9341522
fix: reuse YouTube subtitle stream metadata
Priveetee Aug 1, 2026
12b3067
fix: keep subtitle content on token egress
Priveetee Aug 1, 2026
6d49da2
fix: avoid double compression on rate limits
Priveetee Aug 1, 2026
2b2aacb
fix: preload adaptive SABR initialization ranges
Priveetee Aug 4, 2026
49a75ee
fix: filter cached blocked recommendations
Priveetee Aug 5, 2026
225f83a
feat: configure account session lifetime
Priveetee Aug 5, 2026
e92df8d
test: cover configurable account sessions
Priveetee Aug 5, 2026
71d3b92
test: make fake cache safe for concurrent refreshes
Priveetee Aug 5, 2026
8895f8a
fix: filter unavailable Takeout videos
Priveetee Aug 5, 2026
6f31f93
chore: reconcile main into dev
Priveetee Aug 5, 2026
51786d2
fix: align YouTube clients with PipePipeExtractor
Priveetee Aug 6, 2026
e5ad1a3
chore: prepare server 1.4.0
Priveetee Aug 6, 2026
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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ dependencies {
implementation("io.ktor:ktor-server-call-logging-jvm")
implementation("io.ktor:ktor-server-rate-limit-jvm")
implementation("ch.qos.logback:logback-classic:1.6.1")
implementation("com.github.Priveetee.PipePipeExtractor:extractor:21ab9b6ac415d23ef88df19c5ad126c7952bb543")
implementation("com.github.Priveetee.PipePipeExtractor:extractor:f156813dd4bbebf3b4dffe541fee6c27ae1dd294")
compileOnly("com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751")
implementation("org.json:json:20260719")
implementation("com.squareup.okhttp3:okhttp:5.4.0")
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
org.gradle.jvmargs=-Xmx2g -XX:+UseG1GC
kotlin.code.style=official
appVersion=1.3.1
appVersion=1.4.0
systemProp.sun.net.client.defaultReadTimeout=180000
systemProp.sun.net.client.defaultConnectTimeout=60000
6 changes: 6 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ paths:
/streams/bilibili: { $ref: ./openapi/paths/streams.yaml#/BiliBiliStreams }
/streams/audio-only: { $ref: ./openapi/paths/streams.yaml#/AudioOnly }
/streams/audio-only/source: { $ref: ./openapi/paths/streams.yaml#/AudioOnlySource }
/subtitles/youtube/{videoId}: { $ref: ./openapi/paths/subtitles.yaml#/YoutubeSubtitle }
/proxy: { $ref: ./openapi/paths/proxy.yaml#/Proxy }
/sabr/download/{videoId}: { $ref: ./openapi/paths/sabr-download.yaml#/SabrDownload }
/sabr/playback/{sessionId}/position: { $ref: ./openapi/paths/sabr-playback.yaml#/SabrPlaybackPosition }
/sabr/playback/{sessionId}/window: { $ref: ./openapi/paths/sabr-playback.yaml#/SabrPlaybackWindow }
Expand Down Expand Up @@ -100,6 +102,8 @@ components:
$ref: ./openapi/components/streams.yaml#/StreamResponse
AudioOnlyStreamResponse:
$ref: ./openapi/components/streams.yaml#/AudioOnlyStreamResponse
SubtitleItem:
$ref: ./openapi/components/streams.yaml#/SubtitleItem
SabrPlaybackPositionRequest:
$ref: ./openapi/components/sabr-playback.yaml#/SabrPlaybackPositionRequest
SabrPlaybackWindowRequest:
Expand All @@ -108,6 +112,8 @@ components:
$ref: ./openapi/components/media.yaml#/SearchPageResponse
SearchFilterOption:
$ref: ./openapi/components/media.yaml#/SearchFilterOption
SearchFilterGroup:
$ref: ./openapi/components/media.yaml#/SearchFilterGroup
SearchFiltersResponse:
$ref: ./openapi/components/media.yaml#/SearchFiltersResponse
ChannelResultItem:
Expand Down
12 changes: 11 additions & 1 deletion openapi/components/media.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,22 @@ SearchFilterOption:
properties:
value: { type: string }
label: { type: string }
isDefault: { type: boolean, default: false }
SearchFilterGroup:
type: object
required: [key, label, multiSelect, options]
properties:
key: { type: string }
label: { type: string }
multiSelect: { type: boolean }
options: { type: array, items: { $ref: '#/SearchFilterOption' } }
SearchFiltersResponse:
type: object
required: [contentFilters, sortFilters]
required: [contentFilters, sortFilters, filterGroups]
properties:
contentFilters: { type: array, items: { $ref: '#/SearchFilterOption' } }
sortFilters: { type: array, items: { $ref: '#/SearchFilterOption' } }
filterGroups: { type: array, items: { $ref: '#/SearchFilterGroup' }, default: [] }
ChannelResponse:
type: object
required: [name, description, avatarUrl, bannerUrl, subscriberCount, isVerified, videos, nextpage]
Expand Down
11 changes: 10 additions & 1 deletion openapi/components/streams.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ AudioOnlyStreamResponse:
bitrate: { type: integer, nullable: true }
contentLength: { type: integer, format: int64, nullable: true }
duration: { type: integer, format: int64, nullable: true }
SubtitleItem:
type: object
required: [url, mimeType, languageTag, displayLanguageName, isAutoGenerated]
properties:
url: { type: string, format: uri }
mimeType: { type: string }
languageTag: { type: string }
displayLanguageName: { type: string }
isAutoGenerated: { type: boolean }
StreamResponse:
type: object
required: [id, title, uploaderName, uploaderUrl, thumbnailUrl, description, duration, viewCount, uploadDate, uploaded, streamType, isLive, isPostLive, isLiveContent, hasLiveManifest, videoStreams, audioStreams, videoOnlyStreams, subtitles, relatedStreams]
Expand Down Expand Up @@ -77,6 +86,6 @@ StreamResponse:
videoStreams: { type: array, items: { $ref: '#/VideoStreamItem' } }
audioStreams: { type: array, items: { $ref: '#/AudioStreamItem' } }
videoOnlyStreams: { type: array, items: { $ref: '#/VideoStreamItem' } }
subtitles: { type: array, items: { type: object }, default: [] }
subtitles: { type: array, items: { $ref: '#/SubtitleItem' }, default: [] }
relatedStreams: { type: array, items: { $ref: './media.yaml#/VideoItem' } }
sponsorBlockSegments: { type: array, items: { type: object }, default: [] }
50 changes: 50 additions & 0 deletions openapi/paths/proxy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Proxy:
get:
tags: [extraction]
summary: Retrieve proxied media
description: >-
Streams supported remote content. Existing clients that submit a YouTube timed-text URL are
routed through the dedicated subtitle resolver for compatibility; new clients should use
/subtitles/youtube/{videoId}.
parameters:
- name: url
in: query
required: true
schema: { type: string, format: uri }
- name: Range
in: header
required: false
schema: { type: string }
responses:
'200':
description: Complete proxied content.
headers:
X-Request-ID:
$ref: ../components/common.yaml#/RequestIdHeader
content:
text/vtt:
schema: { type: string }
application/octet-stream:
schema: { type: string, format: binary }
'206':
description: Partial generic proxy response.
headers:
X-Request-ID:
$ref: ../components/common.yaml#/RequestIdHeader
Content-Range:
schema: { type: string }
Accept-Ranges:
schema: { type: string }
content:
application/octet-stream:
schema: { type: string, format: binary }
'400':
$ref: ../components/common.yaml#/JsonError
'404':
$ref: ../components/common.yaml#/JsonError
'422':
$ref: ../components/common.yaml#/JsonError
'429':
$ref: ../components/common.yaml#/JsonError
'502':
$ref: ../components/common.yaml#/JsonError
16 changes: 16 additions & 0 deletions openapi/paths/search.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,18 @@ Search:
- name: sortFilter
in: query
required: false
deprecated: true
description: Legacy single-filter parameter. Use `filter` for grouped selections.
schema: { type: string }
- name: filter
in: query
required: false
description: Repeat for each selected search filter.
style: form
explode: true
schema:
type: array
items: { type: string }
responses:
'200':
description: Search page.
Expand All @@ -46,6 +57,11 @@ SearchFilters:
in: query
required: true
schema: { type: integer, enum: [0, 3, 4, 5, 6] }
- name: contentFilter
in: query
required: false
description: Return filter groups applicable to this content type.
schema: { type: string }
responses:
'200':
description: Search filter capabilities.
Expand Down
16 changes: 16 additions & 0 deletions openapi/paths/streams.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ YoutubeSabrStreams:
$ref: ../components/streams.yaml#/StreamResponse
'400':
$ref: ../components/common.yaml#/JsonError
'401':
$ref: ../components/common.yaml#/JsonError
'403':
$ref: ../components/common.yaml#/JsonError
'422':
$ref: ../components/common.yaml#/JsonError
YoutubeSabrBootstrap:
Expand All @@ -42,6 +46,10 @@ YoutubeSabrBootstrap:
$ref: ../components/streams.yaml#/StreamResponse
'400':
$ref: ../components/common.yaml#/JsonError
'401':
$ref: ../components/common.yaml#/JsonError
'403':
$ref: ../components/common.yaml#/JsonError
'422':
$ref: ../components/common.yaml#/JsonError
NicoNicoStreams:
Expand All @@ -62,6 +70,10 @@ NicoNicoStreams:
$ref: ../components/streams.yaml#/StreamResponse
'400':
$ref: ../components/common.yaml#/JsonError
'401':
$ref: ../components/common.yaml#/JsonError
'403':
$ref: ../components/common.yaml#/JsonError
'422':
$ref: ../components/common.yaml#/JsonError
BiliBiliStreams:
Expand All @@ -82,6 +94,10 @@ BiliBiliStreams:
$ref: ../components/streams.yaml#/StreamResponse
'400':
$ref: ../components/common.yaml#/JsonError
'401':
$ref: ../components/common.yaml#/JsonError
'403':
$ref: ../components/common.yaml#/JsonError
'422':
$ref: ../components/common.yaml#/JsonError
AudioOnly:
Expand Down
62 changes: 62 additions & 0 deletions openapi/paths/subtitles.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
YoutubeSubtitle:
get:
tags: [playback]
summary: Retrieve a YouTube subtitle track
description: >-
Resolves a fresh timed-text track through PipePipeExtractor and returns bounded subtitle
content through the configured YouTube egress. VOD responses are cached by selection and
simultaneous identical requests are coalesced. Active and post-live tracks use a separate
short-lived server cache and are not browser-cacheable.
parameters:
- name: videoId
in: path
required: true
schema:
type: string
pattern: '^[A-Za-z0-9_-]{11}$'
- name: language
in: query
required: true
schema: { type: string, minLength: 1, maxLength: 64 }
- name: variant
in: query
required: true
schema: { type: string, enum: [manual, auto] }
- name: format
in: query
required: false
schema: { type: string, enum: [vtt, ttml], default: vtt }
- name: sourceLanguage
in: query
required: false
schema: { type: string, minLength: 1, maxLength: 64 }
- name: translation
in: query
required: false
schema: { type: string, minLength: 1, maxLength: 64 }
- name: name
in: query
required: false
description: Distinguishes multiple tracks with the same language and generation variant.
schema: { type: string, minLength: 1, maxLength: 128 }
responses:
'200':
description: Selected subtitle content.
headers:
X-Request-ID:
$ref: ../components/common.yaml#/RequestIdHeader
Cache-Control:
schema: { type: string }
content:
text/vtt:
schema: { type: string }
application/ttml+xml:
schema: { type: string }
'400':
$ref: ../components/common.yaml#/JsonError
'404':
$ref: ../components/common.yaml#/JsonError
'429':
$ref: ../components/common.yaml#/JsonError
'502':
$ref: ../components/common.yaml#/JsonError
5 changes: 4 additions & 1 deletion src/main/kotlin/dev/typetype/server/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import dev.typetype.server.db.DatabaseFactory
import dev.typetype.server.downloader.YoutubeProxySelector
import dev.typetype.server.services.ActiveSessionService
import dev.typetype.server.services.AuthService
import dev.typetype.server.services.AuthSessionConfig
import dev.typetype.server.services.AdminSettingsService
import dev.typetype.server.services.AvatarService
import dev.typetype.server.services.DownloaderGatewayService
Expand Down Expand Up @@ -36,7 +37,8 @@ fun Application.module() {
val dbPassword = System.getenv("DATABASE_PASSWORD") ?: "typetype"
DatabaseFactory.init(dbUrl, dbUser, dbPassword)
val jwtSecret = System.getenv("JWT_SECRET") ?: UUID.randomUUID().toString()
val authService = AuthService(jwtSecret)
val authSessionConfig = AuthSessionConfig.fromEnvironment()
val authService = AuthService(jwtSecret, sessionConfig = authSessionConfig)
val oidcAuthService = OidcAuthService(OidcConfigLoader.fromEnvironment(), jwtSecret, authService)
val userAdminService = UserAdminService()
val passwordResetService = PasswordResetService()
Expand Down Expand Up @@ -85,6 +87,7 @@ fun Application.module() {
installApplicationRoutes(
svc = svc,
authService = authService,
authSessionConfig = authSessionConfig,
adminSettingsService = adminSettingsService,
activeSessionService = activeSessionService,
downloaderGatewayService = downloaderGatewayService,
Expand Down
13 changes: 11 additions & 2 deletions src/main/kotlin/dev/typetype/server/ApplicationRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import dev.typetype.server.routes.youtubeRemoteBrowserRoutes
import dev.typetype.server.services.ActiveSessionService
import dev.typetype.server.services.AdminSettingsService
import dev.typetype.server.services.AuthService
import dev.typetype.server.services.AuthSessionConfig
import dev.typetype.server.services.AvatarService
import dev.typetype.server.services.DownloaderGatewayService
import dev.typetype.server.services.GitHubIssueService
Expand All @@ -46,6 +47,7 @@ import io.ktor.server.routing.routing
internal fun Application.installApplicationRoutes(
svc: ServiceRegistry,
authService: AuthService,
authSessionConfig: AuthSessionConfig,
adminSettingsService: AdminSettingsService,
activeSessionService: ActiveSessionService,
downloaderGatewayService: DownloaderGatewayService,
Expand Down Expand Up @@ -90,8 +92,15 @@ internal fun Application.installApplicationRoutes(
)
}
downloaderGatewayRoutes(downloaderGatewayService)
oidcAuthRoutes(oidcAuthService, adminSettingsService)
authRoutes(authService, passwordResetService, profileService, adminSettingsService, svc.homeRecommendationWarmupService)
oidcAuthRoutes(oidcAuthService, adminSettingsService, authSessionConfig)
authRoutes(
authService,
passwordResetService,
profileService,
adminSettingsService,
svc.homeRecommendationWarmupService,
authSessionConfig,
)
adminRoutes(authService, userAdminService, passwordResetService, adminSettingsService)
adminIdentityRoutes(svc.accountIdentityService, authService)
adminAllowListRoutes(authService, userAdminService, svc.adminManagedAccessService, svc.adminUserLookupService, svc.allowedChannelsService, svc.allowedPlaylistsService)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import dev.typetype.server.routes.proxyRoutes
import dev.typetype.server.routes.storyboardProxyRoutes
import dev.typetype.server.routes.streamRoutes
import dev.typetype.server.routes.withPlayableSabrStreams
import dev.typetype.server.routes.youtubeSubtitleRoutes
import dev.typetype.server.services.AdminSettingsService
import dev.typetype.server.services.AuthService
import io.ktor.server.plugins.ratelimit.rateLimit
Expand All @@ -27,6 +28,7 @@ internal fun Route.installStreamRoutes(
authService = authService,
accessControlService = svc.accessControlService,
adminSettingsService = adminSettingsService,
blockedService = svc.blockedService,
publicHlsManifestTokenService = svc.publicHlsManifestTokenService,
sabrStreamContractFilter = { url, data -> data.withPlayableSabrStreams(url, svc.sabrSessionStore) },
)
Expand Down Expand Up @@ -56,7 +58,8 @@ internal fun Route.installStreamRoutes(

internal fun Route.installProxyRoutes(svc: ServiceRegistry) {
rateLimit(PROXY_ZONE) {
proxyRoutes(svc.proxyService)
proxyRoutes(svc.proxyService, svc.youtubeSubtitleDeliveryService)
youtubeSubtitleRoutes(svc.youtubeSubtitleDeliveryService)
audioOnlySourceRoutes(
streamService = svc.streamService,
proxyService = svc.proxyService,
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/dev/typetype/server/CompressionConfig.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package dev.typetype.server

import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.compression.Compression
import io.ktor.server.plugins.compression.condition
import io.ktor.server.plugins.compression.excludeContentType
import io.ktor.server.plugins.compression.gzip

fun Application.configureCompression(): Unit {
install(Compression) {
gzip {
condition { _ -> response.status() != HttpStatusCode.TooManyRequests }
excludeContentType(ContentType.parse("application/vnd.apple.mpegurl"))
excludeContentType(ContentType.Image.Any)
excludeContentType(ContentType.Video.Any)
Expand Down
Loading