Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
0d26e09
chore: prepare server 1.5.0
Priveetee Aug 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
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ PipePipe Client and PipePipeExtractor are the behavioral references for extracti

When a defect is general to PipePipeExtractor, prefer contributing the correction upstream. Keep TypeType-specific behavior in this repository only when it belongs to the TypeType API or when the upstream API cannot express the required backend behavior cleanly.

Builds use the PipePipeExtractor revision pinned in `build.gradle.kts`. To deliberately test a local checkout instead, pass its path explicitly:

```sh
./gradlew -PpipePipeExtractorPath=../PipePipeExtractor test
```

## Programming preferences

- Prefer clear names and structure over explanatory comments, but comments are welcome whenever a contributor finds them useful.
Expand Down
13 changes: 9 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import java.time.Instant
plugins {
kotlin("jvm") version "2.4.10"
kotlin("plugin.serialization") version "2.4.10"
id("io.ktor.plugin") version "3.5.1"
id("io.ktor.plugin") version "3.5.2"
id("jacoco")
}

Expand All @@ -27,6 +27,11 @@ repositories {
dependencies {
implementation(platform("com.fasterxml.jackson:jackson-bom:2.22.1"))
implementation(platform("io.netty:netty-bom:4.2.16.Final"))
constraints {
implementation("org.jsoup:jsoup:1.23.1") {
because("CVE-2026-71497 affects PipePipeExtractor's transitive jsoup version")
}
}
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.ktor:ktor-server-content-negotiation-jvm")
Expand All @@ -43,14 +48,14 @@ dependencies {
implementation("org.json:json:20260719")
implementation("com.squareup.okhttp3:okhttp:5.4.0")
implementation("io.lettuce:lettuce-core:7.6.0.RELEASE")
implementation("org.jetbrains.exposed:exposed-core:1.3.1")
implementation("org.jetbrains.exposed:exposed-jdbc:1.3.1")
implementation("org.jetbrains.exposed:exposed-core:1.4.0")
implementation("org.jetbrains.exposed:exposed-jdbc:1.4.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("com.password4j:password4j:1.8.4")
implementation("com.auth0:java-jwt:4.6.0")
testImplementation("org.junit.jupiter:junit-jupiter:6.1.2")
testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("io.mockk:mockk:1.14.11")
testImplementation("io.ktor:ktor-server-test-host-jvm")
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.4.0
appVersion=1.5.0
systemProp.sun.net.client.defaultReadTimeout=180000
systemProp.sun.net.client.defaultConnectTimeout=60000
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
Expand Down
14 changes: 14 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ tags:
- name: downloader
- name: youtube-session
- name: user-data
- name: rss
paths:
/health: { $ref: ./openapi/paths/health.yaml#/Health }
/instance: { $ref: ./openapi/paths/metadata.yaml#/Instance }
Expand Down Expand Up @@ -43,6 +44,11 @@ paths:
/saved-playlists: { $ref: ./openapi/paths/saved-playlists.yaml#/SavedPlaylists }
/saved-playlists/{id}: { $ref: ./openapi/paths/saved-playlists.yaml#/SavedPlaylist }
/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 }
/rss/feeds/{id}/enabled: { $ref: ./openapi/paths/rss.yaml#/RssFeedEnabled }
/rss/feeds/{id}/regenerate: { $ref: ./openapi/paths/rss.yaml#/RssFeedRegenerate }
/rss/feeds/{id}.xml: { $ref: ./openapi/paths/rss.yaml#/RssFeedDocument }
/settings: { $ref: ./openapi/paths/access-control.yaml#/Settings }
/backup/typetype: { $ref: ./openapi/paths/user-backup.yaml#/TypeTypeBackup }
/restore/typetype: { $ref: ./openapi/paths/user-backup.yaml#/TypeTypeRestore }
Expand All @@ -51,6 +57,10 @@ paths:
/allowed/channels: { $ref: ./openapi/paths/access-control.yaml#/AllowedChannels }
/allowed/channels/{channelUrl}: { $ref: ./openapi/paths/access-control.yaml#/AllowedChannel }
/admin/settings: { $ref: ./openapi/paths/access-control.yaml#/AdminSettings }
/admin/rss/feeds: { $ref: ./openapi/paths/rss-admin.yaml#/AdminRssFeeds }
/admin/rss/feeds/{id}/enabled: { $ref: ./openapi/paths/rss-admin.yaml#/AdminRssFeedEnabled }
/admin/rss/feeds/{id}: { $ref: ./openapi/paths/rss-admin.yaml#/AdminRssFeed }
/admin/rss/users/{id}/enabled: { $ref: ./openapi/paths/rss-admin.yaml#/AdminRssUserEnabled }
/admin/users: { $ref: ./openapi/paths/admin-users.yaml#/AdminUsers }
/admin/users/{id}/access-mode: { $ref: ./openapi/paths/admin-users.yaml#/AdminUserAccessMode }
/admin/users/managed-access: { $ref: ./openapi/paths/admin-managed-access.yaml#/AdminManagedAccessUsers }
Expand Down Expand Up @@ -138,6 +148,10 @@ components:
SavedPlaylistRequest: { $ref: ./openapi/components/media.yaml#/SavedPlaylistRequest }
SubscriptionFeedResponse: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionFeedResponse }
SubscriptionFeedPreparingResponse: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionFeedPreparingResponse }
RssFeedRequest: { $ref: ./openapi/components/rss.yaml#/RssFeedRequest }
RssFeedItem: { $ref: ./openapi/components/rss.yaml#/RssFeedItem }
RssFeedSecretItem: { $ref: ./openapi/components/rss.yaml#/RssFeedSecretItem }
AdminRssFeedsPage: { $ref: ./openapi/components/rss.yaml#/AdminRssFeedsPage }
SettingsItem: { $ref: ./openapi/components/access-control.yaml#/SettingsItem }
TypeTypeBackupItem: { $ref: ./openapi/components/user-backup.yaml#/TypeTypeBackupItem }
TypeTypeRestoreSummary: { $ref: ./openapi/components/user-backup.yaml#/TypeTypeRestoreSummary }
Expand Down
7 changes: 7 additions & 0 deletions openapi/components/access-control.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,17 @@ SettingsItem:
hideRelatedVideos: { type: boolean, default: false }
hideComments: { type: boolean, default: false }
hideShorts: { type: boolean, default: false }
hideSubscriptionLiveStreams: { type: boolean, default: false }
accessMode:
type: string
enum: [unrestricted, allow_list]
default: unrestricted
rssEnabled: { type: boolean, default: false }
rssPublicBaseUrl: { type: string, format: uri, nullable: true }
rssMaxFeedsPerUser: { type: integer, minimum: 1, maximum: 100, default: 10 }
rssMaxItems: { type: integer, minimum: 1, maximum: 200, default: 50 }
rssMinimumPollMinutes: { type: integer, minimum: 1, maximum: 1440, default: 5 }
rssRateLimitPerMinute: { type: integer, minimum: 1, maximum: 600, default: 30 }
AllowedChannelItem:
type: object
required: [url]
Expand Down
14 changes: 13 additions & 1 deletion openapi/components/instance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ InstanceResponse:
- oidcAutoRedirect
- youtubeRemoteLoginEnabled
- youtubeRemoteLoginReady
- rss
properties:
name: { type: string, example: TypeType }
tagline: { type: string, nullable: true }
Expand All @@ -44,8 +45,19 @@ InstanceResponse:
description: True only when the admin setting is enabled and remote login is ready.
youtubeRemoteLoginReady:
type: boolean
description: Non-secret readiness state for YouTube remote login.
description: True when Token accepts the shared secret and callback and its browser runtime is available.
youtubeRemoteLoginUnavailableReason:
type: string
nullable: true
enum: [disabled, not_configured, token_unreachable]
rss:
$ref: '#/RssInstanceCapability'
RssInstanceCapability:
type: object
required: [enabled, maxFeedsPerUser, maxItems, minimumPollMinutes, rateLimitPerMinute]
properties:
enabled: { type: boolean }
maxFeedsPerUser: { type: integer }
maxItems: { type: integer }
minimumPollMinutes: { type: integer }
rateLimitPerMinute: { type: integer }
67 changes: 67 additions & 0 deletions openapi/components/rss.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
RssFeedRequest:
type: object
required: [name]
properties:
name: { type: string, minLength: 1, maxLength: 100 }
scope: { type: string, enum: [all, channels], default: all }
channelUrls:
type: array
maxItems: 100
items: { type: string, format: uri }
serviceIds:
type: array
minItems: 1
uniqueItems: true
default: [0, 5, 6]
items: { type: integer, enum: [0, 5, 6] }
includeVideos: { type: boolean, default: true }
includeShorts: { type: boolean, default: true }
includeLive: { type: boolean, default: true }
includeUpcoming: { type: boolean, default: true }
RssFeedItem:
allOf:
- $ref: '#/RssFeedRequest'
- type: object
required: [id, enabled, createdAt, updatedAt, lastUsedAt]
properties:
id: { type: string }
enabled: { type: boolean }
createdAt: { type: integer, format: int64 }
updatedAt: { type: integer, format: int64 }
lastUsedAt: { type: integer, format: int64, nullable: true }
RssFeedSecretItem:
type: object
required: [feed, feedUrl]
properties:
feed: { $ref: '#/RssFeedItem' }
feedUrl:
type: string
format: uri
description: Returned only after creation or secret regeneration.
RssFeedEnabledRequest:
type: object
required: [enabled]
properties:
enabled: { type: boolean }
RssUserPolicyRequest:
$ref: '#/RssFeedEnabledRequest'
AdminRssFeedItem:
type: object
required: [feed, userId, userName, userEmail, userRssEnabled, userSuspended]
properties:
feed: { $ref: '#/RssFeedItem' }
userId: { type: string }
userName: { type: string }
userEmail: { type: string, format: email }
userRssEnabled: { type: boolean }
userSuspended: { type: boolean }
AdminRssFeedsPage:
type: object
required: [items, page, limit, total]
properties:
items:
type: array
items: { $ref: '#/AdminRssFeedItem' }
page: { type: integer }
limit: { type: integer }
total: { type: integer, format: int64 }
6 changes: 4 additions & 2 deletions openapi/paths/proxy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ Proxy:
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
Streams media from the supported YouTube, NicoNico, and BiliBili delivery hosts. Other
destinations, non-public addresses, cross-provider redirects, and non-HTTPS URLs are
rejected. 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
Expand Down
69 changes: 69 additions & 0 deletions openapi/paths/rss-admin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
AdminRssFeeds:
get:
tags: [rss]
summary: List private RSS feeds across the instance
security: [{ bearerAuth: [] }]
parameters:
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
- { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
responses:
'200':
description: Paginated RSS feeds and owners
content:
application/json:
schema: { $ref: ../components/rss.yaml#/AdminRssFeedsPage }
'400': { description: Invalid pagination }
'401': { description: Missing or invalid token }
'403': { description: Admin role required }
AdminRssFeedEnabled:
put:
tags: [rss]
summary: Enable or disable one RSS feed as an admin
security: [{ bearerAuth: [] }]
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema: { $ref: ../components/rss.yaml#/RssFeedEnabledRequest }
responses:
'200':
description: Updated feed
content:
application/json:
schema: { $ref: ../components/rss.yaml#/RssFeedItem }
'400': { description: Invalid request body }
'401': { description: Missing or invalid token }
'403': { description: Admin role required }
'404': { description: Feed not found }
AdminRssFeed:
delete:
tags: [rss]
summary: Revoke and delete one RSS feed as an admin
security: [{ bearerAuth: [] }]
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'204': { description: Deleted }
'401': { description: Missing or invalid token }
'403': { description: Admin role required }
'404': { description: Feed not found }
AdminRssUserEnabled:
put:
tags: [rss]
summary: Enable or disable RSS for one account
security: [{ bearerAuth: [] }]
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema: { $ref: ../components/rss.yaml#/RssUserPolicyRequest }
responses:
'204': { description: Account RSS policy updated }
'400': { description: Invalid request body }
'401': { description: Missing or invalid token }
'403': { description: Admin role required }
'404': { description: Account not found }
Loading