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
18 changes: 18 additions & 0 deletions database_functions/migration_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7151,6 +7151,24 @@ def migration_075_adaptive_refresh_schedule(conn, db_type: str) -> None:
cursor.close()



@register_migration("076", "youtube_video_filters", "Podcasts.VideoMinMinutes/VideoMaxMinutes/VideoTitleFilter: which new videos a YouTube channel subscription adds", requires=["001"])
def migration_076_youtube_video_filters(conn, db_type: str) -> None:
"""Per-subscription filters for a YouTube channel's new videos: shortest and longest length in
minutes, and text the title must contain. NULL means no filter."""
cursor = conn.cursor()
podcasts = '"Podcasts"' if db_type == "postgresql" else "Podcasts"
try:
for column, ddl in (("VideoMinMinutes", "INT"), ("VideoMaxMinutes", "INT"), ("VideoTitleFilter", "VARCHAR(255)")):
if not _column_exists(cursor, db_type, "Podcasts", column):
cursor.execute(f"ALTER TABLE {podcasts} ADD COLUMN {column} {ddl}")
conn.commit()
except Exception:
conn.rollback()
raise
finally:
cursor.close()

def _refresh_interval_minutes(published, now) -> int:
"""A feed's refresh interval from its newest publishing dates: an eighth of the median gap,
15 minutes to 6 hours, 24 hours when nothing came out in 90 days. Matches the refresh."""
Expand Down
172 changes: 172 additions & 0 deletions rust-api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -10002,6 +10002,54 @@
]
}
},
"/api/data/update_youtube_video_filter": {
"post": {
"tags": [
"youtube"
],
"summary": "Set a channel's video filter",
"operationId": "update_youtube_video_filter",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateYouTubeVideoFilterRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "The filter as stored",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/YouTubeVideoFilter"
}
}
}
},
"400": {
"description": "Negative lengths, shortest longer than longest, or title filter too long"
},
"401": {
"description": "Invalid or missing API key"
},
"403": {
"description": "Not your podcast"
},
"404": {
"description": "No such YouTube channel"
}
},
"security": [
{
"api_key": []
}
]
}
},
"/api/data/user/custom_themes": {
"post": {
"tags": [
Expand Down Expand Up @@ -11112,6 +11160,61 @@
]
}
},
"/api/data/youtube_video_filter": {
"get": {
"tags": [
"youtube"
],
"summary": "Get a channel's video filter",
"operationId": "get_youtube_video_filter",
"parameters": [
{
"name": "podcast_id",
"in": "query",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "user_id",
"in": "query",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "The filter; unset fields mean no limit",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/YouTubeVideoFilter"
}
}
}
},
"401": {
"description": "Invalid or missing API key"
},
"403": {
"description": "Not your podcast"
},
"404": {
"description": "No such YouTube channel"
}
},
"security": [
{
"api_key": []
}
]
}
},
"/api/data/ytdlp_settings": {
"get": {
"tags": [
Expand Down Expand Up @@ -19006,6 +19109,46 @@
}
}
},
"UpdateYouTubeVideoFilterRequest": {
"type": "object",
"required": [
"podcast_id",
"user_id"
],
"properties": {
"podcast_id": {
"type": "integer",
"format": "int32"
},
"user_id": {
"type": "integer",
"format": "int32"
},
"min_minutes": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Shortest video to add, in minutes; empty or 0 for no limit."
},
"max_minutes": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Longest video to add, in minutes; empty or 0 for no limit."
},
"title_contains": {
"type": [
"string",
"null"
],
"description": "Only add videos whose title contains this (any case); empty for all."
}
}
},
"UserCategoriesResponse": {
"type": "object",
"required": [
Expand Down Expand Up @@ -19231,6 +19374,35 @@
}
}
},
"YouTubeVideoFilter": {
"type": "object",
"description": "Which new videos a channel subscription adds. `None` means no limit.",
"properties": {
"min_minutes": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Shortest video, in minutes."
},
"max_minutes": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Longest video, in minutes."
},
"title_contains": {
"type": [
"string",
"null"
],
"description": "Text the title must contain (any case)."
}
}
},
"YtDlpSettingsUpdate": {
"type": "object",
"description": "Update payload from the settings page (only the two user-editable fields).",
Expand Down
5 changes: 5 additions & 0 deletions rust-api/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ pub struct Config {
pub api: ApiConfig,
}

pub const YOUTUBE_FEED_URL: &str = "https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}";

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiConfig {
pub search_api_url: String,
pub people_api_url: String,
/// A YouTube channel's RSS feed, with `{channel_id}` in place of the channel.
pub youtube_feed_url: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -335,6 +339,7 @@ impl Config {
let api = ApiConfig {
search_api_url: env::var("SEARCH_API_URL").unwrap(),
people_api_url: env::var("PEOPLE_API_URL").unwrap(),
youtube_feed_url: YOUTUBE_FEED_URL.to_string(),
};

// Validate OIDC configuration
Expand Down
Loading
Loading