From 9fd3ae6563bad2196eb5ee1e9e7b180debddf0ee Mon Sep 17 00:00:00 2001 From: lonelyhentxi Date: Tue, 24 Jun 2025 06:37:19 +0800 Subject: [PATCH] feat: basic support rss --- .../examples/mikan_collect_classic_eps.rs | 50 +-- apps/recorder/src/app/core.rs | 5 +- .../src/extract/bittorrent/extract.rs | 2 +- apps/recorder/src/extract/mikan/web.rs | 23 +- apps/recorder/src/graphql/domains/feeds.rs | 45 ++- .../src/graphql/domains/subscribers.rs | 1 - apps/recorder/src/migrations/defs.rs | 3 + .../m20240224_082543_add_downloads.rs | 4 +- .../src/migrations/m20250622_015618_feeds.rs | 5 +- ...0250622_020819_bangumi_and_episode_type.rs | 12 +- apps/recorder/src/models/bangumi.rs | 10 +- apps/recorder/src/models/downloads.rs | 4 +- apps/recorder/src/models/episodes.rs | 3 +- apps/recorder/src/models/feeds/mod.rs | 2 + apps/recorder/src/models/feeds/registry.rs | 23 +- apps/recorder/src/models/feeds/rss.rs | 36 +-- .../feeds/subscription_episodes_feed.rs | 6 +- apps/recorder/src/models/subscribers.rs | 20 ++ apps/recorder/src/models/subscriptions/mod.rs | 10 + apps/recorder/src/web/controller/feeds/mod.rs | 9 +- apps/recorder/src/web/controller/oidc/mod.rs | 4 +- .../src/domains/recorder/schema/feeds.ts | 19 ++ .../domains/recorder/schema/subscriptions.ts | 11 +- apps/webui/src/infra/graphql/gql/gql.ts | 18 +- apps/webui/src/infra/graphql/gql/graphql.ts | 287 ++++++++++++++++-- .../routes/_app/subscriptions/detail.$id.tsx | 134 +++++++- 26 files changed, 637 insertions(+), 109 deletions(-) create mode 100644 apps/webui/src/domains/recorder/schema/feeds.ts diff --git a/apps/recorder/examples/mikan_collect_classic_eps.rs b/apps/recorder/examples/mikan_collect_classic_eps.rs index 257e797..b3bbad2 100644 --- a/apps/recorder/examples/mikan_collect_classic_eps.rs +++ b/apps/recorder/examples/mikan_collect_classic_eps.rs @@ -500,31 +500,31 @@ async fn merge_mikan_classic_episodes_and_strip_columns() -> RecorderResult<()> } select_columns_and_write(merged_df.clone(), "tiny", &["fansub_name", "original_name"])?; - select_columns_and_write( - merged_df.clone(), - "lite", - &[ - "mikan_fansub_id", - "fansub_name", - "mikan_episode_id", - "original_name", - ], - )?; - select_columns_and_write( - merged_df, - "full", - &[ - "id", - "publish_at_timestamp", - "mikan_fansub_id", - "fansub_name", - "mikan_episode_id", - "original_name", - "magnet_link", - "file_size", - "torrent_link", - ], - )?; + // select_columns_and_write( + // merged_df.clone(), + // "lite", + // &[ + // "mikan_fansub_id", + // "fansub_name", + // "mikan_episode_id", + // "original_name", + // ], + // )?; + // select_columns_and_write( + // merged_df, + // "full", + // &[ + // "id", + // "publish_at_timestamp", + // "mikan_fansub_id", + // "fansub_name", + // "mikan_episode_id", + // "original_name", + // "magnet_link", + // "file_size", + // "torrent_link", + // ], + // )?; Ok(()) } diff --git a/apps/recorder/src/app/core.rs b/apps/recorder/src/app/core.rs index 22d7f70..85d11cc 100644 --- a/apps/recorder/src/app/core.rs +++ b/apps/recorder/src/app/core.rs @@ -53,14 +53,15 @@ impl App { let mut router = Router::>::new(); - let (graphql_c, oidc_c, metadata_c, static_c) = futures::try_join!( + let (graphql_c, oidc_c, metadata_c, static_c, feeds_c) = futures::try_join!( controller::graphql::create(context.clone()), controller::oidc::create(context.clone()), controller::metadata::create(context.clone()), controller::r#static::create(context.clone()), + controller::feeds::create(context.clone()), )?; - for c in [graphql_c, oidc_c, metadata_c, static_c] { + for c in [graphql_c, oidc_c, metadata_c, static_c, feeds_c] { router = c.apply_to(router); } diff --git a/apps/recorder/src/extract/bittorrent/extract.rs b/apps/recorder/src/extract/bittorrent/extract.rs index 7af4c09..07b46b2 100644 --- a/apps/recorder/src/extract/bittorrent/extract.rs +++ b/apps/recorder/src/extract/bittorrent/extract.rs @@ -39,7 +39,7 @@ pub struct EpisodeEnclosureMeta { pub magnet_link: Option, pub torrent_link: Option, pub pub_date: Option>, - pub content_length: Option, + pub content_length: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] diff --git a/apps/recorder/src/extract/mikan/web.rs b/apps/recorder/src/extract/mikan/web.rs index f59e774..230ad9a 100644 --- a/apps/recorder/src/extract/mikan/web.rs +++ b/apps/recorder/src/extract/mikan/web.rs @@ -41,7 +41,7 @@ use crate::{ pub struct MikanRssEpisodeItem { pub title: String, pub torrent_link: Url, - pub content_length: Option, + pub content_length: Option, pub mime: String, pub pub_date: Option>, pub mikan_episode_id: String, @@ -95,15 +95,32 @@ impl TryFrom for MikanRssEpisodeItem { RecorderError::from_mikan_rss_invalid_field(Cow::Borrowed("mikan_episode_id")) })?; + let pub_date = item + .extensions + .get("torrent") + .and_then(|t| t.get("pubDate")) + .and_then(|e| e.first()) + .and_then(|e| e.value.as_deref()); + Ok(MikanRssEpisodeItem { title, torrent_link: enclosure_url, content_length: enclosure.length.parse().ok(), mime: mime_type, - pub_date: item.pub_date.and_then(|s| { - DateTime::parse_from_rfc2822(&s) + pub_date: pub_date.and_then(|s| { + DateTime::parse_from_rfc2822(s) .ok() .map(|s| s.with_timezone(&Utc)) + .or_else(|| { + DateTime::parse_from_rfc3339(s) + .ok() + .map(|s| s.with_timezone(&Utc)) + }) + .or_else(|| { + DateTime::parse_from_rfc3339(&format!("{s}+08:00")) + .ok() + .map(|s| s.with_timezone(&Utc)) + }) }), mikan_episode_id, magnet_link: None, diff --git a/apps/recorder/src/graphql/domains/feeds.rs b/apps/recorder/src/graphql/domains/feeds.rs index b9c9d16..095a795 100644 --- a/apps/recorder/src/graphql/domains/feeds.rs +++ b/apps/recorder/src/graphql/domains/feeds.rs @@ -1,9 +1,50 @@ -use seaography::{Builder as SeaographyBuilder, BuilderContext}; +use std::sync::Arc; -use crate::{graphql::domains::subscribers::restrict_subscriber_for_entity, models::feeds}; +use async_graphql::dynamic::ResolverContext; +use sea_orm::Value as SeaValue; +use seaography::{Builder as SeaographyBuilder, BuilderContext, SeaResult}; + +use crate::{ + graphql::{ + domains::subscribers::restrict_subscriber_for_entity, + infra::util::{get_entity_column_key, get_entity_key}, + }, + models::feeds, +}; pub fn register_feeds_to_schema_context(context: &mut BuilderContext) { restrict_subscriber_for_entity::(context, &feeds::Column::SubscriberId); + { + let entity_column_key = + get_entity_column_key::(context, &feeds::Column::Token); + let entity_key = get_entity_key::(context); + let entity_name = context.entity_query_field.type_name.as_ref()(&entity_key); + let entity_create_one_mutation_field_name = Arc::new(format!( + "{}{}", + entity_name, context.entity_create_one_mutation.mutation_suffix + )); + let entity_create_batch_mutation_field_name = Arc::new(format!( + "{}{}", + entity_name, + context.entity_create_batch_mutation.mutation_suffix.clone() + )); + + context.types.input_none_conversions.insert( + entity_column_key, + Box::new( + move |context: &ResolverContext| -> SeaResult> { + let field_name = context.field().name(); + if field_name == entity_create_one_mutation_field_name.as_str() + || field_name == entity_create_batch_mutation_field_name.as_str() + { + Ok(Some(SeaValue::String(Some(Box::new(nanoid::nanoid!()))))) + } else { + Ok(None) + } + }, + ), + ); + } } pub fn register_feeds_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder { diff --git a/apps/recorder/src/graphql/domains/subscribers.rs b/apps/recorder/src/graphql/domains/subscribers.rs index 6b47afe..00a2b1f 100644 --- a/apps/recorder/src/graphql/domains/subscribers.rs +++ b/apps/recorder/src/graphql/domains/subscribers.rs @@ -267,7 +267,6 @@ where Box::new( move |context: &ResolverContext| -> SeaResult> { let field_name = context.field().name(); - tracing::warn!("field_name: {:?}", field_name); if field_name == entity_create_one_mutation_field_name.as_str() || field_name == entity_create_batch_mutation_field_name.as_str() { diff --git a/apps/recorder/src/migrations/defs.rs b/apps/recorder/src/migrations/defs.rs index b1e5959..ac07220 100644 --- a/apps/recorder/src/migrations/defs.rs +++ b/apps/recorder/src/migrations/defs.rs @@ -79,6 +79,9 @@ pub enum Episodes { BangumiId, SubscriberId, DownloadId, + /** + * @deprecated + */ SavePath, Resolution, Season, diff --git a/apps/recorder/src/migrations/m20240224_082543_add_downloads.rs b/apps/recorder/src/migrations/m20240224_082543_add_downloads.rs index f854f66..04f397c 100644 --- a/apps/recorder/src/migrations/m20240224_082543_add_downloads.rs +++ b/apps/recorder/src/migrations/m20240224_082543_add_downloads.rs @@ -95,8 +95,8 @@ impl MigrationTrait for Migration { DownloadMimeEnum, DownloadMime::iden_values(), )) - .col(big_unsigned(Downloads::AllSize)) - .col(big_unsigned(Downloads::CurrSize)) + .col(big_integer(Downloads::AllSize)) + .col(big_integer(Downloads::CurrSize)) .col(text(Downloads::Url)) .col(text_null(Downloads::Homepage)) .col(text_null(Downloads::SavePath)) diff --git a/apps/recorder/src/migrations/m20250622_015618_feeds.rs b/apps/recorder/src/migrations/m20250622_015618_feeds.rs index 3fed4d8..821f5e4 100644 --- a/apps/recorder/src/migrations/m20250622_015618_feeds.rs +++ b/apps/recorder/src/migrations/m20250622_015618_feeds.rs @@ -1,8 +1,5 @@ use async_trait::async_trait; -use sea_orm_migration::{ - prelude::*, - schema::{enumeration, integer_null, pk_auto, text}, -}; +use sea_orm_migration::{prelude::*, schema::*}; use crate::{ migrations::defs::{ diff --git a/apps/recorder/src/migrations/m20250622_020819_bangumi_and_episode_type.rs b/apps/recorder/src/migrations/m20250622_020819_bangumi_and_episode_type.rs index d9e066e..3c033d3 100644 --- a/apps/recorder/src/migrations/m20250622_020819_bangumi_and_episode_type.rs +++ b/apps/recorder/src/migrations/m20250622_020819_bangumi_and_episode_type.rs @@ -1,10 +1,5 @@ use async_trait::async_trait; -use sea_orm_migration::{ - prelude::*, - schema::{ - enumeration, enumeration_null, integer_null, text_null, timestamp_with_time_zone_null, - }, -}; +use sea_orm_migration::{prelude::*, schema::*}; use crate::{ migrations::defs::{Bangumi, CustomSchemaManagerExt, Episodes}, @@ -85,7 +80,10 @@ impl MigrationTrait for Migration { .add_column_if_not_exists(timestamp_with_time_zone_null( Episodes::EnclosurePubDate, )) - .add_column_if_not_exists(integer_null(Episodes::EnclosureContentLength)) + .add_column_if_not_exists(big_integer_null( + Episodes::EnclosureContentLength, + )) + .drop_column(Episodes::SavePath) .to_owned(), ) .await?; diff --git a/apps/recorder/src/models/bangumi.rs b/apps/recorder/src/models/bangumi.rs index ac767d4..eb49829 100644 --- a/apps/recorder/src/models/bangumi.rs +++ b/apps/recorder/src/models/bangumi.rs @@ -17,7 +17,7 @@ use crate::{ MikanBangumiHash, MikanBangumiMeta, build_mikan_bangumi_subscription_rss_url, scrape_mikan_poster_meta_from_image_url, }, - origin::{OriginCompTrait, SeasonComp}, + origin::{BangumiComps, OriginCompTrait}, }, }; @@ -129,11 +129,12 @@ impl ActiveModel { ) -> RecorderResult { let mikan_client = ctx.mikan(); let mikan_base_url = mikan_client.base_url(); - let season_comp = SeasonComp::parse_comp(&meta.bangumi_title) + let season_comp = BangumiComps::parse_comp(&meta.bangumi_title) .ok() - .map(|(_, s)| s); + .map(|(_, s)| s) + .and_then(|s| s.season); let season_index = season_comp.as_ref().map(|s| s.num).unwrap_or(1); - let season_raw = season_comp.map(|s| s.source.into_owned()); + let season_raw = season_comp.map(|s| s.source.to_string()); let rss_url = build_mikan_bangumi_subscription_rss_url( mikan_base_url.clone(), @@ -162,6 +163,7 @@ impl ActiveModel { origin_poster_link: ActiveValue::Set(meta.origin_poster_src.map(|src| src.to_string())), homepage: ActiveValue::Set(Some(meta.homepage.to_string())), rss_link: ActiveValue::Set(Some(rss_url.to_string())), + bangumi_type: ActiveValue::Set(BangumiType::Mikan), ..Default::default() }) } diff --git a/apps/recorder/src/models/downloads.rs b/apps/recorder/src/models/downloads.rs index fee5538..6611bd2 100644 --- a/apps/recorder/src/models/downloads.rs +++ b/apps/recorder/src/models/downloads.rs @@ -52,8 +52,8 @@ pub struct Model { pub status: DownloadStatus, pub mime: DownloadMime, pub url: String, - pub all_size: Option, - pub curr_size: Option, + pub all_size: Option, + pub curr_size: Option, pub homepage: Option, pub save_path: Option, } diff --git a/apps/recorder/src/models/episodes.rs b/apps/recorder/src/models/episodes.rs index 95f6a4f..553ea24 100644 --- a/apps/recorder/src/models/episodes.rs +++ b/apps/recorder/src/models/episodes.rs @@ -36,7 +36,7 @@ pub struct Model { pub enclosure_torrent_link: Option, pub enclosure_magnet_link: Option, pub enclosure_pub_date: Option, - pub enclosure_content_length: Option, + pub enclosure_content_length: Option, pub episode_type: EpisodeType, pub origin_name: String, pub display_name: String, @@ -166,6 +166,7 @@ impl ActiveModel { enclosure_magnet_link: ActiveValue::Set(enclosure_meta.magnet_link), enclosure_pub_date: ActiveValue::Set(enclosure_meta.pub_date), enclosure_content_length: ActiveValue::Set(enclosure_meta.content_length), + episode_type: ActiveValue::Set(EpisodeType::Mikan), ..Default::default() }; diff --git a/apps/recorder/src/models/feeds/mod.rs b/apps/recorder/src/models/feeds/mod.rs index 317edd1..7c4c8fd 100644 --- a/apps/recorder/src/models/feeds/mod.rs +++ b/apps/recorder/src/models/feeds/mod.rs @@ -39,7 +39,9 @@ pub enum FeedSource { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "feeds")] pub struct Model { + #[sea_orm(default_expr = "Expr::current_timestamp()")] pub created_at: DateTimeUtc, + #[sea_orm(default_expr = "Expr::current_timestamp()")] pub updated_at: DateTimeUtc, #[sea_orm(primary_key)] pub id: i32, diff --git a/apps/recorder/src/models/feeds/registry.rs b/apps/recorder/src/models/feeds/registry.rs index 90cc63f..68ed7e1 100644 --- a/apps/recorder/src/models/feeds/registry.rs +++ b/apps/recorder/src/models/feeds/registry.rs @@ -1,5 +1,5 @@ use rss::Channel; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; +use sea_orm::{ColumnTrait, EntityTrait, JoinType, QueryFilter, QuerySelect, RelationTrait}; use url::Url; use crate::{ @@ -8,7 +8,7 @@ use crate::{ models::{ episodes, feeds::{self, FeedSource, RssFeedTrait, SubscriptionEpisodesFeed}, - subscriptions, + subscription_episode, subscriptions, }, }; @@ -22,19 +22,30 @@ impl Feed { FeedSource::SubscriptionEpisode => { let db = ctx.db(); let (subscription, episodes) = if let Some(subscription_id) = m.subscription_id - && let Some((subscription, episodes)) = subscriptions::Entity::find() + && let Some(subscription) = subscriptions::Entity::find() .filter(subscriptions::Column::Id.eq(subscription_id)) - .find_with_related(episodes::Entity) - .all(db) + .one(db) .await? - .pop() { + let episodes = episodes::Entity::find() + .join( + JoinType::InnerJoin, + episodes::Relation::SubscriptionEpisode.def(), + ) + .join( + JoinType::InnerJoin, + subscription_episode::Relation::Subscription.def(), + ) + .filter(subscriptions::Column::Id.eq(subscription_id)) + .all(db) + .await?; (subscription, episodes) } else { return Err(RecorderError::ModelEntityNotFound { entity: "Subscription".into(), }); }; + Ok(Feed::SubscritpionEpisodes( SubscriptionEpisodesFeed::from_model(m, subscription, episodes), )) diff --git a/apps/recorder/src/models/feeds/rss.rs b/apps/recorder/src/models/feeds/rss.rs index f1f4df1..444dd4b 100644 --- a/apps/recorder/src/models/feeds/rss.rs +++ b/apps/recorder/src/models/feeds/rss.rs @@ -23,7 +23,7 @@ pub trait RssFeedItemTrait: Sized { fn get_enclosure_link(&self, ctx: &dyn AppContextTrait, api_base: &Url) -> Option>; fn get_enclosure_pub_date(&self) -> Option>; - fn get_enclosure_content_length(&self) -> Option; + fn get_enclosure_content_length(&self) -> Option; fn into_item(self, ctx: &dyn AppContextTrait, api_base: &Url) -> RecorderResult { let enclosure_mime_type = self.get_enclosure_mime() @@ -43,12 +43,7 @@ pub trait RssFeedItemTrait: Sized { source: None.into(), } })?; - let enclosure_pub_date = self.get_enclosure_pub_date().ok_or_else(|| { - RecorderError::MikanRssInvalidFieldError { - field: "enclosure_pub_date".into(), - source: None.into(), - } - })?; + let enclosure_pub_date = self.get_enclosure_pub_date(); let link = self.get_link(ctx, api_base).ok_or_else(|| { RecorderError::MikanRssInvalidFieldError { field: "link".into(), @@ -58,9 +53,8 @@ pub trait RssFeedItemTrait: Sized { let mut extensions = ExtensionMap::default(); if enclosure_mime_type == BITTORRENT_MIME_TYPE { - extensions.insert( - "torrent".to_string(), - btreemap! { + extensions.insert("torrent".to_string(), { + let mut map = btreemap! { "link".to_string() => vec![ ExtensionBuilder::default().name( "link" @@ -71,13 +65,20 @@ pub trait RssFeedItemTrait: Sized { "contentLength" ).value(enclosure_content_length.to_string()).build() ], - "pubDate".to_string() => vec![ - ExtensionBuilder::default().name( - "pubDate" - ).value(enclosure_pub_date.to_rfc3339()).build() - ], - }, - ); + }; + if let Some(pub_date) = enclosure_pub_date { + map.insert( + "pubDate".to_string(), + vec![ + ExtensionBuilder::default() + .name("pubDate") + .value(pub_date.to_rfc3339()) + .build(), + ], + ); + } + map + }); }; let enclosure = EnclosureBuilder::default() @@ -97,7 +98,6 @@ pub trait RssFeedItemTrait: Sized { .description(self.get_description().to_string()) .link(link.to_string()) .enclosure(enclosure) - .pub_date(enclosure_pub_date.to_rfc3339()) .extensions(extensions) .build(); diff --git a/apps/recorder/src/models/feeds/subscription_episodes_feed.rs b/apps/recorder/src/models/feeds/subscription_episodes_feed.rs index 1839b43..230a4d2 100644 --- a/apps/recorder/src/models/feeds/subscription_episodes_feed.rs +++ b/apps/recorder/src/models/feeds/subscription_episodes_feed.rs @@ -74,7 +74,7 @@ impl RssFeedItemTrait for episodes::Model { self.enclosure_pub_date } - fn get_enclosure_content_length(&self) -> Option { + fn get_enclosure_content_length(&self) -> Option { self.enclosure_content_length } } @@ -84,8 +84,8 @@ impl RssFeedTrait for SubscriptionEpisodesFeed { fn get_description(&self) -> Cow<'_, str> { Cow::Owned(format!( - "{PROJECT_NAME} - episodes of subscription \"{}\"", - self.subscription.display_name + "{PROJECT_NAME} - episodes of subscription {}", + self.subscription.id )) } diff --git a/apps/recorder/src/models/subscribers.rs b/apps/recorder/src/models/subscribers.rs index 010b7c3..5c13d23 100644 --- a/apps/recorder/src/models/subscribers.rs +++ b/apps/recorder/src/models/subscribers.rs @@ -41,6 +41,10 @@ pub enum Relation { Auth, #[sea_orm(has_many = "super::credential_3rd::Entity")] Credential3rd, + #[sea_orm(has_many = "super::feeds::Entity")] + Feed, + #[sea_orm(has_many = "super::subscriber_tasks::Entity")] + SubscriberTask, } impl Related for Entity { @@ -79,6 +83,18 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Feed.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SubscriberTask.def() + } +} + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] pub enum RelatedEntity { #[sea_orm(entity = "super::subscriptions::Entity")] @@ -91,6 +107,10 @@ pub enum RelatedEntity { Episode, #[sea_orm(entity = "super::credential_3rd::Entity")] Credential3rd, + #[sea_orm(entity = "super::feeds::Entity")] + Feed, + #[sea_orm(entity = "super::subscriber_tasks::Entity")] + SubscriberTask, } #[derive(Debug, Deserialize, Serialize)] diff --git a/apps/recorder/src/models/subscriptions/mod.rs b/apps/recorder/src/models/subscriptions/mod.rs index 6371d59..565f1d4 100644 --- a/apps/recorder/src/models/subscriptions/mod.rs +++ b/apps/recorder/src/models/subscriptions/mod.rs @@ -59,6 +59,8 @@ pub enum Relation { on_delete = "SetNull" )] Credential3rd, + #[sea_orm(has_many = "super::feeds::Entity")] + Feed, } impl Related for Entity { @@ -93,6 +95,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Feed.def() + } +} + impl Related for Entity { fn to() -> RelationDef { super::subscription_episode::Relation::Episode.def() @@ -127,6 +135,8 @@ pub enum RelatedEntity { SubscriptionBangumi, #[sea_orm(entity = "super::credential_3rd::Entity")] Credential3rd, + #[sea_orm(entity = "super::feeds::Entity")] + Feed, } #[async_trait] diff --git a/apps/recorder/src/web/controller/feeds/mod.rs b/apps/recorder/src/web/controller/feeds/mod.rs index 694bdfd..6d3cb0a 100644 --- a/apps/recorder/src/web/controller/feeds/mod.rs +++ b/apps/recorder/src/web/controller/feeds/mod.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use axum::{ - Extension, Router, + Router, extract::{Path, State}, response::IntoResponse, routing::get, @@ -21,21 +21,22 @@ pub const CONTROLLER_PREFIX: &str = "/api/feeds"; async fn rss_handler( State(ctx): State>, Path(token): Path, - forwarded_info: Extension, + forwarded_info: ForwardedRelatedInfo, ) -> RecorderResult { let api_base = forwarded_info .resolved_origin() .ok_or(RecorderError::MissingOriginError)?; let channel = feeds::Model::find_rss_feed_by_token(ctx.as_ref(), &token, &api_base).await?; + Ok(( StatusCode::OK, - [("Content-Type", "application/rss+xml")], + [("Content-Type", "application/xml; charset=utf-8")], channel.to_string(), )) } pub async fn create(_ctx: Arc) -> RecorderResult { - let router = Router::>::new().route("rss/{token}", get(rss_handler)); + let router = Router::>::new().route("/rss/{token}", get(rss_handler)); Ok(Controller::from_prefix(CONTROLLER_PREFIX, router)) } diff --git a/apps/recorder/src/web/controller/oidc/mod.rs b/apps/recorder/src/web/controller/oidc/mod.rs index a780533..83bc4ae 100644 --- a/apps/recorder/src/web/controller/oidc/mod.rs +++ b/apps/recorder/src/web/controller/oidc/mod.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use axum::{ - Extension, Json, Router, + Json, Router, extract::{Query, State}, routing::get, }; @@ -42,7 +42,7 @@ async fn oidc_callback( async fn oidc_auth( State(ctx): State>, - forwarded_info: Extension, + forwarded_info: ForwardedRelatedInfo, ) -> Result, AuthError> { let auth_service = ctx.auth(); if let AuthService::Oidc(oidc_auth_service) = auth_service { diff --git a/apps/webui/src/domains/recorder/schema/feeds.ts b/apps/webui/src/domains/recorder/schema/feeds.ts new file mode 100644 index 0000000..f82c8f7 --- /dev/null +++ b/apps/webui/src/domains/recorder/schema/feeds.ts @@ -0,0 +1,19 @@ +import { gql } from '@apollo/client'; + +export const INSERT_FEED = gql` + mutation InsertFeed($data: FeedsInsertInput!) { + feedsCreateOne(data: $data) { + id + createdAt + updatedAt + feedType + token + } + } +`; + +export const DELETE_FEED = gql` + mutation DeleteFeed($filters: FeedsFilterInput!) { + feedsDelete(filter: $filters) + } +`; diff --git a/apps/webui/src/domains/recorder/schema/subscriptions.ts b/apps/webui/src/domains/recorder/schema/subscriptions.ts index 75243ed..28dfaec 100644 --- a/apps/webui/src/domains/recorder/schema/subscriptions.ts +++ b/apps/webui/src/domains/recorder/schema/subscriptions.ts @@ -95,6 +95,16 @@ query GetSubscriptionDetail ($id: Int!) { category sourceUrl enabled + feed { + nodes { + id + createdAt + updatedAt + token + feedType + feedSource + } + } credential3rd { id username @@ -112,7 +122,6 @@ query GetSubscriptionDetail ($id: Int!) { mikanFansubId rssLink posterLink - savePath homepage } } diff --git a/apps/webui/src/infra/graphql/gql/gql.ts b/apps/webui/src/infra/graphql/gql/gql.ts index fd36210..6a2266c 100644 --- a/apps/webui/src/infra/graphql/gql/gql.ts +++ b/apps/webui/src/infra/graphql/gql/gql.ts @@ -20,11 +20,13 @@ type Documents = { "\n mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n": typeof types.DeleteCredential3rdDocument, "\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": typeof types.GetCredential3rdDetailDocument, "\n query CheckCredential3rdAvailable($id: Int!) {\n credential3rdCheckAvailable(filter: { id: $id }) {\n available\n }\n }\n": typeof types.CheckCredential3rdAvailableDocument, + "\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": typeof types.InsertFeedDocument, + "\n mutation DeleteFeed($filters: FeedsFilterInput!) {\n feedsDelete(filter: $filters)\n }\n": typeof types.DeleteFeedDocument, "\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetSubscriptionsDocument, "\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument, "\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument, "\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": typeof types.DeleteSubscriptionsDocument, - "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument, + "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument, "\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsIncrementalDocument, "\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsFullDocument, "\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionSourcesDocument, @@ -39,11 +41,13 @@ const documents: Documents = { "\n mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n": types.DeleteCredential3rdDocument, "\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": types.GetCredential3rdDetailDocument, "\n query CheckCredential3rdAvailable($id: Int!) {\n credential3rdCheckAvailable(filter: { id: $id }) {\n available\n }\n }\n": types.CheckCredential3rdAvailableDocument, + "\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": types.InsertFeedDocument, + "\n mutation DeleteFeed($filters: FeedsFilterInput!) {\n feedsDelete(filter: $filters)\n }\n": types.DeleteFeedDocument, "\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetSubscriptionsDocument, "\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument, "\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument, "\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": types.DeleteSubscriptionsDocument, - "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument, + "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument, "\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsIncrementalDocument, "\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsFullDocument, "\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionSourcesDocument, @@ -90,6 +94,14 @@ export function gql(source: "\n query GetCredential3rdDetail($id: Int!) {\n * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql(source: "\n query CheckCredential3rdAvailable($id: Int!) {\n credential3rdCheckAvailable(filter: { id: $id }) {\n available\n }\n }\n"): (typeof documents)["\n query CheckCredential3rdAvailable($id: Int!) {\n credential3rdCheckAvailable(filter: { id: $id }) {\n available\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n"): (typeof documents)["\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n mutation DeleteFeed($filters: FeedsFilterInput!) {\n feedsDelete(filter: $filters)\n }\n"): (typeof documents)["\n mutation DeleteFeed($filters: FeedsFilterInput!) {\n feedsDelete(filter: $filters)\n }\n"]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -109,7 +121,7 @@ export function gql(source: "\n mutation DeleteSubscriptions($filters: Subscr /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"]; +export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/apps/webui/src/infra/graphql/gql/graphql.ts b/apps/webui/src/infra/graphql/gql/graphql.ts index 44658e3..173b293 100644 --- a/apps/webui/src/infra/graphql/gql/graphql.ts +++ b/apps/webui/src/infra/graphql/gql/graphql.ts @@ -21,6 +21,7 @@ export type Scalars = { export type Bangumi = { __typename?: 'Bangumi'; + bangumiType: BangumiTypeEnum; createdAt: Scalars['String']['output']; displayName: Scalars['String']['output']; episode: EpisodesConnection; @@ -34,7 +35,6 @@ export type Bangumi = { originPosterLink?: Maybe; posterLink?: Maybe; rssLink?: Maybe; - savePath?: Maybe; season: Scalars['Int']['output']; seasonRaw?: Maybe; subscriber?: Maybe; @@ -67,6 +67,7 @@ export type BangumiSubscriptionBangumiArgs = { export type BangumiBasic = { __typename?: 'BangumiBasic'; + bangumiType: BangumiTypeEnum; createdAt: Scalars['String']['output']; displayName: Scalars['String']['output']; fansub?: Maybe; @@ -79,7 +80,6 @@ export type BangumiBasic = { originPosterLink?: Maybe; posterLink?: Maybe; rssLink?: Maybe; - savePath?: Maybe; season: Scalars['Int']['output']; seasonRaw?: Maybe; subscriberId: Scalars['Int']['output']; @@ -102,6 +102,7 @@ export type BangumiEdge = { export type BangumiFilterInput = { and?: InputMaybe>; + bangumiType?: InputMaybe; createdAt?: InputMaybe; displayName?: InputMaybe; fansub?: InputMaybe; @@ -114,14 +115,14 @@ export type BangumiFilterInput = { originPosterLink?: InputMaybe; posterLink?: InputMaybe; rssLink?: InputMaybe; - savePath?: InputMaybe; season?: InputMaybe; seasonRaw?: InputMaybe; - subscriberId?: InputMaybe; + subscriberId?: InputMaybe; updatedAt?: InputMaybe; }; export type BangumiInsertInput = { + bangumiType: BangumiTypeEnum; createdAt?: InputMaybe; displayName: Scalars['String']['input']; fansub?: InputMaybe; @@ -134,14 +135,14 @@ export type BangumiInsertInput = { originPosterLink?: InputMaybe; posterLink?: InputMaybe; rssLink?: InputMaybe; - savePath?: InputMaybe; season: Scalars['Int']['input']; seasonRaw?: InputMaybe; - subscriberId: Scalars['Int']['input']; + subscriberId?: InputMaybe; updatedAt?: InputMaybe; }; export type BangumiOrderInput = { + bangumiType?: InputMaybe; createdAt?: InputMaybe; displayName?: InputMaybe; fansub?: InputMaybe; @@ -154,14 +155,32 @@ export type BangumiOrderInput = { originPosterLink?: InputMaybe; posterLink?: InputMaybe; rssLink?: InputMaybe; - savePath?: InputMaybe; season?: InputMaybe; seasonRaw?: InputMaybe; subscriberId?: InputMaybe; updatedAt?: InputMaybe; }; +export const BangumiTypeEnum = { + Mikan: 'mikan' +} as const; + +export type BangumiTypeEnum = typeof BangumiTypeEnum[keyof typeof BangumiTypeEnum]; +export type BangumiTypeEnumFilterInput = { + eq?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + is_in?: InputMaybe>; + is_not_in?: InputMaybe>; + is_not_null?: InputMaybe; + is_null?: InputMaybe; + lt?: InputMaybe; + lte?: InputMaybe; + ne?: InputMaybe; +}; + export type BangumiUpdateInput = { + bangumiType?: InputMaybe; createdAt?: InputMaybe; displayName?: InputMaybe; fansub?: InputMaybe; @@ -174,10 +193,8 @@ export type BangumiUpdateInput = { originPosterLink?: InputMaybe; posterLink?: InputMaybe; rssLink?: InputMaybe; - savePath?: InputMaybe; season?: InputMaybe; seasonRaw?: InputMaybe; - subscriberId?: InputMaybe; updatedAt?: InputMaybe; }; @@ -613,6 +630,24 @@ export type DownloadsUpdateInput = { url?: InputMaybe; }; +export const EpisodeTypeEnum = { + Mikan: 'mikan' +} as const; + +export type EpisodeTypeEnum = typeof EpisodeTypeEnum[keyof typeof EpisodeTypeEnum]; +export type EpisodeTypeEnumFilterInput = { + eq?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + is_in?: InputMaybe>; + is_not_in?: InputMaybe>; + is_not_null?: InputMaybe; + is_null?: InputMaybe; + lt?: InputMaybe; + lte?: InputMaybe; + ne?: InputMaybe; +}; + export type Episodes = { __typename?: 'Episodes'; bangumi?: Maybe; @@ -620,7 +655,12 @@ export type Episodes = { createdAt: Scalars['String']['output']; displayName: Scalars['String']['output']; download: SubscriptionsConnection; + enclosureContentLength?: Maybe; + enclosureMagnetLink?: Maybe; + enclosurePubDate?: Maybe; + enclosureTorrentLink?: Maybe; episodeIndex: Scalars['Int']['output']; + episodeType: EpisodeTypeEnum; fansub?: Maybe; homepage?: Maybe; id: Scalars['Int']['output']; @@ -629,7 +669,6 @@ export type Episodes = { originPosterLink?: Maybe; posterLink?: Maybe; resolution?: Maybe; - savePath?: Maybe; season: Scalars['Int']['output']; seasonRaw?: Maybe; source?: Maybe; @@ -667,7 +706,12 @@ export type EpisodesBasic = { bangumiId: Scalars['Int']['output']; createdAt: Scalars['String']['output']; displayName: Scalars['String']['output']; + enclosureContentLength?: Maybe; + enclosureMagnetLink?: Maybe; + enclosurePubDate?: Maybe; + enclosureTorrentLink?: Maybe; episodeIndex: Scalars['Int']['output']; + episodeType: EpisodeTypeEnum; fansub?: Maybe; homepage?: Maybe; id: Scalars['Int']['output']; @@ -676,7 +720,6 @@ export type EpisodesBasic = { originPosterLink?: Maybe; posterLink?: Maybe; resolution?: Maybe; - savePath?: Maybe; season: Scalars['Int']['output']; seasonRaw?: Maybe; source?: Maybe; @@ -704,7 +747,12 @@ export type EpisodesFilterInput = { bangumiId?: InputMaybe; createdAt?: InputMaybe; displayName?: InputMaybe; + enclosureContentLength?: InputMaybe; + enclosureMagnetLink?: InputMaybe; + enclosurePubDate?: InputMaybe; + enclosureTorrentLink?: InputMaybe; episodeIndex?: InputMaybe; + episodeType?: InputMaybe; fansub?: InputMaybe; homepage?: InputMaybe; id?: InputMaybe; @@ -714,7 +762,6 @@ export type EpisodesFilterInput = { originPosterLink?: InputMaybe; posterLink?: InputMaybe; resolution?: InputMaybe; - savePath?: InputMaybe; season?: InputMaybe; seasonRaw?: InputMaybe; source?: InputMaybe; @@ -727,7 +774,12 @@ export type EpisodesInsertInput = { bangumiId: Scalars['Int']['input']; createdAt?: InputMaybe; displayName: Scalars['String']['input']; + enclosureContentLength?: InputMaybe; + enclosureMagnetLink?: InputMaybe; + enclosurePubDate?: InputMaybe; + enclosureTorrentLink?: InputMaybe; episodeIndex: Scalars['Int']['input']; + episodeType: EpisodeTypeEnum; fansub?: InputMaybe; homepage?: InputMaybe; id?: InputMaybe; @@ -736,7 +788,6 @@ export type EpisodesInsertInput = { originPosterLink?: InputMaybe; posterLink?: InputMaybe; resolution?: InputMaybe; - savePath?: InputMaybe; season: Scalars['Int']['input']; seasonRaw?: InputMaybe; source?: InputMaybe; @@ -749,7 +800,12 @@ export type EpisodesOrderInput = { bangumiId?: InputMaybe; createdAt?: InputMaybe; displayName?: InputMaybe; + enclosureContentLength?: InputMaybe; + enclosureMagnetLink?: InputMaybe; + enclosurePubDate?: InputMaybe; + enclosureTorrentLink?: InputMaybe; episodeIndex?: InputMaybe; + episodeType?: InputMaybe; fansub?: InputMaybe; homepage?: InputMaybe; id?: InputMaybe; @@ -758,7 +814,6 @@ export type EpisodesOrderInput = { originPosterLink?: InputMaybe; posterLink?: InputMaybe; resolution?: InputMaybe; - savePath?: InputMaybe; season?: InputMaybe; seasonRaw?: InputMaybe; source?: InputMaybe; @@ -771,7 +826,12 @@ export type EpisodesUpdateInput = { bangumiId?: InputMaybe; createdAt?: InputMaybe; displayName?: InputMaybe; + enclosureContentLength?: InputMaybe; + enclosureMagnetLink?: InputMaybe; + enclosurePubDate?: InputMaybe; + enclosureTorrentLink?: InputMaybe; episodeIndex?: InputMaybe; + episodeType?: InputMaybe; fansub?: InputMaybe; homepage?: InputMaybe; id?: InputMaybe; @@ -780,7 +840,6 @@ export type EpisodesUpdateInput = { originPosterLink?: InputMaybe; posterLink?: InputMaybe; resolution?: InputMaybe; - savePath?: InputMaybe; season?: InputMaybe; seasonRaw?: InputMaybe; source?: InputMaybe; @@ -788,6 +847,127 @@ export type EpisodesUpdateInput = { updatedAt?: InputMaybe; }; +export const FeedSourceEnum = { + SubscriptionEpisode: 'subscription_episode' +} as const; + +export type FeedSourceEnum = typeof FeedSourceEnum[keyof typeof FeedSourceEnum]; +export type FeedSourceEnumFilterInput = { + eq?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + is_in?: InputMaybe>; + is_not_in?: InputMaybe>; + is_not_null?: InputMaybe; + is_null?: InputMaybe; + lt?: InputMaybe; + lte?: InputMaybe; + ne?: InputMaybe; +}; + +export const FeedTypeEnum = { + Rss: 'rss' +} as const; + +export type FeedTypeEnum = typeof FeedTypeEnum[keyof typeof FeedTypeEnum]; +export type FeedTypeEnumFilterInput = { + eq?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + is_in?: InputMaybe>; + is_not_in?: InputMaybe>; + is_not_null?: InputMaybe; + is_null?: InputMaybe; + lt?: InputMaybe; + lte?: InputMaybe; + ne?: InputMaybe; +}; + +export type Feeds = { + __typename?: 'Feeds'; + createdAt: Scalars['String']['output']; + feedSource: FeedSourceEnum; + feedType: FeedTypeEnum; + id: Scalars['Int']['output']; + subscriber?: Maybe; + subscriberId?: Maybe; + subscription?: Maybe; + subscriptionId?: Maybe; + token: Scalars['String']['output']; + updatedAt: Scalars['String']['output']; +}; + +export type FeedsBasic = { + __typename?: 'FeedsBasic'; + createdAt: Scalars['String']['output']; + feedSource: FeedSourceEnum; + feedType: FeedTypeEnum; + id: Scalars['Int']['output']; + subscriberId?: Maybe; + subscriptionId?: Maybe; + token: Scalars['String']['output']; + updatedAt: Scalars['String']['output']; +}; + +export type FeedsConnection = { + __typename?: 'FeedsConnection'; + edges: Array; + nodes: Array; + pageInfo: PageInfo; + paginationInfo?: Maybe; +}; + +export type FeedsEdge = { + __typename?: 'FeedsEdge'; + cursor: Scalars['String']['output']; + node: Feeds; +}; + +export type FeedsFilterInput = { + and?: InputMaybe>; + createdAt?: InputMaybe; + feedSource?: InputMaybe; + feedType?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + subscriberId?: InputMaybe; + subscriptionId?: InputMaybe; + token?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type FeedsInsertInput = { + createdAt?: InputMaybe; + feedSource: FeedSourceEnum; + feedType: FeedTypeEnum; + id?: InputMaybe; + subscriberId?: InputMaybe; + subscriptionId?: InputMaybe; + token?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type FeedsOrderInput = { + createdAt?: InputMaybe; + feedSource?: InputMaybe; + feedType?: InputMaybe; + id?: InputMaybe; + subscriberId?: InputMaybe; + subscriptionId?: InputMaybe; + token?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type FeedsUpdateInput = { + createdAt?: InputMaybe; + feedSource?: InputMaybe; + feedType?: InputMaybe; + id?: InputMaybe; + subscriptionId?: InputMaybe; + token?: InputMaybe; + updatedAt?: InputMaybe; +}; + export type IntegerFilterInput = { between?: InputMaybe>; eq?: InputMaybe; @@ -826,6 +1006,10 @@ export type Mutation = { episodesCreateOne: EpisodesBasic; episodesDelete: Scalars['Int']['output']; episodesUpdate: Array; + feedsCreateBatch: Array; + feedsCreateOne: FeedsBasic; + feedsDelete: Scalars['Int']['output']; + feedsUpdate: Array; subscriberTasksDelete: Scalars['Int']['output']; subscriberTasksRetryOne: SubscriberTasks; subscriptionBangumiCreateBatch: Array; @@ -951,6 +1135,27 @@ export type MutationEpisodesUpdateArgs = { }; +export type MutationFeedsCreateBatchArgs = { + data: Array; +}; + + +export type MutationFeedsCreateOneArgs = { + data: FeedsInsertInput; +}; + + +export type MutationFeedsDeleteArgs = { + filter?: InputMaybe; +}; + + +export type MutationFeedsUpdateArgs = { + data: FeedsUpdateInput; + filter?: InputMaybe; +}; + + export type MutationSubscriberTasksDeleteArgs = { filter?: InputMaybe; }; @@ -1085,6 +1290,7 @@ export type Query = { downloaders: DownloadersConnection; downloads: DownloadsConnection; episodes: EpisodesConnection; + feeds: FeedsConnection; subscriberTasks: SubscriberTasksConnection; subscribers: SubscribersConnection; subscriptionBangumi: SubscriptionBangumiConnection; @@ -1138,6 +1344,13 @@ export type QueryEpisodesArgs = { }; +export type QueryFeedsArgs = { + filters?: InputMaybe; + orderBy?: InputMaybe; + pagination?: InputMaybe; +}; + + export type QuerySubscriberTasksArgs = { filters?: InputMaybe; orderBy?: InputMaybe; @@ -1288,7 +1501,9 @@ export type Subscribers = { displayName: Scalars['String']['output']; downloader: DownloadersConnection; episode: EpisodesConnection; + feed: FeedsConnection; id: Scalars['Int']['output']; + subscriberTask: SubscriberTasksConnection; subscription: SubscriptionsConnection; updatedAt: Scalars['String']['output']; }; @@ -1322,6 +1537,20 @@ export type SubscribersEpisodeArgs = { }; +export type SubscribersFeedArgs = { + filters?: InputMaybe; + orderBy?: InputMaybe; + pagination?: InputMaybe; +}; + + +export type SubscribersSubscriberTaskArgs = { + filters?: InputMaybe; + orderBy?: InputMaybe; + pagination?: InputMaybe; +}; + + export type SubscribersSubscriptionArgs = { filters?: InputMaybe; orderBy?: InputMaybe; @@ -1511,6 +1740,7 @@ export type Subscriptions = { displayName: Scalars['String']['output']; enabled: Scalars['Boolean']['output']; episode: EpisodesConnection; + feed: FeedsConnection; id: Scalars['Int']['output']; sourceUrl: Scalars['String']['output']; subscriber?: Maybe; @@ -1535,6 +1765,13 @@ export type SubscriptionsEpisodeArgs = { }; +export type SubscriptionsFeedArgs = { + filters?: InputMaybe; + orderBy?: InputMaybe; + pagination?: InputMaybe; +}; + + export type SubscriptionsSubscriptionBangumiArgs = { filters?: InputMaybe; orderBy?: InputMaybe; @@ -1684,6 +1921,20 @@ export type CheckCredential3rdAvailableQueryVariables = Exact<{ export type CheckCredential3rdAvailableQuery = { __typename?: 'Query', credential3rdCheckAvailable: { __typename?: 'Credential3rdCheckAvailableInfo', available: boolean } }; +export type InsertFeedMutationVariables = Exact<{ + data: FeedsInsertInput; +}>; + + +export type InsertFeedMutation = { __typename?: 'Mutation', feedsCreateOne: { __typename?: 'FeedsBasic', id: number, createdAt: string, updatedAt: string, feedType: FeedTypeEnum, token: string } }; + +export type DeleteFeedMutationVariables = Exact<{ + filters: FeedsFilterInput; +}>; + + +export type DeleteFeedMutation = { __typename?: 'Mutation', feedsDelete: number }; + export type GetSubscriptionsQueryVariables = Exact<{ filters: SubscriptionsFilterInput; orderBy: SubscriptionsOrderInput; @@ -1720,7 +1971,7 @@ export type GetSubscriptionDetailQueryVariables = Exact<{ }>; -export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, savePath?: string | null, homepage?: string | null }> } }> } }; +export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, feed: { __typename?: 'FeedsConnection', nodes: Array<{ __typename?: 'Feeds', id: number, createdAt: string, updatedAt: string, token: string, feedType: FeedTypeEnum, feedSource: FeedSourceEnum }> }, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, homepage?: string | null }> } }> } }; export type SyncSubscriptionFeedsIncrementalMutationVariables = Exact<{ filter: SubscriptionsFilterInput; @@ -1773,11 +2024,13 @@ export const UpdateCredential3rdDocument = {"kind":"Document","definitions":[{"k export const DeleteCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode; export const GetCredential3rdDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rdDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]}}]} as unknown as DocumentNode; export const CheckCredential3rdAvailableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CheckCredential3rdAvailable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdCheckAvailable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"available"}}]}}]}}]} as unknown as DocumentNode; +export const InsertFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FeedsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedsCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"feedType"}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]}}]} as unknown as DocumentNode; +export const DeleteFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FeedsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode; export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsOrderInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credentialId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode; export const InsertSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credentialId"}}]}}]}}]} as unknown as DocumentNode; export const UpdateSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}}]}}]} as unknown as DocumentNode; export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode; -export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"savePath"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"feedType"}},{"kind":"Field","name":{"kind":"Name","value":"feedSource"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const SyncSubscriptionFeedsIncrementalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsIncremental"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsIncremental"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const SyncSubscriptionFeedsFullDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsFull"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsFull"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const SyncSubscriptionSourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionSources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneSources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; diff --git a/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx b/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx index 001b06e..39ed52b 100644 --- a/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx +++ b/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx @@ -14,6 +14,7 @@ import { Img } from '@/components/ui/img'; import { Label } from '@/components/ui/label'; import { QueryErrorView } from '@/components/ui/query-error-view'; import { Separator } from '@/components/ui/separator'; +import { DELETE_FEED, INSERT_FEED } from '@/domains/recorder/schema/feeds'; import { GET_SUBSCRIPTION_DETAIL } from '@/domains/recorder/schema/subscriptions'; import { SubscriptionService } from '@/domains/recorder/services/subscription.service'; import { useInject } from '@/infra/di/inject'; @@ -22,10 +23,16 @@ import { getApolloQueryError, } from '@/infra/errors/apollo'; import { + type DeleteFeedMutation, + type DeleteFeedMutationVariables, + FeedSourceEnum, + FeedTypeEnum, type GetSubscriptionDetailQuery, + type InsertFeedMutation, + type InsertFeedMutationVariables, SubscriptionCategoryEnum, } from '@/infra/graphql/gql/graphql'; -import { useQuery } from '@apollo/client'; +import { useMutation, useQuery } from '@apollo/client'; import { createFileRoute, useCanGoBack, @@ -38,7 +45,9 @@ import { Edit, ExternalLink, ListIcon, + PlusIcon, RefreshCcwIcon, + Trash2, } from 'lucide-react'; import { useMemo } from 'react'; import { toast } from 'sonner'; @@ -91,6 +100,50 @@ function SubscriptionDetailRouteComponent() { }); }; + const [insertFeed] = useMutation< + InsertFeedMutation, + InsertFeedMutationVariables + >(INSERT_FEED, { + onCompleted: async () => { + const result = await refetch(); + const error = getApolloQueryError(result); + if (error) { + toast.error('Failed to add feed', { + description: apolloErrorToMessage(error), + }); + return; + } + toast.success('Feed added'); + }, + onError: (error) => { + toast.error('Failed to add feed', { + description: apolloErrorToMessage(error), + }); + }, + }); + + const [deleteFeed] = useMutation< + DeleteFeedMutation, + DeleteFeedMutationVariables + >(DELETE_FEED, { + onCompleted: async () => { + const result = await refetch(); + const error = getApolloQueryError(result); + if (error) { + toast.error('Failed to delete feed', { + description: apolloErrorToMessage(error), + }); + return; + } + toast.success('Feed deleted'); + }, + onError: (error) => { + toast.error('Failed to delete feed', { + description: apolloErrorToMessage(error), + }); + }, + }); + const subscription = data?.subscriptions?.nodes?.[0]; const sourceUrlMeta = useMemo( @@ -314,6 +367,85 @@ function SubscriptionDetailRouteComponent() { + +
+
+ + +
+ +
+ {subscription.feed?.nodes && + subscription.feed.nodes.length > 0 ? ( + subscription.feed.nodes.map((feed) => ( + { + window.open(`/api/feeds/rss/${feed.token}`, '_blank'); + }} + > +
+
+ + +
+ + + {feed.token} + + +
+ {format(new Date(feed.createdAt), 'MM-dd HH:mm')} +
+
+
+ )) + ) : ( +
+ No associated feeds now +
+ )} +
+
+ {subscription.bangumi?.nodes && subscription.bangumi.nodes.length > 0 && ( <>