fix: fix dotenv loader inconsistent and many ui issues

This commit is contained in:
master 2025-06-25 06:36:15 +08:00
parent 41ff5c2a11
commit 003d8840fd
29 changed files with 481 additions and 168 deletions

View File

@ -1,18 +1,18 @@
HOST="konobangu.com" LOGGER__LEVEL = "debug"
DATABASE_URL = "postgres://konobangu:konobangu@localhost:5432/konobangu"
STORAGE_DATA_DIR = "./data" DATABASE__URI = "postgres://konobangu:konobangu@localhost:5432/konobangu"
AUTH_TYPE = "basic" # or oidc
BASIC_USER = "konobangu" AUTH__AUTH_TYPE = "basic"
BASIC_PASSWORD = "konobangu" AUTH__BASIC_USER = "konobangu"
LOG_LEVEL = "debug" AUTH__BASIC_PASSWORD = "konobangu"
# OIDC_ISSUER="https://auth.logto.io/oidc"
# OIDC_AUDIENCE = "https://konobangu.com/api" # AUTH__OIDC_ISSUER = "https://auth.logto.io/oidc"
# OIDC_CLIENT_ID = "client_id" # AUTH__OIDC_AUDIENCE = "https://konobangu.com/api"
# OIDC_CLIENT_SECRET = "client_secret" # optional # AUTH__OIDC_CLIENT_ID = "client_id"
# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" # AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional
# OIDC_EXTRA_CLAIM_KEY = "" # AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"
# OIDC_EXTRA_CLAIM_VALUE = "" # AUTH__OIDC_EXTRA_CLAIM_KEY = ""
MIKAN_PROXY = "http://127.0.0.1:8899" # AUTH__OIDC_EXTRA_CLAIM_VALUE = ""
# MIKAN_PROXY_AUTH_HEADER = ""
# MIKAN_NO_PROXY = "" MIKAN__HTTP_CLIENT__PROXY__ACCEPT_INVALID_CERTS = true
MIKAN_PROXY_ACCEPT_INVALID_CERTS = true MIKAN__HTTP_CLIENT__PROXY__SERVER = "http://127.0.0.1:8899"

View File

@ -1,14 +1,15 @@
HOST="konobangu.com" HOST="konobangu.com"
DATABASE_URL = "postgres://konobangu:konobangu@localhost:5432/konobangu"
STORAGE_DATA_DIR = "./data" DATABASE__URI = "postgres://konobangu:konobangu@localhost:5432/konobangu"
AUTH_TYPE = "basic" # or oidc
BASIC_USER = "konobangu" AUTH__AUTH_TYPE = "basic" # or oidc
BASIC_PASSWORD = "konobangu" AUTH__BASIC_USER = "konobangu"
LOG_LEVEL = "info" AUTH__BASIC_PASSWORD = "konobangu"
# OIDC_ISSUER="https://auth.logto.io/oidc"
# OIDC_AUDIENCE = "https://konobangu.com/api" # AUTH__OIDC_ISSUER="https://auth.logto.io/oidc"
# OIDC_CLIENT_ID = "client_id" # AUTH__OIDC_AUDIENCE = "https://konobangu.com/api"
# OIDC_CLIENT_SECRET = "client_secret" # optional # AUTH__OIDC_CLIENT_ID = "client_id"
# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" # AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional
# OIDC_EXTRA_CLAIM_KEY = "" # AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"
# OIDC_EXTRA_CLAIM_VALUE = "" # AUTH__OIDC_EXTRA_CLAIM_KEY = ""
# AUTH__OIDC_EXTRA_CLAIM_VALUE = ""

View File

@ -4,7 +4,7 @@
enable = true enable = true
# Enable pretty backtrace (sets RUST_BACKTRACE=1) # Enable pretty backtrace (sets RUST_BACKTRACE=1)
pretty_backtrace = true pretty_backtrace = true
level = '{{ get_env(name="LOG_LEVEL", default="info") }}' level = "info"
# Log level, options: trace, debug, info, warn or error. # Log level, options: trace, debug, info, warn or error.
# Define the logging format. options: compact, pretty or Json # Define the logging format. options: compact, pretty or Json
format = "compact" format = "compact"
@ -77,7 +77,7 @@ max_connections = 10
auto_migrate = true auto_migrate = true
[storage] [storage]
data_dir = '{{ get_env(name="STORAGE_DATA_DIR", default="./data") }}' data_dir = './data'
[mikan] [mikan]
base_url = "https://mikanani.me/" base_url = "https://mikanani.me/"
@ -89,26 +89,6 @@ leaky_bucket_initial_tokens = 1
leaky_bucket_refill_tokens = 1 leaky_bucket_refill_tokens = 1
leaky_bucket_refill_interval = 500 leaky_bucket_refill_interval = 500
[mikan.http_client.proxy]
server = '{{ get_env(name="MIKAN_PROXY", default = "") }}'
auth_header = '{{ get_env(name="MIKAN_PROXY_AUTH_HEADER", default = "") }}'
no_proxy = '{{ get_env(name="MIKAN_NO_PROXY", default = "") }}'
accept_invalid_certs = '{{ get_env(name="MIKAN_PROXY_ACCEPT_INVALID_CERTS", default = "false") }}'
[auth]
auth_type = '{{ get_env(name="AUTH_TYPE", default = "basic") }}'
basic_user = '{{ get_env(name="BASIC_USER", default = "konobangu") }}'
basic_password = '{{ get_env(name="BASIC_PASSWORD", default = "konobangu") }}'
oidc_issuer = '{{ get_env(name="OIDC_ISSUER", default = "") }}'
oidc_audience = '{{ get_env(name="OIDC_AUDIENCE", default = "") }}'
oidc_client_id = '{{ get_env(name="OIDC_CLIENT_ID", default = "") }}'
oidc_client_secret = '{{ get_env(name="OIDC_CLIENT_SECRET", default = "") }}'
oidc_extra_scopes = '{{ get_env(name="OIDC_EXTRA_SCOPES", default = "") }}'
oidc_extra_claim_key = '{{ get_env(name="OIDC_EXTRA_CLAIM_KEY", default = "") }}'
oidc_extra_claim_value = '{{ get_env(name="OIDC_EXTRA_CLAIM_VALUE", default = "") }}'
[graphql] [graphql]
# depth_limit = inf # depth_limit = inf
# complexity_limit = inf # complexity_limit = inf

View File

@ -1,8 +1,13 @@
use std::{fs, path::Path, str}; use std::{
collections::HashMap,
fs,
path::Path,
str::{self, FromStr},
};
use figment::{ use figment::{
Figment, Provider, Figment, Provider,
providers::{Format, Json, Toml, Yaml}, providers::{Env, Format, Json, Toml, Yaml},
}; };
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -65,6 +70,102 @@ impl AppConfig {
Toml::string(DEFAULT_CONFIG_MIXIN) Toml::string(DEFAULT_CONFIG_MIXIN)
} }
fn build_enhanced_tera_engine() -> tera::Tera {
let mut tera = tera::Tera::default();
tera.register_filter(
"cast_to",
|value: &tera::Value,
args: &HashMap<String, tera::Value>|
-> tera::Result<tera::Value> {
let target_type = args
.get("type")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("invalid target type: should be string"))?;
let target_type = TeraCastToFilterType::from_str(target_type)
.map_err(|e| tera::Error::msg(format!("invalid target type: {e}")))?;
let input_str = value.as_str().unwrap_or("");
match target_type {
TeraCastToFilterType::Boolean => {
let is_true = matches!(input_str.to_lowercase().as_str(), "true" | "1");
let is_false = matches!(input_str.to_lowercase().as_str(), "false" | "0");
if is_true {
Ok(tera::Value::Bool(true))
} else if is_false {
Ok(tera::Value::Bool(false))
} else {
Err(tera::Error::msg(
"target type is bool but value is not a boolean like true, false, \
1, 0",
))
}
}
TeraCastToFilterType::Integer => {
let parsed = input_str.parse::<i64>().map_err(|e| {
tera::Error::call_filter("invalid integer".to_string(), e)
})?;
Ok(tera::Value::Number(serde_json::Number::from(parsed)))
}
TeraCastToFilterType::Unsigned => {
let parsed = input_str.parse::<u64>().map_err(|e| {
tera::Error::call_filter("invalid unsigned integer".to_string(), e)
})?;
Ok(tera::Value::Number(serde_json::Number::from(parsed)))
}
TeraCastToFilterType::Float => {
let parsed = input_str.parse::<f64>().map_err(|e| {
tera::Error::call_filter("invalid float".to_string(), e)
})?;
Ok(tera::Value::Number(
serde_json::Number::from_f64(parsed).ok_or_else(|| {
tera::Error::msg("failed to convert f64 to serde_json::Number")
})?,
))
}
TeraCastToFilterType::String => Ok(tera::Value::String(input_str.to_string())),
TeraCastToFilterType::Null => Ok(tera::Value::Null),
}
},
);
tera.register_filter(
"try_auto_cast",
|value: &tera::Value,
_args: &HashMap<String, tera::Value>|
-> tera::Result<tera::Value> {
let input_str = value.as_str().unwrap_or("");
if input_str == "null" {
return Ok(tera::Value::Null);
}
if matches!(input_str, "true" | "false") {
return Ok(tera::Value::Bool(input_str == "true"));
}
if let Ok(parsed) = input_str.parse::<i64>() {
return Ok(tera::Value::Number(serde_json::Number::from(parsed)));
}
if let Ok(parsed) = input_str.parse::<u64>() {
return Ok(tera::Value::Number(serde_json::Number::from(parsed)));
}
if let Ok(parsed) = input_str.parse::<f64>() {
return Ok(tera::Value::Number(
serde_json::Number::from_f64(parsed).ok_or_else(|| {
tera::Error::msg("failed to convert f64 to serde_json::Number")
})?,
));
}
Ok(tera::Value::String(input_str.to_string()))
},
);
tera
}
pub fn merge_provider_from_file( pub fn merge_provider_from_file(
fig: Figment, fig: Figment,
filepath: impl AsRef<Path>, filepath: impl AsRef<Path>,
@ -72,11 +173,9 @@ impl AppConfig {
) -> RecorderResult<Figment> { ) -> RecorderResult<Figment> {
let content = fs::read_to_string(filepath)?; let content = fs::read_to_string(filepath)?;
let rendered = tera::Tera::one_off( let mut tera_engine = AppConfig::build_enhanced_tera_engine();
&content, let rendered =
&tera::Context::from_value(serde_json::json!({}))?, tera_engine.render_str(&content, &tera::Context::from_value(serde_json::json!({}))?)?;
false,
)?;
Ok(match ext { Ok(match ext {
".toml" => fig.merge(Toml::string(&rendered)), ".toml" => fig.merge(Toml::string(&rendered)),
@ -182,8 +281,43 @@ impl AppConfig {
} }
} }
fig = fig.merge(Env::prefixed("").split("__").lowercase(true));
let app_config: AppConfig = fig.extract()?; let app_config: AppConfig = fig.extract()?;
Ok(app_config) Ok(app_config)
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum TeraCastToFilterType {
#[serde(alias = "str")]
String,
#[serde(alias = "bool")]
Boolean,
#[serde(alias = "int")]
Integer,
#[serde(alias = "uint")]
Unsigned,
#[serde(alias = "float")]
Float,
#[serde(alias = "null")]
Null,
}
impl FromStr for TeraCastToFilterType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"string" | "str" => Ok(TeraCastToFilterType::String),
"boolean" | "bool" => Ok(TeraCastToFilterType::Boolean),
"integer" | "int" => Ok(TeraCastToFilterType::Integer),
"unsigned" | "uint" => Ok(TeraCastToFilterType::Unsigned),
"float" => Ok(TeraCastToFilterType::Float),
"null" => Ok(TeraCastToFilterType::Null),
_ => Err(format!("invalid target type: {s}")),
}
}
}

View File

@ -47,8 +47,27 @@ impl<'a> EpisodeComp<'a> {
Ok((input, f32::round(num) as i32)) Ok((input, f32::round(num) as i32))
} }
fn parse_ep_special_num(input: &'a str) -> IResult<&'a str, i32> {
terminated(
alt((
value(0, tag_no_case("ova")),
value(0, tag_no_case("oad")),
value(0, tag_no_case("sp")),
value(0, tag_no_case("ex")),
)),
(space0, opt(parse_int::<i32>)),
)
.parse(input)
}
fn parse_ep_num(input: &'a str) -> IResult<&'a str, i32> { fn parse_ep_num(input: &'a str) -> IResult<&'a str, i32> {
alt((parse_int::<i32>, Self::parse_ep_round_num, ZhNum::parse_int)).parse(input) alt((
parse_int::<i32>,
Self::parse_ep_round_num,
ZhNum::parse_int,
Self::parse_ep_special_num,
))
.parse(input)
} }
fn parse_ep_nums_core(input: &'a str) -> IResult<&'a str, (i32, Option<i32>)> { fn parse_ep_nums_core(input: &'a str) -> IResult<&'a str, (i32, Option<i32>)> {
@ -175,8 +194,13 @@ impl<'a> std::fmt::Debug for MoiveComp<'a> {
impl<'a> OriginCompTrait<'a> for MoiveComp<'a> { impl<'a> OriginCompTrait<'a> for MoiveComp<'a> {
#[cfg_attr(debug_assertions, instrument(level = Level::TRACE, ret, err(level=Level::TRACE), "MoiveComp::parse_comp"))] #[cfg_attr(debug_assertions, instrument(level = Level::TRACE, ret, err(level=Level::TRACE), "MoiveComp::parse_comp"))]
fn parse_comp(input: &'a str) -> IResult<&'a str, Self> { fn parse_comp(input: &'a str) -> IResult<&'a str, Self> {
let (input, source) = let (input, source) = alt((
alt((tag("剧场版"), tag("电影"), tag_no_case("movie"))).parse(input)?; tag("剧场版"),
tag("电影"),
tag_no_case("movie"),
tag_no_case("film"),
))
.parse(input)?;
Ok(( Ok((
input, input,
Self { Self {

View File

@ -0,0 +1,62 @@
use async_trait::async_trait;
use sea_orm_migration::prelude::*;
use crate::task::SUBSCRIBER_TASK_APALIS_NAME;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(&format!(
r#"CREATE OR REPLACE VIEW subscriber_tasks AS
SELECT
job,
job_type,
status,
(job ->> 'subscriber_id'::text)::integer AS subscriber_id,
job ->> 'task_type'::text AS task_type,
id,
attempts,
max_attempts,
run_at,
last_error,
lock_at,
lock_by,
done_at,
priority,
(job ->> 'subscription_id'::text)::integer AS subscription_id
FROM apalis.jobs
WHERE job_type = '{SUBSCRIBER_TASK_APALIS_NAME}'
AND jsonb_path_exists(job, '$.subscriber_id ? (@.type() == "number")')
AND jsonb_path_exists(job, '$.task_type ? (@.type() == "string")')"#,
))
.await?;
db.execute_unprepared(&format!(
r#"CREATE INDEX IF NOT EXISTS idx_apalis_jobs_subscription_id
ON apalis.jobs (((job -> 'subscription_id')::integer))
WHERE job_type = '{SUBSCRIBER_TASK_APALIS_NAME}'
AND jsonb_path_exists(job, '$.subscription_id ? (@.type() == "number")')
AND jsonb_path_exists(job, '$.task_type ? (@.type() == "string")')"#
))
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
r#"DROP INDEX IF EXISTS idx_apalis_jobs_subscription_id
ON apalis.jobs"#,
)
.await?;
Ok(())
}
}

View File

@ -10,6 +10,7 @@ pub mod m20250501_021523_credential_3rd;
pub mod m20250520_021135_subscriber_tasks; pub mod m20250520_021135_subscriber_tasks;
pub mod m20250622_015618_feeds; pub mod m20250622_015618_feeds;
pub mod m20250622_020819_bangumi_and_episode_type; pub mod m20250622_020819_bangumi_and_episode_type;
pub mod m20250625_060701_add_subscription_id_to_subscriber_tasks;
pub struct Migrator; pub struct Migrator;
@ -24,6 +25,7 @@ impl MigratorTrait for Migrator {
Box::new(m20250520_021135_subscriber_tasks::Migration), Box::new(m20250520_021135_subscriber_tasks::Migration),
Box::new(m20250622_015618_feeds::Migration), Box::new(m20250622_015618_feeds::Migration),
Box::new(m20250622_020819_bangumi_and_episode_type::Migration), Box::new(m20250622_020819_bangumi_and_episode_type::Migration),
Box::new(m20250625_060701_add_subscription_id_to_subscriber_tasks::Migration),
] ]
} }
} }

View File

@ -129,7 +129,7 @@ pub enum RelatedEntity {
} }
impl ActiveModel { impl ActiveModel {
#[tracing::instrument(err, skip(ctx), fields(bangumi_id = ?bangumi.id, mikan_episode_id = ?episode.mikan_episode_id))] #[tracing::instrument(err, skip_all, fields(bangumi_id = ?bangumi.id, mikan_episode_id = ?episode.mikan_episode_id))]
pub fn from_mikan_bangumi_and_episode_meta( pub fn from_mikan_bangumi_and_episode_meta(
ctx: &dyn AppContextTrait, ctx: &dyn AppContextTrait,
bangumi: &bangumi::Model, bangumi: &bangumi::Model,

View File

@ -1,5 +1,7 @@
use rss::Channel; use rss::Channel;
use sea_orm::{ColumnTrait, EntityTrait, JoinType, QueryFilter, QuerySelect, RelationTrait}; use sea_orm::{
ColumnTrait, EntityTrait, JoinType, Order, QueryFilter, QueryOrder, QuerySelect, RelationTrait,
};
use url::Url; use url::Url;
use crate::{ use crate::{
@ -37,6 +39,7 @@ impl Feed {
subscription_episode::Relation::Subscription.def(), subscription_episode::Relation::Subscription.def(),
) )
.filter(subscriptions::Column::Id.eq(subscription_id)) .filter(subscriptions::Column::Id.eq(subscription_id))
.order_by(episodes::Column::EnclosurePubDate, Order::Desc)
.all(db) .all(db)
.await?; .await?;
(subscription, episodes) (subscription, episodes)

View File

@ -29,6 +29,7 @@ pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: String, pub id: String,
pub subscriber_id: i32, pub subscriber_id: i32,
pub subscription_id: Option<i32>,
pub job: SubscriberTask, pub job: SubscriberTask,
pub task_type: SubscriberTaskType, pub task_type: SubscriberTaskType,
pub status: SubscriberTaskStatus, pub status: SubscriberTaskStatus,
@ -52,6 +53,14 @@ pub enum Relation {
on_delete = "Cascade" on_delete = "Cascade"
)] )]
Subscriber, Subscriber,
#[sea_orm(
belongs_to = "super::subscriptions::Entity",
from = "Column::SubscriptionId",
to = "super::subscriptions::Column::Id",
on_update = "NoAction",
on_delete = "NoAction"
)]
Subscription,
} }
impl Related<super::subscribers::Entity> for Entity { impl Related<super::subscribers::Entity> for Entity {
@ -60,10 +69,18 @@ impl Related<super::subscribers::Entity> for Entity {
} }
} }
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscription.def()
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity { pub enum RelatedEntity {
#[sea_orm(entity = "super::subscribers::Entity")] #[sea_orm(entity = "super::subscribers::Entity")]
Subscriber, Subscriber,
#[sea_orm(entity = "super::subscriptions::Entity")]
Subscription,
} }
#[async_trait] #[async_trait]

View File

@ -61,6 +61,8 @@ pub enum Relation {
Credential3rd, Credential3rd,
#[sea_orm(has_many = "super::feeds::Entity")] #[sea_orm(has_many = "super::feeds::Entity")]
Feed, Feed,
#[sea_orm(has_many = "super::subscriber_tasks::Entity")]
SubscriberTask,
} }
impl Related<super::subscribers::Entity> for Entity { impl Related<super::subscribers::Entity> for Entity {
@ -121,6 +123,12 @@ impl Related<super::credential_3rd::Entity> for Entity {
} }
} }
impl Related<super::subscriber_tasks::Entity> for Entity {
fn to() -> RelationDef {
Relation::SubscriberTask.def()
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity { pub enum RelatedEntity {
#[sea_orm(entity = "super::subscribers::Entity")] #[sea_orm(entity = "super::subscribers::Entity")]
@ -137,6 +145,8 @@ pub enum RelatedEntity {
Credential3rd, Credential3rd,
#[sea_orm(entity = "super::feeds::Entity")] #[sea_orm(entity = "super::feeds::Entity")]
Feed, Feed,
#[sea_orm(entity = "super::subscriber_tasks::Entity")]
SubscriberTask,
} }
#[async_trait] #[async_trait]

View File

@ -209,7 +209,7 @@ impl StorageService {
lister.try_collect().await lister.try_collect().await
} }
#[instrument(skip_all, err, fields(storage_path = %storage_path.as_ref(), range = ?range, accept = ?accept))] #[instrument(skip_all, err, fields(storage_path = %storage_path.as_ref(), range = ?range, accept = accept.to_string()))]
pub async fn serve_optimized_image( pub async fn serve_optimized_image(
&self, &self,
storage_path: impl AsRef<Path>, storage_path: impl AsRef<Path>,

View File

@ -1,8 +1,8 @@
AUTH_TYPE = "basic" # or oidc AUTH__AUTH_TYPE = "basic" # or oidc
BASIC_USER = "konobangu" AUTH__BASIC_USER = "konobangu"
BASIC_PASSWORD = "konobangu" AUTH__BASIC_PASSWORD = "konobangu"
# OIDC_ISSUER="https://auth.logto.io/oidc" # AUTH__OIDC_ISSUER="https://auth.logto.io/oidc"
# OIDC_AUDIENCE = "https://konobangu.com/api" # AUTH__OIDC_AUDIENCE = "https://konobangu.com/api"
# OIDC_CLIENT_ID = "client_id" # AUTH__OIDC_CLIENT_ID = "client_id"
# OIDC_CLIENT_SECRET = "client_secret" # optional # AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional
# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" # AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"

View File

@ -1,6 +1,6 @@
AUTH_TYPE = "basic" # or oidc AUTH__AUTH_TYPE = "basic" # or oidc
# OIDC_ISSUER="https://auth.logto.io/oidc" # AUTH__OIDC_ISSUER="https://auth.logto.io/oidc"
# OIDC_AUDIENCE = "https://konobangu.com/api" # AUTH__OIDC_AUDIENCE = "https://konobangu.com/api"
# OIDC_CLIENT_ID = "client_id" # AUTH__OIDC_CLIENT_ID = "client_id"
# OIDC_CLIENT_SECRET = "client_secret" # optional # AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional
# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" # AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"

View File

@ -20,15 +20,23 @@ export default defineConfig({
index: './src/main.tsx', index: './src/main.tsx',
}, },
define: { define: {
'process.env.AUTH_TYPE': JSON.stringify(process.env.AUTH_TYPE), 'process.env.AUTH__AUTH_TYPE': JSON.stringify(
'process.env.OIDC_CLIENT_ID': JSON.stringify(process.env.OIDC_CLIENT_ID), process.env.AUTH__AUTH_TYPE
'process.env.OIDC_CLIENT_SECRET': JSON.stringify(
process.env.OIDC_CLIENT_SECRET
), ),
'process.env.OIDC_ISSUER': JSON.stringify(process.env.OIDC_ISSUER), 'process.env.AUTH__OIDC_CLIENT_ID': JSON.stringify(
'process.env.OIDC_AUDIENCE': JSON.stringify(process.env.OIDC_AUDIENCE), process.env.AUTH__OIDC_CLIENT_ID
'process.env.OIDC_EXTRA_SCOPES': JSON.stringify( ),
process.env.OIDC_EXTRA_SCOPES 'process.env.AUTH__OIDC_CLIENT_SECRET': JSON.stringify(
process.env.AUTH__OIDC_CLIENT_SECRET
),
'process.env.AUTH__OIDC_ISSUER': JSON.stringify(
process.env.AUTH__OIDC_ISSUER
),
'process.env.AUTH__OIDC_AUDIENCE': JSON.stringify(
process.env.AUTH__OIDC_AUDIENCE
),
'process.env.AUTH__OIDC_EXTRA_SCOPES': JSON.stringify(
process.env.AUTH__OIDC_EXTRA_SCOPES
), ),
}, },
}, },
@ -39,7 +47,7 @@ export default defineConfig({
setupMiddlewares: [ setupMiddlewares: [
(middlewares) => { (middlewares) => {
middlewares.unshift((req, res, next) => { middlewares.unshift((req, res, next) => {
if (process.env.AUTH_TYPE === 'basic') { if (process.env.AUTH__AUTH_TYPE === 'basic') {
res.setHeader('WWW-Authenticate', 'Basic realm="konobangu"'); res.setHeader('WWW-Authenticate', 'Basic realm="konobangu"');
const authorization = const authorization =
@ -49,8 +57,8 @@ export default defineConfig({
.split(':'); .split(':');
if ( if (
user !== process.env.BASIC_USER || user !== process.env.AUTH__BASIC_USER ||
password !== process.env.BASIC_PASSWORD password !== process.env.AUTH__BASIC_PASSWORD
) { ) {
res.statusCode = 401; res.statusCode = 401;
res.write('Unauthorized'); res.write('Unauthorized');

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { MoreHorizontal } from "lucide-react"; import { Eye, MoreHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -18,7 +18,7 @@ import { ComponentProps, PropsWithChildren } from "react";
interface DropdownMenuActionsProps<Id> interface DropdownMenuActionsProps<Id>
extends ComponentProps<typeof DropdownMenuPrimitive.Root> { extends ComponentProps<typeof DropdownMenuPrimitive.Root> {
id: Id; id: Id;
showDetail?: boolean; showDetail?: boolean | "dropdown-menu";
showEdit?: boolean; showEdit?: boolean;
showDelete?: boolean; showDelete?: boolean;
onDetail?: (id: Id) => void; onDetail?: (id: Id) => void;
@ -38,34 +38,49 @@ export function DropdownMenuActions<Id>({
...rest ...rest
}: PropsWithChildren<DropdownMenuActionsProps<Id>>) { }: PropsWithChildren<DropdownMenuActionsProps<Id>>) {
return ( return (
<DropdownMenu {...rest}> <div className="flex gap-2 items-center justify-center">
<DropdownMenuTrigger asChild> {showDetail === true && (
<Button <Button
variant="ghost" variant="ghost"
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
onClick={() => onDetail?.(id)}
> >
<MoreHorizontal /> <Eye />
<span className="sr-only">Open menu</span> <span className="sr-only">Detail</span>
</Button> </Button>
</DropdownMenuTrigger> )}
<DropdownMenuContent align="end" className="w-[160px]"> <DropdownMenu {...rest}>
{children} <DropdownMenuTrigger asChild>
{showDetail && ( <Button
<DropdownMenuItem onClick={() => onDetail?.(id)}> variant="ghost"
Detail className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
</DropdownMenuItem> >
)} <MoreHorizontal />
{showEdit && ( <span className="sr-only">Open menu</span>
<DropdownMenuItem onClick={() => onEdit?.(id)}>Edit</DropdownMenuItem> </Button>
)} </DropdownMenuTrigger>
{(showDetail || showEdit) && showDelete && <DropdownMenuSeparator />} <DropdownMenuContent align="end" className="w-[160px]">
{showDelete && ( {children}
<DropdownMenuItem onClick={() => onDelete?.(id)}> {showDetail === "dropdown-menu" && (
Delete <DropdownMenuItem onClick={() => onDetail?.(id)}>
<DropdownMenuShortcut></DropdownMenuShortcut> Detail
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</DropdownMenuContent> {showEdit && (
</DropdownMenu> <DropdownMenuItem onClick={() => onEdit?.(id)}>
Edit
</DropdownMenuItem>
)}
{(showDetail === "dropdown-menu" || showEdit || children) &&
showDelete && <DropdownMenuSeparator />}
{showDelete && (
<DropdownMenuItem onClick={() => onDelete?.(id)}>
Delete
<DropdownMenuShortcut></DropdownMenuShortcut>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
); );
} }

View File

@ -105,6 +105,13 @@ query GetSubscriptionDetail ($id: Int!) {
feedSource feedSource
} }
} }
subscriberTask {
nodes {
id
taskType
status
}
}
credential3rd { credential3rd {
id id
username username

View File

@ -8,5 +8,5 @@ export const AUTH_METHOD = {
export type AuthMethodType = ValueOf<typeof AUTH_METHOD>; export type AuthMethodType = ValueOf<typeof AUTH_METHOD>;
export function getAppAuthMethod(): AuthMethodType { export function getAppAuthMethod(): AuthMethodType {
return process.env.AUTH_TYPE as AuthMethodType; return process.env.AUTH__AUTH_TYPE as AuthMethodType;
} }

View File

@ -3,16 +3,16 @@ import { LogLevel, type OpenIdConfiguration } from 'oidc-client-rx';
export function buildOidcConfig(): OpenIdConfiguration { export function buildOidcConfig(): OpenIdConfiguration {
const origin = window.location.origin; const origin = window.location.origin;
const resource = process.env.OIDC_AUDIENCE!; const resource = process.env.AUTH__OIDC_AUDIENCE!;
return { return {
authority: process.env.OIDC_ISSUER!, authority: process.env.AUTH__OIDC_ISSUER!,
redirectUrl: `${origin}/auth/oidc/callback`, redirectUrl: `${origin}/auth/oidc/callback`,
postLogoutRedirectUri: `${origin}/`, postLogoutRedirectUri: `${origin}/`,
clientId: process.env.OIDC_CLIENT_ID!, clientId: process.env.AUTH__OIDC_CLIENT_ID!,
clientSecret: process.env.OIDC_CLIENT_SECRET, clientSecret: process.env.AUTH__OIDC_CLIENT_SECRET,
scope: process.env.OIDC_EXTRA_SCOPES scope: process.env.AUTH__OIDC_EXTRA_SCOPES
? `openid profile email offline_access ${process.env.OIDC_EXTRA_SCOPES}` ? `openid profile email offline_access ${process.env.AUTH__OIDC_EXTRA_SCOPES}`
: 'openid profile email offline_access', : 'openid profile email offline_access',
triggerAuthorizationResultEvent: true, triggerAuthorizationResultEvent: true,
responseType: 'code', responseType: 'code',

View File

@ -26,7 +26,7 @@ type Documents = {
"\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 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 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, "\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 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, "\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 subscriberTask {\n nodes {\n id\n taskType\n status\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 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 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, "\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionSourcesDocument,
@ -47,7 +47,7 @@ const documents: Documents = {
"\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 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 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, "\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 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, "\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 subscriberTask {\n nodes {\n id\n taskType\n status\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 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 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, "\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionSourcesDocument,
@ -121,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. * 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 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"]; 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 subscriberTask {\n nodes {\n id\n taskType\n status\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 subscriberTask {\n nodes {\n id\n taskType\n status\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. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

View File

@ -1441,6 +1441,8 @@ export type SubscriberTasks = {
status: SubscriberTaskStatusEnum; status: SubscriberTaskStatusEnum;
subscriber?: Maybe<Subscribers>; subscriber?: Maybe<Subscribers>;
subscriberId: Scalars['Int']['output']; subscriberId: Scalars['Int']['output'];
subscription?: Maybe<Subscriptions>;
subscriptionId?: Maybe<Scalars['Int']['output']>;
taskType: SubscriberTaskTypeEnum; taskType: SubscriberTaskTypeEnum;
}; };
@ -1473,6 +1475,7 @@ export type SubscriberTasksFilterInput = {
runAt?: InputMaybe<TextFilterInput>; runAt?: InputMaybe<TextFilterInput>;
status?: InputMaybe<StringFilterInput>; status?: InputMaybe<StringFilterInput>;
subscriberId?: InputMaybe<SubscriberIdFilterInput>; subscriberId?: InputMaybe<SubscriberIdFilterInput>;
subscriptionId?: InputMaybe<IntegerFilterInput>;
taskType?: InputMaybe<StringFilterInput>; taskType?: InputMaybe<StringFilterInput>;
}; };
@ -1489,6 +1492,7 @@ export type SubscriberTasksOrderInput = {
runAt?: InputMaybe<OrderByEnum>; runAt?: InputMaybe<OrderByEnum>;
status?: InputMaybe<OrderByEnum>; status?: InputMaybe<OrderByEnum>;
subscriberId?: InputMaybe<OrderByEnum>; subscriberId?: InputMaybe<OrderByEnum>;
subscriptionId?: InputMaybe<OrderByEnum>;
taskType?: InputMaybe<OrderByEnum>; taskType?: InputMaybe<OrderByEnum>;
}; };
@ -1745,6 +1749,7 @@ export type Subscriptions = {
sourceUrl: Scalars['String']['output']; sourceUrl: Scalars['String']['output'];
subscriber?: Maybe<Subscribers>; subscriber?: Maybe<Subscribers>;
subscriberId: Scalars['Int']['output']; subscriberId: Scalars['Int']['output'];
subscriberTask: SubscriberTasksConnection;
subscriptionBangumi: SubscriptionBangumiConnection; subscriptionBangumi: SubscriptionBangumiConnection;
subscriptionEpisode: SubscriptionEpisodeConnection; subscriptionEpisode: SubscriptionEpisodeConnection;
updatedAt: Scalars['String']['output']; updatedAt: Scalars['String']['output'];
@ -1772,6 +1777,13 @@ export type SubscriptionsFeedArgs = {
}; };
export type SubscriptionsSubscriberTaskArgs = {
filters?: InputMaybe<SubscriberTasksFilterInput>;
orderBy?: InputMaybe<SubscriberTasksOrderInput>;
pagination?: InputMaybe<PaginationInput>;
};
export type SubscriptionsSubscriptionBangumiArgs = { export type SubscriptionsSubscriptionBangumiArgs = {
filters?: InputMaybe<SubscriptionBangumiFilterInput>; filters?: InputMaybe<SubscriptionBangumiFilterInput>;
orderBy?: InputMaybe<SubscriptionBangumiOrderInput>; orderBy?: InputMaybe<SubscriptionBangumiOrderInput>;
@ -1971,7 +1983,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, 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 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 }> }, subscriberTask: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum }> }, 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<{ export type SyncSubscriptionFeedsIncrementalMutationVariables = Exact<{
filter: SubscriptionsFilterInput; filter: SubscriptionsFilterInput;
@ -2030,7 +2042,7 @@ export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind
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<InsertSubscriptionMutation, InsertSubscriptionMutationVariables>; 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<InsertSubscriptionMutation, InsertSubscriptionMutationVariables>;
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<UpdateSubscriptionsMutation, UpdateSubscriptionsMutationVariables>; 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<UpdateSubscriptionsMutation, UpdateSubscriptionsMutationVariables>;
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<DeleteSubscriptionsMutation, DeleteSubscriptionsMutationVariables>; 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<DeleteSubscriptionsMutation, DeleteSubscriptionsMutationVariables>;
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<GetSubscriptionDetailQuery, GetSubscriptionDetailQueryVariables>; 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":"subscriberTask"},"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":"taskType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}},{"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<GetSubscriptionDetailQuery, GetSubscriptionDetailQueryVariables>;
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<SyncSubscriptionFeedsIncrementalMutation, SyncSubscriptionFeedsIncrementalMutationVariables>; 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<SyncSubscriptionFeedsIncrementalMutation, SyncSubscriptionFeedsIncrementalMutationVariables>;
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<SyncSubscriptionFeedsFullMutation, SyncSubscriptionFeedsFullMutationVariables>; 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<SyncSubscriptionFeedsFullMutation, SyncSubscriptionFeedsFullMutationVariables>;
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<SyncSubscriptionSourcesMutation, SyncSubscriptionSourcesMutationVariables>; 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<SyncSubscriptionSourcesMutation, SyncSubscriptionSourcesMutationVariables>;

View File

@ -51,6 +51,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { prettyTaskType } from '../tasks/-pretty-task-type';
import { SubscriptionSyncDialogContent } from './-sync'; import { SubscriptionSyncDialogContent } from './-sync';
export const Route = createFileRoute('/_app/subscriptions/detail/$id')({ export const Route = createFileRoute('/_app/subscriptions/detail/$id')({
@ -212,18 +213,6 @@ function SubscriptionDetailRouteComponent() {
</CardDescription> </CardDescription>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<RefreshCcwIcon className="h-4 w-4" />
Sync
</Button>
</DialogTrigger>
<SubscriptionSyncDialogContent
id={subscription.id}
onCancel={handleReload}
/>
</Dialog>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@ -446,6 +435,64 @@ function SubscriptionDetailRouteComponent() {
</div> </div>
</div> </div>
<Separator />
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="font-medium text-sm">Associated Tasks</Label>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<RefreshCcwIcon className="h-4 w-4" />
Sync
</Button>
</DialogTrigger>
<SubscriptionSyncDialogContent
id={subscription.id}
onCancel={handleReload}
/>
</Dialog>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{subscription.subscriberTask?.nodes &&
subscription.subscriberTask.nodes.length > 0 ? (
subscription.subscriberTask.nodes.map((task) => (
<Card
key={task.id}
className="group relative cursor-pointer p-4 transition-colors hover:bg-accent/50"
onClick={() =>
navigate({
to: '/tasks/detail/$id',
params: {
id: task.id,
},
})
}
>
<div className="flex flex-col space-y-2">
<div className="flex items-center justify-between">
<Label className="font-medium text-sm capitalize">
<span>{prettyTaskType(task.taskType)} Task</span>
</Label>
</div>
<code className="break-all rounded bg-muted px-2 py-1 font-mono text-xs">
{task.id}
</code>
<div className="text-muted-foreground text-xs">
{task.status}
</div>
</div>
</Card>
))
) : (
<div className="col-span-full py-8 text-center text-muted-foreground">
No associated tasks now
</div>
)}
</div>
</div>
{subscription.bangumi?.nodes && {subscription.bangumi?.nodes &&
subscription.bangumi.nodes.length > 0 && ( subscription.bangumi.nodes.length > 0 && (
<> <>
@ -465,6 +512,7 @@ function SubscriptionDetailRouteComponent() {
src={`/api/static${bangumi.posterLink}`} src={`/api/static${bangumi.posterLink}`}
alt="Poster" alt="Poster"
className="h-full w-full object-cover" className="h-full w-full object-cover"
loading="lazy"
/> />
)} )}
</div> </div>

View File

@ -0,0 +1,3 @@
export function prettyTaskType(taskType: string) {
return taskType.replace(/_/g, ' ');
}

View File

@ -33,6 +33,7 @@ import {
import { format } from 'date-fns'; import { format } from 'date-fns';
import { ArrowLeft, RefreshCw } from 'lucide-react'; import { ArrowLeft, RefreshCw } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { prettyTaskType } from './-pretty-task-type';
import { getStatusBadge } from './-status-badge'; import { getStatusBadge } from './-status-badge';
export const Route = createFileRoute('/_app/tasks/detail/$id')({ export const Route = createFileRoute('/_app/tasks/detail/$id')({
@ -182,7 +183,9 @@ function TaskDetailRouteComponent() {
<div className="space-y-2"> <div className="space-y-2">
<Label className="font-medium text-sm">Task Type</Label> <Label className="font-medium text-sm">Task Type</Label>
<div className="rounded-md bg-muted p-3"> <div className="rounded-md bg-muted p-3">
<Badge variant="secondary">{task.taskType}</Badge> <Badge variant="secondary" className="capitalize">
{prettyTaskType(task.taskType)}
</Badge>
</div> </div>
</div> </div>

View File

@ -42,6 +42,7 @@ import {
} from '@/infra/errors/apollo'; } from '@/infra/errors/apollo';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { prettyTaskType } from './-pretty-task-type';
import { getStatusBadge } from './-status-badge'; import { getStatusBadge } from './-status-badge';
export const Route = createFileRoute('/_app/tasks/manage')({ export const Route = createFileRoute('/_app/tasks/manage')({
@ -202,7 +203,9 @@ function TaskManageRouteComponent() {
# {task.id} # {task.id}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Badge variant="outline">{task.taskType}</Badge> <Badge variant="outline" className="capitalize">
{prettyTaskType(task.taskType)}
</Badge>
</div> </div>
</div> </div>
<div className="mt-1 flex items-center gap-2"> <div className="mt-1 flex items-center gap-2">

View File

@ -176,7 +176,7 @@ impl HttpClient {
let accept_invalid_certs = proxy let accept_invalid_certs = proxy
.accept_invalid_certs .accept_invalid_certs
.as_ref() .as_ref()
.map(|b| b.as_bool()) .map(|b| *b)
.unwrap_or_default(); .unwrap_or_default();
let proxy = proxy.clone().into_proxy()?; let proxy = proxy.clone().into_proxy()?;
if let Some(proxy) = proxy { if let Some(proxy) = proxy {
@ -307,7 +307,7 @@ impl HttpClient {
let accept_invalid_certs = proxy let accept_invalid_certs = proxy
.accept_invalid_certs .accept_invalid_certs
.as_ref() .as_ref()
.map(|b| b.as_bool()) .map(|b| *b)
.unwrap_or_default(); .unwrap_or_default();
let proxy = proxy.clone().into_proxy().unwrap_or_default(); let proxy = proxy.clone().into_proxy().unwrap_or_default();
if let Some(proxy) = proxy { if let Some(proxy) = proxy {

View File

@ -2,22 +2,24 @@ use axum::http::{HeaderMap, HeaderValue};
use reqwest::{NoProxy, Proxy}; use reqwest::{NoProxy, Proxy};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::{NoneAsEmptyString, serde_as}; use serde_with::{NoneAsEmptyString, serde_as};
use util::BooleanLike;
use crate::HttpClientError; use crate::HttpClientError;
#[serde_as] #[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpClientProxyConfig { pub struct HttpClientProxyConfig {
#[serde(default)]
#[serde_as(as = "NoneAsEmptyString")] #[serde_as(as = "NoneAsEmptyString")]
pub server: Option<String>, pub server: Option<String>,
#[serde(default)]
#[serde_as(as = "NoneAsEmptyString")] #[serde_as(as = "NoneAsEmptyString")]
pub auth_header: Option<String>, pub auth_header: Option<String>,
#[serde(with = "http_serde::option::header_map")] #[serde(with = "http_serde::option::header_map")]
pub headers: Option<HeaderMap>, pub headers: Option<HeaderMap>,
#[serde(default)]
#[serde_as(as = "NoneAsEmptyString")] #[serde_as(as = "NoneAsEmptyString")]
pub no_proxy: Option<String>, pub no_proxy: Option<String>,
pub accept_invalid_certs: Option<BooleanLike>, pub accept_invalid_certs: Option<bool>,
} }
impl HttpClientProxyConfig { impl HttpClientProxyConfig {

View File

@ -1,5 +1,3 @@
pub mod errors; pub mod errors;
pub mod loose;
pub use errors::OptDynErr; pub use errors::OptDynErr;
pub use loose::BooleanLike;

View File

@ -1,19 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BooleanLike {
Boolean(bool),
String(String),
Number(i32),
}
impl BooleanLike {
pub fn as_bool(&self) -> bool {
match self {
BooleanLike::Boolean(b) => *b,
BooleanLike::String(s) => s.to_lowercase() == "true",
BooleanLike::Number(n) => *n != 0,
}
}
}