Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
9f1dbbc
ci: route workflows to R730 runners
Priveetee Aug 31, 2026
f74bf02
chore: benchmark dev runners
Priveetee Aug 31, 2026
f604dad
feat: add batch playback progress lookup
Priveetee Sep 4, 2026
bdd5f04
refactor: add TypeType SABR boundary models
Priveetee Sep 4, 2026
02c3621
refactor: add TypeType SABR session adapter
Priveetee Sep 4, 2026
41a16fd
refactor: route Server SABR code through adapter
Priveetee Sep 4, 2026
df345ca
test: migrate SABR fixtures to TypeType boundary
Priveetee Sep 4, 2026
915991c
test: cover Pipe boundary doubles in SABR isolation
Priveetee Sep 4, 2026
3a60872
test: enforce SABR adapter boundary
Priveetee Sep 4, 2026
9f570c7
fix: return a typed error for oversized portability uploads
Priveetee Sep 6, 2026
d797660
test: add opt-in Takeout archive upload reproduction
Priveetee Sep 6, 2026
484ecad
feat: add RSS video thumbnails
Priveetee Sep 7, 2026
11f8450
feat: add account profile persistence
Priveetee Sep 8, 2026
bf6a6ee
feat: add isolated profile lifecycle
Priveetee Sep 8, 2026
9a8f06b
feat: expose account profile routes
Priveetee Sep 8, 2026
e70df63
feat: bind authentication to active profile
Priveetee Sep 8, 2026
d599514
feat: add profile scoped service notifications
Priveetee Sep 8, 2026
9fd2efd
chore: bump server version to 1.8.1
Priveetee Sep 8, 2026
9162c3c
feat: add UnifiedPush channel notifications
Priveetee Sep 8, 2026
f85957e
fix: harden profiles and notification state
Priveetee Sep 8, 2026
407f87a
fix: retain notifications during feed refresh
Priveetee Sep 8, 2026
2a8078a
perf: expire warmup tracking and deduplicate concurrent scheduling
Priveetee Sep 8, 2026
f73f1b1
perf: skip cache expiry scans before the next deadline
Priveetee Sep 9, 2026
3b8816a
perf: stream BiliBili ranges with bounded retry memory
Priveetee Sep 9, 2026
78a796b
test: verify bounded range streaming and safe retries
Priveetee Sep 9, 2026
db9f577
chore: update Exposed and SQLite JDBC
Priveetee Sep 9, 2026
76168cc
perf: bound Takeout import memory caches
Priveetee Sep 9, 2026
f3a274c
perf: bound auxiliary server memory caches
Priveetee Sep 9, 2026
898f6df
perf: close shared resources on shutdown
Priveetee Sep 9, 2026
7439e16
perf: skip unnecessary BiliBili related lookups
Priveetee Sep 9, 2026
659786d
perf: preserve NicoNico segment caching
Priveetee Sep 9, 2026
6f90ecc
chore: update Kotlin and JWT dependencies
Priveetee Sep 9, 2026
2325514
perf: stop recommendation scopes on shutdown
Priveetee Sep 9, 2026
e55eda7
perf: stop feed and takeout jobs on shutdown
Priveetee Sep 9, 2026
8cb9c87
fix: preserve injected feed scope ownership
Priveetee Sep 9, 2026
367fea3
perf: cache BiliBili related lookups
Priveetee Sep 9, 2026
501a31a
fix: hide provider media behind expiring handles
Priveetee Sep 9, 2026
d0e736b
fix: rewrite provider manifests to local handles
Priveetee Sep 9, 2026
ff4f52b
test: cover provider media transport
Priveetee Sep 9, 2026
b65be35
fix: preserve stream request cancellation
Priveetee Sep 9, 2026
811e44f
perf: parallelize provider media handle mapping
Priveetee Sep 9, 2026
15dc17e
test: cover concurrent provider media mapping
Priveetee Sep 9, 2026
5eecb9d
perf: reuse BiliBili range connections
Priveetee Sep 9, 2026
0d0e7c5
fix: isolate recommendations by service
Priveetee Sep 10, 2026
fe782d8
chore: update PipePipeExtractor
Priveetee Sep 10, 2026
9ca9376
chore: sync release with main
Priveetee Sep 10, 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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ YOUTUBE_REMOTE_LOGIN_INTERNAL_TOKEN=replace-with-shared-internal-token
YOUTUBE_REMOTE_LOGIN_TTL_MS=480000
YOUTUBE_REMOTE_LOGIN_MAX_SESSIONS=2

TYPE_TYPE_INSTANCE_ID=typetype
TYPE_TYPE_PUSH_NOTIFICATIONS_ENABLED=true
TYPE_TYPE_PUSH_NOTIFICATIONS_INTERVAL_SECONDS=300

ALLOWED_ORIGINS=http://localhost:5173
16 changes: 8 additions & 8 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import java.time.Instant

plugins {
kotlin("jvm") version "2.4.10"
kotlin("plugin.serialization") version "2.4.10"
kotlin("jvm") version "2.4.20"
kotlin("plugin.serialization") version "2.4.20"
id("io.ktor.plugin") version "3.5.2"
id("jacoco")
}
Expand All @@ -29,7 +29,7 @@ dependencies {
implementation("com.fasterxml.jackson.core:jackson-core")
implementation(platform("io.netty:netty-bom:4.2.17.Final"))
constraints {
implementation("org.jsoup:jsoup:1.23.1") {
implementation("org.jsoup:jsoup:1.23.2") {
because("CVE-2026-71497 affects PipePipeExtractor's transitive jsoup version")
}
}
Expand All @@ -44,18 +44,18 @@ 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.3")
implementation("com.github.Priveetee.PipePipeExtractor:extractor:ca3280f28f3aa0b980b63a2b2d23c362f7616620")
implementation("com.github.Priveetee.PipePipeExtractor:extractor:a395a9ba16ae75987969ed9e7d330c928ad3bc20")
compileOnly("com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751")
implementation("org.json:json:20260814")
implementation("com.squareup.okhttp3:okhttp:5.5.0")
implementation("io.lettuce:lettuce-core:7.7.0.RELEASE")
implementation("org.jetbrains.exposed:exposed-core:1.4.0")
implementation("org.jetbrains.exposed:exposed-jdbc:1.4.0")
implementation("org.jetbrains.exposed:exposed-core:1.5.0")
implementation("org.jetbrains.exposed:exposed-jdbc:1.5.0")
implementation("com.zaxxer:HikariCP:7.1.0")
implementation("org.postgresql:postgresql:42.7.13")
implementation("org.xerial:sqlite-jdbc:3.53.2.1")
implementation("org.xerial:sqlite-jdbc:3.53.4.0")
implementation("com.password4j:password4j:1.8.4")
implementation("com.auth0:java-jwt:4.6.0")
implementation("com.auth0:java-jwt:4.6.1")
testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("io.mockk:mockk:1.14.11")
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.8.0
appVersion=1.8.1
systemProp.sun.net.client.defaultReadTimeout=180000
systemProp.sun.net.client.defaultConnectTimeout=60000
31 changes: 31 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ paths:
/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 }
/media/{handle}: { $ref: ./openapi/paths/media.yaml#/ProviderMedia }
/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 All @@ -50,6 +51,17 @@ paths:
/subscriptions/groups/{groupId}: { $ref: ./openapi/paths/subscriptions.yaml#/SubscriptionGroup }
/subscriptions/groups/{groupId}/channels: { $ref: ./openapi/paths/subscriptions.yaml#/SubscriptionGroupChannels }
/subscriptions/feed: { $ref: ./openapi/paths/subscriptions.yaml#/SubscriptionFeed }
/profiles: { $ref: ./openapi/paths/profiles.yaml#/AccountProfiles }
/profiles/{profileId}: { $ref: ./openapi/paths/profiles.yaml#/AccountProfile }
/profiles/{profileId}/default: { $ref: ./openapi/paths/profiles.yaml#/AccountProfileDefault }
/profiles/{profileId}/switch: { $ref: ./openapi/paths/profiles.yaml#/AccountProfileSwitch }
/notifications: { $ref: ./openapi/paths/notifications.yaml#/Notifications }
/notifications/unread-count: { $ref: ./openapi/paths/notifications.yaml#/NotificationsUnreadCount }
/notifications/read-all: { $ref: ./openapi/paths/notifications.yaml#/NotificationsReadAll }
/notifications/{notificationId}/read: { $ref: ./openapi/paths/notifications.yaml#/NotificationsRead }
/notifications/channel-preferences: { $ref: ./openapi/paths/push-notifications.yaml#/ChannelNotificationPreferences }
/notifications/push/devices: { $ref: ./openapi/paths/push-notifications.yaml#/PushDevices }
/notifications/push/devices/{deviceId}: { $ref: ./openapi/paths/push-notifications.yaml#/PushDevice }
/rss/feeds: { $ref: ./openapi/paths/rss.yaml#/RssFeeds }
/rss/feeds/{id}: { $ref: ./openapi/paths/rss.yaml#/RssFeed }
/rss/feeds/{id}/enabled: { $ref: ./openapi/paths/rss.yaml#/RssFeedEnabled }
Expand Down Expand Up @@ -114,6 +126,11 @@ paths:
/internal/youtube-session/browser/complete:
$ref: ./openapi/paths/youtube-session.yaml#/InternalBrowserComplete
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
ErrorResponse:
$ref: ./openapi/components/common.yaml#/ErrorResponse
Expand Down Expand Up @@ -169,6 +186,20 @@ components:
SubscriptionGroupMembershipBatchRequest: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionGroupMembershipBatchRequest }
SubscriptionFeedResponse: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionFeedResponse }
SubscriptionFeedPreparingResponse: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionFeedPreparingResponse }
AccountProfileItem: { $ref: ./openapi/components/profiles.yaml#/AccountProfileItem }
AccountProfilesResponse: { $ref: ./openapi/components/profiles.yaml#/AccountProfilesResponse }
ProfileNameRequest: { $ref: ./openapi/components/profiles.yaml#/ProfileNameRequest }
ProfileSwitchResponse: { $ref: ./openapi/components/profiles.yaml#/ProfileSwitchResponse }
NotificationItem: { $ref: ./openapi/components/notifications.yaml#/NotificationItem }
NotificationsResponse: { $ref: ./openapi/components/notifications.yaml#/NotificationsResponse }
UnreadCountResponse: { $ref: ./openapi/components/notifications.yaml#/UnreadCountResponse }
MarkNotificationsReadResponse: { $ref: ./openapi/components/notifications.yaml#/MarkNotificationsReadResponse }
PushNotificationCapability: { $ref: ./openapi/components/instance.yaml#/PushNotificationCapability }
PushDeviceRegistrationRequest: { $ref: ./openapi/components/push-notifications.yaml#/PushDeviceRegistrationRequest }
PushDeviceRegistrationResponse: { $ref: ./openapi/components/push-notifications.yaml#/PushDeviceRegistrationResponse }
ChannelNotificationPreferenceRequest: { $ref: ./openapi/components/push-notifications.yaml#/ChannelNotificationPreferenceRequest }
ChannelNotificationPreference: { $ref: ./openapi/components/push-notifications.yaml#/ChannelNotificationPreference }
UnifiedPushNotificationPayload: { $ref: ./openapi/components/push-notifications.yaml#/UnifiedPushNotificationPayload }
RssFeedRequest: { $ref: ./openapi/components/rss.yaml#/RssFeedRequest }
RssFeedItem: { $ref: ./openapi/components/rss.yaml#/RssFeedItem }
RssFeedSecretItem: { $ref: ./openapi/components/rss.yaml#/RssFeedSecretItem }
Expand Down
14 changes: 14 additions & 0 deletions openapi/components/instance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ InstanceResponse:
- youtubeRemoteLoginReady
- parentalControlsEnabled
- rss
- pushNotifications
properties:
name: { type: string, example: TypeType }
tagline: { type: string, nullable: true }
Expand Down Expand Up @@ -56,6 +57,8 @@ InstanceResponse:
description: True when the instance-wide allow-list policy is enabled.
rss:
$ref: '#/RssInstanceCapability'
pushNotifications:
$ref: '#/PushNotificationCapability'
RssInstanceCapability:
type: object
required: [enabled, maxFeedsPerUser, maxItems, minimumPollMinutes, rateLimitPerMinute]
Expand All @@ -65,3 +68,14 @@ RssInstanceCapability:
maxItems: { type: integer }
minimumPollMinutes: { type: integer }
rateLimitPerMinute: { type: integer }

PushNotificationCapability:
type: object
required: [enabled, provider, eventTypes, maxDevicesPerAccount]
properties:
enabled: { type: boolean }
provider: { type: string, enum: [unifiedpush] }
eventTypes:
type: array
items: { type: string }
maxDevicesPerAccount: { type: integer, minimum: 0 }
39 changes: 39 additions & 0 deletions openapi/components/notifications.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
NotificationItem:
type: object
required: [id, type, title, createdAt, publishedAt, channelUrl, channelName, channelAvatarUrl, serviceId, serviceName, read, video]
properties:
id: { type: string }
type: { type: string }
title: { type: string }
createdAt: { type: integer, format: int64 }
publishedAt: { type: integer, format: int64 }
channelUrl: { type: string }
channelName: { type: string }
channelAvatarUrl: { type: string }
serviceId: { type: integer, format: int32, minimum: 0 }
serviceName: { type: string }
read: { type: boolean }
video: { $ref: ./media.yaml#/VideoItem }
NotificationsResponse:
type: object
required: [items, unreadCount, nextpage, available]
properties:
items:
type: array
items: { $ref: '#/NotificationItem' }
unreadCount: { type: integer, minimum: 0 }
nextpage: { type: string, nullable: true }
available: { type: boolean }
UnreadCountResponse:
type: object
required: [unreadCount, available]
properties:
unreadCount: { type: integer, minimum: 0 }
available: { type: boolean }
MarkNotificationsReadResponse:
type: object
required: [readAt, unreadCount, available]
properties:
readAt: { type: integer, format: int64 }
unreadCount: { type: integer, minimum: 0 }
available: { type: boolean }
33 changes: 33 additions & 0 deletions openapi/components/profiles.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
AccountProfileItem:
type: object
required: [id, name, isActive, isDefault, lastUsedAt]
properties:
id: { type: string, format: uuid }
name: { type: string, minLength: 1, maxLength: 40 }
isActive: { type: boolean }
isDefault: { type: boolean }
lastUsedAt: { type: integer, format: int64 }
publicUsername: { type: string, nullable: true }
avatarUrl: { type: string, nullable: true }
avatarType: { type: string, nullable: true }
avatarCode: { type: string, nullable: true }
AccountProfilesResponse:
type: object
required: [profiles, activeProfileId, defaultProfileId]
properties:
profiles:
type: array
items: { $ref: '#/AccountProfileItem' }
activeProfileId: { type: string, format: uuid }
defaultProfileId: { type: string, format: uuid }
ProfileNameRequest:
type: object
required: [name]
properties:
name: { type: string, minLength: 1, maxLength: 40 }
ProfileSwitchResponse:
type: object
required: [accessToken, profile]
properties:
accessToken: { type: string }
profile: { $ref: '#/AccountProfileItem' }
62 changes: 62 additions & 0 deletions openapi/components/push-notifications.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
PushDeviceRegistrationRequest:
type: object
required: [deviceId, endpoint]
properties:
deviceId: { type: string, minLength: 1, maxLength: 128 }
platform: { type: string, enum: [android], default: android }
endpoint: { type: string, format: uri, minLength: 1, maxLength: 2048 }
expiresAt: { type: integer, format: int64, nullable: true }
PushDeviceRegistrationResponse:
type: object
required: [id, deviceId, platform, expiresAt, updatedAt]
properties:
id: { type: string }
deviceId: { type: string }
platform: { type: string, enum: [android] }
expiresAt: { type: integer, format: int64, nullable: true }
updatedAt: { type: integer, format: int64 }
ChannelNotificationPreferenceRequest:
type: object
required: [channelUrl, enabled]
properties:
channelUrl: { type: string, minLength: 1 }
enabled: { type: boolean }
ChannelNotificationPreference:
type: object
required: [channelUrl, enabled, updatedAt]
properties:
channelUrl: { type: string }
enabled: { type: boolean }
updatedAt: { type: integer, format: int64 }
UnifiedPushNotificationPayload:
type: object
required:
- version
- eventType
- serviceId
- serviceName
- eventId
- videoId
- videoUrl
- channelId
- channelName
- channelAvatarUrl
- instanceId
- accountId
- publishedAt
- title
properties:
version: { type: integer, minimum: 1 }
eventType: { type: string, enum: [subscription_new_video] }
serviceId: { type: integer, minimum: 0 }
serviceName: { type: string }
eventId: { type: string }
videoId: { type: string }
videoUrl: { type: string }
channelId: { type: string }
channelName: { type: string }
channelAvatarUrl: { type: string }
instanceId: { type: string }
accountId: { type: string }
publishedAt: { type: integer, format: int64 }
title: { type: string }
59 changes: 59 additions & 0 deletions openapi/paths/media.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
ProviderMedia:
get:
tags: [playback]
summary: Retrieve provider media through an opaque temporary handle
description: >-
Streams BiliBili or NicoNico media referenced by a handle returned from a provider stream
endpoint. The signed provider URL and NicoNico cookie remain server-side; clients must not
construct or proxy the provider URL themselves. Handles expire with the upstream media
signature and support byte ranges for progressive and segmented playback.
parameters:
- name: handle
in: path
required: true
schema:
type: string
pattern: '^m1_[A-Za-z0-9_-]{24}$'
- name: Range
in: header
required: false
schema: { type: string }
responses:
'200':
description: Complete provider media response.
headers:
X-Request-ID:
$ref: ../components/common.yaml#/RequestIdHeader
content:
application/octet-stream:
schema: { type: string, format: binary }
video/mp4:
schema: { type: string, format: binary }
audio/mp4:
schema: { type: string, format: binary }
application/vnd.apple.mpegurl:
schema: { type: string }
'206':
description: Partial provider media 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 }
video/mp4:
schema: { type: string, format: binary }
audio/mp4:
schema: { type: string, format: binary }
'404':
$ref: ../components/common.yaml#/JsonError
'422':
$ref: ../components/common.yaml#/JsonError
'429':
$ref: ../components/common.yaml#/JsonError
'503':
$ref: ../components/common.yaml#/JsonError
Loading