Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
e8ab41f
chore: prepare server 1.4.0
Priveetee Aug 6, 2026
022d159
fix: verify remote login capability
Priveetee Aug 7, 2026
c8558cc
fix: restrict public proxy destinations
Priveetee Aug 7, 2026
ba098b8
test: cover proxy destination validation
Priveetee Aug 7, 2026
e75f181
test: cover proxy redirect and dns attacks
Priveetee Aug 7, 2026
c646e1f
fix: update vulnerable jsoup dependency
Priveetee Aug 8, 2026
25eb032
chore: update server dependencies
Priveetee Aug 8, 2026
c32ad41
fix: require explicit local extractor path
Priveetee Aug 9, 2026
951bca9
fix: normalize YouTube channel tab URLs
Priveetee Aug 9, 2026
83f53a1
feat: add RSS feed data model
Priveetee Aug 9, 2026
a4fc41c
feat: add RSS feed persistence
Priveetee Aug 9, 2026
bae862e
feat: configure RSS instance policy
Priveetee Aug 9, 2026
a5d277c
feat: manage private RSS feeds
Priveetee Aug 9, 2026
c2d06fc
feat: render private RSS feeds
Priveetee Aug 9, 2026
8ec0fff
feat: expose private RSS feed API
Priveetee Aug 9, 2026
9ffa815
docs: document private RSS feed API
Priveetee Aug 9, 2026
33717e2
test: cover RSS feed management
Priveetee Aug 9, 2026
7a520f6
test: cover private RSS delivery
Priveetee Aug 9, 2026
50e4161
test: cover RSS policy and content filters
Priveetee Aug 9, 2026
abed280
feat: hide live streams from subscription feeds
Priveetee Aug 9, 2026
815aec5
fix: drop stale live videos from feeds
Priveetee Aug 10, 2026
d9750e8
chore: reconcile main into dev
Priveetee Aug 10, 2026
eb47a56
chore: align development version with 1.5.0
Priveetee Aug 11, 2026
d3a2081
fix: proxy marked artifact redirects
Priveetee Aug 12, 2026
9dbe199
fix: preserve scheduled live feed ordering
Priveetee Aug 12, 2026
1045b57
feat: hide members-only content
Priveetee Aug 12, 2026
44941e5
fix: prevent stale OIDC bootstrap status
Priveetee Aug 15, 2026
8261d90
feat: add notification popup preference
Priveetee Aug 15, 2026
2c5e0a4
feat: support account-bound SABR token pairs
Priveetee Aug 15, 2026
e73c00e
fix: use connected accounts for SABR playback
Priveetee Aug 15, 2026
0d1d89b
test: cover authenticated SABR info
Priveetee Aug 15, 2026
4d985f0
test: preserve authenticated SABR sources
Priveetee Aug 15, 2026
c781e21
fix: route gated playback through YouTube sessions
Priveetee Aug 15, 2026
f8d9eb3
test: cover gated YouTube playback access
Priveetee Aug 15, 2026
b086e61
docs: document YouTube session playback errors
Priveetee Aug 15, 2026
97afc71
fix: preserve authenticated YouTube player tokens
Priveetee Aug 15, 2026
eb2e206
fix: preserve selected YouTube account
Priveetee Aug 15, 2026
7c9c93b
feat: persist subscription group records
noreply Aug 17, 2026
7ef9808
feat: manage subscription group membership
noreply Aug 17, 2026
61b01ad
feat: expose subscription group API
noreply Aug 17, 2026
4612697
feat: filter subscription lists by group
noreply Aug 17, 2026
8665282
feat: keep group feed pagination stable
noreply Aug 17, 2026
860d686
feat: preserve subscription groups in backups
noreply Aug 17, 2026
f756bfa
docs: document subscription group endpoints
noreply Aug 17, 2026
8336b00
test: cover subscription group persistence
noreply Aug 17, 2026
939158a
test: cover subscription group routes
noreply Aug 17, 2026
af4cd5c
test: cover stable subscription group feeds
noreply Aug 17, 2026
abd1ae9
fix: bound feed membership snapshot storage
noreply Aug 17, 2026
93a0830
fix: prevent orphaned subscription memberships
noreply Aug 18, 2026
267fda9
fix: keep exported subscription backups restorable
noreply Aug 18, 2026
af9fb57
refactor: clarify subscription lock namespace
noreply Aug 18, 2026
4fc8081
fix: hide finished live streams from feeds
Priveetee Aug 21, 2026
ad2073d
fix: classify streams tab feed entries
Priveetee Aug 21, 2026
628dab4
fix: keep auth database work off request threads
Priveetee Aug 21, 2026
4be753c
fix: isolate database work from shared IO
Priveetee Aug 21, 2026
35776cc
fix: warm live playback behind edge
Priveetee Aug 21, 2026
37c44c9
fix: preserve contiguous live playback ranges
Priveetee Aug 21, 2026
dcf9cde
fix: recover live segments behind head
Priveetee Aug 21, 2026
967aab3
fix: recover lagging live audio segments
Priveetee Aug 21, 2026
6ee0171
fix: recover contiguous live media gaps
Priveetee Aug 21, 2026
949ec54
test: cover hidden live feed reconstruction
Priveetee Aug 21, 2026
b2ce460
fix: recover transitioning live playback
Priveetee Aug 21, 2026
18b2c33
fix: reuse authenticated SABR preparation
Priveetee Aug 21, 2026
17c2f55
chore: merge dev into subscription groups
Priveetee Aug 22, 2026
5a06bd9
fix: serialize subscription group mutations
Priveetee Aug 22, 2026
a2f7eef
fix: refresh legacy grouped feed snapshots
Priveetee Aug 22, 2026
b7eccb4
Merge pull request #76 from kapdon/feat/subscription-groups-server
Priveetee Aug 22, 2026
43b288b
chore: prepare server 1.6.0
Priveetee Aug 22, 2026
b233738
chore: reconcile main for 1.6.0
Priveetee Aug 22, 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 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.5.1
appVersion=1.6.0
systemProp.sun.net.client.defaultReadTimeout=180000
systemProp.sun.net.client.defaultConnectTimeout=60000
8 changes: 8 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ paths:
/playlist: { $ref: ./openapi/paths/playlists.yaml#/Playlist }
/saved-playlists: { $ref: ./openapi/paths/saved-playlists.yaml#/SavedPlaylists }
/saved-playlists/{id}: { $ref: ./openapi/paths/saved-playlists.yaml#/SavedPlaylist }
/subscriptions: { $ref: ./openapi/paths/subscriptions.yaml#/Subscriptions }
/subscriptions/groups: { $ref: ./openapi/paths/subscriptions.yaml#/SubscriptionGroups }
/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 }
/rss/feeds: { $ref: ./openapi/paths/rss.yaml#/RssFeeds }
/rss/feeds/{id}: { $ref: ./openapi/paths/rss.yaml#/RssFeed }
Expand Down Expand Up @@ -146,6 +150,10 @@ components:
$ref: ./openapi/components/media.yaml#/PublicPlaylistItem
SavedPlaylistItem: { $ref: ./openapi/components/media.yaml#/SavedPlaylistItem }
SavedPlaylistRequest: { $ref: ./openapi/components/media.yaml#/SavedPlaylistRequest }
SubscriptionItem: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionItem }
SubscriptionGroupItem: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionGroupItem }
SubscriptionGroupRequest: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionGroupRequest }
SubscriptionGroupMembershipRequest: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionGroupMembershipRequest }
SubscriptionFeedResponse: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionFeedResponse }
SubscriptionFeedPreparingResponse: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionFeedPreparingResponse }
RssFeedRequest: { $ref: ./openapi/components/rss.yaml#/RssFeedRequest }
Expand Down
2 changes: 2 additions & 0 deletions openapi/components/access-control.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ SettingsItem:
autoplay: { type: boolean, default: true }
volume: { type: number, format: double, default: 1.0 }
muted: { type: boolean, default: false }
notificationPopupsEnabled: { type: boolean, default: true }
subtitlesEnabled: { type: boolean, default: false }
defaultSubtitleLanguage: { type: string, default: "" }
defaultAudioLanguage: { type: string, default: "" }
Expand All @@ -34,6 +35,7 @@ SettingsItem:
hideComments: { type: boolean, default: false }
hideShorts: { type: boolean, default: false }
hideSubscriptionLiveStreams: { type: boolean, default: false }
hideMembersOnlyContent: { type: boolean, default: false }
accessMode:
type: string
enum: [unrestricted, allow_list]
Expand Down
34 changes: 34 additions & 0 deletions openapi/components/subscriptions.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
SubscriptionItem:
type: object
required: [channelUrl, name, avatarUrl, subscribedAt]
properties:
channelUrl: { type: string, minLength: 1 }
name: { type: string }
avatarUrl: { type: string }
subscribedAt: { type: integer, format: int64 }
SubscriptionCreateRequest:
type: object
required: [channelUrl, name, avatarUrl]
properties:
channelUrl: { type: string, minLength: 1 }
name: { type: string }
avatarUrl: { type: string }
SubscriptionGroupItem:
type: object
required: [id, name, channelCount, createdAt, updatedAt]
properties:
id: { type: string, format: uuid }
name: { type: string, minLength: 1, maxLength: 100 }
channelCount: { type: integer, minimum: 0 }
createdAt: { type: integer, format: int64 }
updatedAt: { type: integer, format: int64 }
SubscriptionGroupRequest:
type: object
required: [name]
properties:
name: { type: string, minLength: 1, maxLength: 100 }
SubscriptionGroupMembershipRequest:
type: object
required: [channelUrl]
properties:
channelUrl: { type: string, minLength: 1 }
SubscriptionFeedResponse:
type: object
required: [videos, nextpage, generation, generatedAt, refreshing]
Expand Down
12 changes: 12 additions & 0 deletions openapi/components/user-backup.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ TypeTypeContentFiltersBackup:
allowedPlaylists:
type: array
items: { $ref: ./access-control.yaml#/AllowedPlaylistItem }
SubscriptionGroupBackupItem:
type: object
required: [name, channelUrls, createdAt, updatedAt]
properties:
name: { type: string, minLength: 1, maxLength: 100 }
channelUrls:
type: array
uniqueItems: true
items: { type: string, minLength: 1 }
createdAt: { type: integer, format: int64 }
updatedAt: { type: integer, format: int64 }
TypeTypeBackupItem:
type: object
required: [format, version, exportedAt, categories]
Expand All @@ -47,6 +58,7 @@ TypeTypeBackupItem:
- settings
- contentFilters
subscriptions: { type: array, nullable: true, items: { type: object, additionalProperties: true } }
subscriptionGroups: { type: array, nullable: true, items: { $ref: '#/SubscriptionGroupBackupItem' } }
history: { type: array, nullable: true, items: { type: object, additionalProperties: true } }
playlists: { type: array, nullable: true, items: { type: object, additionalProperties: true } }
watchLater: { type: array, nullable: true, items: { type: object, additionalProperties: true } }
Expand Down
3 changes: 2 additions & 1 deletion openapi/components/youtube-session.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ YoutubeRemoteBrowserStartResponse:
expiresAt: { type: integer, format: int64 }
YoutubeRemoteBrowserCompleteRequest:
type: object
required: [sessionId, tokenSessionId, status, cookies, poToken, capturedAt]
required: [sessionId, tokenSessionId, status, cookies, poToken, authUser, capturedAt]
properties:
sessionId: { type: string }
tokenSessionId: { type: string }
Expand All @@ -23,4 +23,5 @@ YoutubeRemoteBrowserCompleteRequest:
enum: [completed]
cookies: { type: string }
poToken: { type: string }
authUser: { type: integer, minimum: 0, maximum: 99 }
capturedAt: { type: integer, format: int64 }
12 changes: 10 additions & 2 deletions openapi/paths/streams.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ YoutubeSabrStreams:
schema:
$ref: ../components/streams.yaml#/StreamResponse
'400':
$ref: ../components/common.yaml#/JsonError
description: Invalid request or YouTube account connection required (`youtube_session_required`).
content:
application/json:
schema:
$ref: ../components/common.yaml#/ErrorResponse
'401':
$ref: ../components/common.yaml#/JsonError
'403':
Expand All @@ -45,7 +49,11 @@ YoutubeSabrBootstrap:
schema:
$ref: ../components/streams.yaml#/StreamResponse
'400':
$ref: ../components/common.yaml#/JsonError
description: Invalid request or YouTube account connection required (`youtube_session_required`).
content:
application/json:
schema:
$ref: ../components/common.yaml#/ErrorResponse
'401':
$ref: ../components/common.yaml#/JsonError
'403':
Expand Down
166 changes: 165 additions & 1 deletion openapi/paths/subscriptions.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,161 @@
Subscriptions:
get:
tags: [user-data]
summary: List the current user's subscriptions
description: Omit both filters for the global list. Use groupId for one named group or ungrouped=true for subscriptions in no groups.
parameters:
- name: groupId
in: query
required: false
schema: { type: string, format: uuid }
- name: ungrouped
in: query
required: false
schema: { type: boolean, default: false }
responses:
'200':
description: The selected subscription projection.
content:
application/json:
schema:
type: array
items: { $ref: ../components/subscriptions.yaml#/SubscriptionItem }
'400': { $ref: ../components/common.yaml#/JsonError }
'401': { $ref: ../components/common.yaml#/JsonError }
'404': { $ref: ../components/common.yaml#/JsonError }
post:
tags: [user-data]
summary: Subscribe to a channel
requestBody:
required: true
content:
application/json:
schema: { $ref: ../components/subscriptions.yaml#/SubscriptionCreateRequest }
responses:
'201':
description: Subscription created.
content:
application/json:
schema: { $ref: ../components/subscriptions.yaml#/SubscriptionItem }
'400': { $ref: ../components/common.yaml#/JsonError }
'401': { $ref: ../components/common.yaml#/JsonError }
delete:
tags: [user-data]
summary: Unsubscribe from a channel
parameters:
- name: url
in: query
required: true
schema: { type: string, minLength: 1 }
responses:
'204': { description: Subscription deleted. }
'400': { $ref: ../components/common.yaml#/JsonError }
'401': { $ref: ../components/common.yaml#/JsonError }
'404': { $ref: ../components/common.yaml#/JsonError }
SubscriptionGroups:
get:
tags: [user-data]
summary: List the current user's subscription groups
responses:
'200':
description: Account-scoped named groups.
content:
application/json:
schema:
type: array
items: { $ref: ../components/subscriptions.yaml#/SubscriptionGroupItem }
'401': { $ref: ../components/common.yaml#/JsonError }
post:
tags: [user-data]
summary: Create a subscription group
requestBody:
required: true
content:
application/json:
schema: { $ref: ../components/subscriptions.yaml#/SubscriptionGroupRequest }
responses:
'201':
description: Group created.
content:
application/json:
schema: { $ref: ../components/subscriptions.yaml#/SubscriptionGroupItem }
'400': { $ref: ../components/common.yaml#/JsonError }
'401': { $ref: ../components/common.yaml#/JsonError }
'409': { $ref: ../components/common.yaml#/JsonError }
SubscriptionGroup:
parameters:
- name: groupId
in: path
required: true
schema: { type: string, format: uuid }
put:
tags: [user-data]
summary: Rename a subscription group
requestBody:
required: true
content:
application/json:
schema: { $ref: ../components/subscriptions.yaml#/SubscriptionGroupRequest }
responses:
'204': { description: Group renamed. }
'400': { $ref: ../components/common.yaml#/JsonError }
'401': { $ref: ../components/common.yaml#/JsonError }
'404': { $ref: ../components/common.yaml#/JsonError }
'409': { $ref: ../components/common.yaml#/JsonError }
delete:
tags: [user-data]
summary: Delete a subscription group
responses:
'204': { description: Group and its memberships deleted. }
'401': { $ref: ../components/common.yaml#/JsonError }
'404': { $ref: ../components/common.yaml#/JsonError }
SubscriptionGroupChannels:
parameters:
- name: groupId
in: path
required: true
schema: { type: string, format: uuid }
put:
tags: [user-data]
summary: Add a subscribed channel to a group
requestBody:
required: true
content:
application/json:
schema: { $ref: ../components/subscriptions.yaml#/SubscriptionGroupMembershipRequest }
responses:
'204': { description: Membership exists. }
'400': { $ref: ../components/common.yaml#/JsonError }
'401': { $ref: ../components/common.yaml#/JsonError }
'404': { $ref: ../components/common.yaml#/JsonError }
delete:
tags: [user-data]
summary: Remove a subscribed channel from a group
parameters:
- name: url
in: query
required: true
schema: { type: string, minLength: 1 }
responses:
'204': { description: Membership deleted. }
'400': { $ref: ../components/common.yaml#/JsonError }
'401': { $ref: ../components/common.yaml#/JsonError }
'404': { $ref: ../components/common.yaml#/JsonError }
SubscriptionFeed:
get:
tags: [user-data]
summary: Read a stable page from the current user's subscription feed snapshot
parameters:
- name: groupId
in: query
required: false
description: Restrict the snapshot projection to subscriptions in one account-owned group.
schema: { type: string, format: uuid }
- name: ungrouped
in: query
required: false
description: Restrict the snapshot projection to subscriptions in no groups.
schema: { type: boolean, default: false }
- name: page
in: query
required: false
Expand All @@ -16,7 +169,7 @@ SubscriptionFeed:
- name: cursor
in: query
required: false
description: Opaque continuation returned in nextpage.
description: Opaque continuation returned in nextpage and bound to the selected membership snapshot.
schema: { type: string }
responses:
'200':
Expand Down Expand Up @@ -45,6 +198,8 @@ SubscriptionFeed:
$ref: ../components/common.yaml#/JsonError
'401':
$ref: ../components/common.yaml#/JsonError
'404':
$ref: ../components/common.yaml#/JsonError
'409':
description: The cursor references a snapshot generation that is no longer retained.
headers:
Expand All @@ -54,3 +209,12 @@ SubscriptionFeed:
application/json:
schema:
$ref: ../components/common.yaml#/ErrorResponse
'429':
description: The account already has the maximum number of active filtered cursor sessions.
headers:
X-Request-ID:
$ref: ../components/common.yaml#/RequestIdHeader
content:
application/json:
schema:
$ref: ../components/common.yaml#/ErrorResponse
3 changes: 2 additions & 1 deletion openapi/paths/youtube-session.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ InternalBrowserComplete:
application/json:
schema:
type: object
required: [sessionId, tokenSessionId, status, cookies, poToken, capturedAt]
required: [sessionId, tokenSessionId, status, cookies, poToken, authUser, capturedAt]
properties:
sessionId: { type: string }
tokenSessionId: { type: string }
Expand All @@ -78,6 +78,7 @@ InternalBrowserComplete:
enum: [completed]
cookies: { type: string }
poToken: { type: string }
authUser: { type: integer, minimum: 0, maximum: 99 }
capturedAt: { type: integer, format: int64 }
responses:
'204': { description: YouTube credentials stored. }
Expand Down
4 changes: 4 additions & 0 deletions src/main/kotlin/dev/typetype/server/ApplicationRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ internal fun Application.installApplicationRoutes(
svc.accessControlService,
adminSettingsService,
svc.audioOnlyMediaTokenService,
svc.authenticatedSabrInfoService,
svc.youtubeSessionSabrStreamService?.let { service ->
{ userId, url -> service.getStreamInfo(userId, url) }
},
)
}
downloaderGatewayRoutes(downloaderGatewayService)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ internal fun Route.installStreamRoutes(
blockedService = svc.blockedService,
publicHlsManifestTokenService = svc.publicHlsManifestTokenService,
sabrStreamContractFilter = { url, data -> data.withPlayableSabrStreams(url, svc.sabrSessionStore) },
youtubeSessionSabrStreamInfo = svc.youtubeSessionSabrStreamService?.let { service ->
{ userId, url -> service.getStreamInfo(userId, url) }
},
)
audioOnlyContractRoutes(
streamService = svc.streamService,
Expand Down
Loading