feat: add basic graphql support

This commit is contained in:
master 2025-01-04 20:38:41 +08:00
parent caaa5dc0cc
commit 40cbf86f0f
62 changed files with 4053 additions and 675 deletions

40
Cargo.lock generated
View File

@ -220,18 +220,22 @@ dependencies = [
"async-trait", "async-trait",
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"chrono",
"fast_chemail", "fast_chemail",
"fnv", "fnv",
"futures-channel",
"futures-timer", "futures-timer",
"futures-util", "futures-util",
"handlebars", "handlebars",
"http 1.2.0", "http 1.2.0",
"indexmap 2.7.0", "indexmap 2.7.0",
"lru",
"mime", "mime",
"multer", "multer",
"num-traits", "num-traits",
"pin-project-lite", "pin-project-lite",
"regex", "regex",
"rust_decimal",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
@ -1760,6 +1764,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.3.2" version = "0.3.2"
@ -2071,6 +2081,11 @@ name = "hashbrown"
version = "0.15.2" version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]] [[package]]
name = "hashlink" name = "hashlink"
@ -3315,6 +3330,15 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lru"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.15.2",
]
[[package]] [[package]]
name = "lru-cache" name = "lru-cache"
version = "0.1.2" version = "0.1.2"
@ -4522,6 +4546,7 @@ dependencies = [
"chrono", "chrono",
"eyre", "eyre",
"fancy-regex", "fancy-regex",
"fastrand",
"figment", "figment",
"futures", "futures",
"html-escape", "html-escape",
@ -4548,6 +4573,7 @@ dependencies = [
"scraper", "scraper",
"sea-orm", "sea-orm",
"sea-orm-migration", "sea-orm-migration",
"seaography",
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",
@ -5286,6 +5312,20 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "seaography"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bca7168531927846a9da73b20e65aa36cc258b377035286e70ebb34874097b1"
dependencies = [
"async-graphql",
"fnv",
"heck 0.4.1",
"itertools 0.12.1",
"sea-orm",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.11.1" version = "2.11.1"

View File

@ -85,8 +85,10 @@ testcontainers-modules = { version = "0.11.4", optional = true }
log = "0.4.22" log = "0.4.22"
anyhow = "1.0.95" anyhow = "1.0.95"
bollard = { version = "0.18", optional = true } bollard = { version = "0.18", optional = true }
async-graphql = "7.0.13" async-graphql = { version = "7.0.13", features = [] }
async-graphql-axum = "7.0.13" async-graphql-axum = "7.0.13"
fastrand = "2.3.0"
seaography = "1.1.2"
[dev-dependencies] [dev-dependencies]

View File

@ -16,9 +16,10 @@ use sea_orm::DatabaseConnection;
use crate::{ use crate::{
auth::service::AppAuthService, auth::service::AppAuthService,
controllers, controllers::{self},
dal::{AppDalClient, AppDalInitalizer}, dal::{AppDalClient, AppDalInitalizer},
extract::mikan::{client::AppMikanClientInitializer, AppMikanClient}, extract::mikan::{client::AppMikanClientInitializer, AppMikanClient},
graphql::service::{AppGraphQLService, AppGraphQLServiceInitializer},
migrations::Migrator, migrations::Migrator,
models::subscribers, models::subscribers,
workers::subscription_worker::SubscriptionWorker, workers::subscription_worker::SubscriptionWorker,
@ -36,6 +37,10 @@ pub trait AppContextExt {
fn get_auth_service(&self) -> &AppAuthService { fn get_auth_service(&self) -> &AppAuthService {
AppAuthService::app_instance() AppAuthService::app_instance()
} }
fn get_graphql_service(&self) -> &AppGraphQLService {
AppGraphQLService::app_instance()
}
} }
impl AppContextExt for AppContext {} impl AppContextExt for AppContext {}
@ -52,6 +57,7 @@ impl Hooks for App {
let initializers: Vec<Box<dyn Initializer>> = vec![ let initializers: Vec<Box<dyn Initializer>> = vec![
Box::new(AppDalInitalizer), Box::new(AppDalInitalizer),
Box::new(AppMikanClientInitializer), Box::new(AppMikanClientInitializer),
Box::new(AppGraphQLServiceInitializer),
]; ];
Ok(initializers) Ok(initializers)
@ -71,10 +77,11 @@ impl Hooks for App {
create_app::<Self, Migrator>(mode, environment).await create_app::<Self, Migrator>(mode, environment).await
} }
fn routes(_ctx: &AppContext) -> AppRoutes { fn routes(ctx: &AppContext) -> AppRoutes {
AppRoutes::with_default_routes() AppRoutes::with_default_routes()
.prefix("/api") .prefix("/api")
.add_route(controllers::subscribers::routes()) .add_route(controllers::subscribers::routes())
.add_route(controllers::graphql::routes(ctx.get_graphql_service()))
} }
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> { async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use axum::{http::request::Parts, RequestPartsExt}; use axum::{http::request::Parts, RequestPartsExt};
use axum_auth::AuthBasic; use axum_auth::AuthBasic;
@ -13,7 +14,7 @@ pub struct BasicAuthService {
pub config: BasicAuthConfig, pub config: BasicAuthConfig,
} }
#[async_trait::async_trait] #[async_trait]
impl AuthService for BasicAuthService { impl AuthService for BasicAuthService {
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> { async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
if let Ok(AuthBasic((found_user, found_password))) = request.extract().await { if let Ok(AuthBasic((found_user, found_password))) = request.extract().await {

View File

@ -1,5 +1,6 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use async_trait::async_trait;
use axum::http::request::Parts; use axum::http::request::Parts;
use itertools::Itertools; use itertools::Itertools;
use jwt_authorizer::{authorizer::Authorizer, NumericDate, OneOrArray}; use jwt_authorizer::{authorizer::Authorizer, NumericDate, OneOrArray};
@ -80,7 +81,7 @@ pub struct OidcAuthService {
pub authorizer: Authorizer<OidcAuthClaims>, pub authorizer: Authorizer<OidcAuthClaims>,
} }
#[async_trait::async_trait] #[async_trait]
impl AuthService for OidcAuthService { impl AuthService for OidcAuthService {
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> { async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
let config = &self.config; let config = &self.config;

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use axum::{ use axum::{
extract::FromRequestParts, extract::FromRequestParts,
http::request::Parts, http::request::Parts,
@ -21,7 +22,7 @@ pub struct AuthUserInfo {
pub auth_type: AuthType, pub auth_type: AuthType,
} }
#[async_trait::async_trait] #[async_trait]
impl<S> FromRequestParts<S> for AuthUserInfo impl<S> FromRequestParts<S> for AuthUserInfo
where where
S: Send + Sync, S: Send + Sync,
@ -42,7 +43,7 @@ where
} }
} }
#[async_trait::async_trait] #[async_trait]
pub trait AuthService { pub trait AuthService {
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError>; async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError>;
} }
@ -84,7 +85,7 @@ impl AppAuthService {
} }
} }
#[async_trait::async_trait] #[async_trait]
impl AuthService for AppAuthService { impl AuthService for AppAuthService {
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> { async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
match self { match self {
@ -96,7 +97,7 @@ impl AuthService for AppAuthService {
pub struct AppAuthServiceInitializer; pub struct AppAuthServiceInitializer;
#[async_trait::async_trait] #[async_trait]
impl Initializer for AppAuthServiceInitializer { impl Initializer for AppAuthServiceInitializer {
fn name(&self) -> String { fn name(&self) -> String {
String::from("AppAuthServiceInitializer") String::from("AppAuthServiceInitializer")

View File

@ -4,7 +4,10 @@ use figment::{
}; };
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::{auth::AppAuthConfig, dal::config::AppDalConfig, extract::mikan::AppMikanConfig}; use crate::{
auth::AppAuthConfig, dal::config::AppDalConfig, extract::mikan::AppMikanConfig,
graphql::config::AppGraphQLConfig,
};
const DEFAULT_APP_SETTINGS_MIXIN: &str = include_str!("./settings_mixin.yaml"); const DEFAULT_APP_SETTINGS_MIXIN: &str = include_str!("./settings_mixin.yaml");
@ -13,6 +16,7 @@ pub struct AppConfig {
pub auth: AppAuthConfig, pub auth: AppAuthConfig,
pub dal: AppDalConfig, pub dal: AppDalConfig,
pub mikan: AppMikanConfig, pub mikan: AppMikanConfig,
pub graphql: AppGraphQLConfig,
} }
pub fn deserialize_key_path_from_json_value<T: DeserializeOwned>( pub fn deserialize_key_path_from_json_value<T: DeserializeOwned>(

View File

@ -4,9 +4,12 @@ dal:
mikan: mikan:
http_client: http_client:
exponential_backoff_max_retries: 3 exponential_backoff_max_retries: 3
leaky_bucket_max_tokens: 3 leaky_bucket_max_tokens: 2
leaky_bucket_initial_tokens: 0 leaky_bucket_initial_tokens: 0
leaky_bucket_refill_tokens: 1 leaky_bucket_refill_tokens: 1
leaky_bucket_refill_interval: 500 leaky_bucket_refill_interval: 500
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
base_url: "https://mikanani.me/" base_url: "https://mikanani.me/"
graphql:
depth_limit: null
complexity_limit: null

View File

@ -0,0 +1,19 @@
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
use async_graphql_axum::GraphQL;
use axum::response::Html;
use loco_rs::prelude::*;
use crate::graphql::service::AppGraphQLService;
pub async fn graphql_playground() -> impl IntoResponse {
Html(playground_source(GraphQLPlaygroundConfig::new(
"/api/graphql",
)))
}
pub fn routes(graphql_service: &AppGraphQLService) -> Routes {
Routes::new().prefix("/graphql").add(
"/",
get(graphql_playground).post_service(GraphQL::new(graphql_service.schema.clone())),
)
}

View File

@ -1 +1,2 @@
pub mod graphql;
pub mod subscribers; pub mod subscribers;

View File

@ -1,5 +1,6 @@
use std::fmt; use std::fmt;
use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use loco_rs::app::{AppContext, Initializer}; use loco_rs::app::{AppContext, Initializer};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -184,7 +185,7 @@ impl AppDalClient {
pub struct AppDalInitalizer; pub struct AppDalInitalizer;
#[async_trait::async_trait] #[async_trait]
impl Initializer for AppDalInitalizer { impl Initializer for AppDalInitalizer {
fn name(&self) -> String { fn name(&self) -> String {
String::from("AppDalInitalizer") String::from("AppDalInitalizer")

View File

@ -1,5 +1,6 @@
use std::ops::Deref; use std::ops::Deref;
use async_trait::async_trait;
use loco_rs::app::{AppContext, Initializer}; use loco_rs::app::{AppContext, Initializer};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -45,7 +46,7 @@ impl Deref for AppMikanClient {
pub struct AppMikanClientInitializer; pub struct AppMikanClientInitializer;
#[async_trait::async_trait] #[async_trait]
impl Initializer for AppMikanClientInitializer { impl Initializer for AppMikanClientInitializer {
fn name(&self) -> String { fn name(&self) -> String {
"AppMikanClientInitializer".to_string() "AppMikanClientInitializer".to_string()

View File

@ -6,6 +6,12 @@ use super::HttpClient;
pub async fn fetch_bytes<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<Bytes> { pub async fn fetch_bytes<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<Bytes> {
let client = client.unwrap_or_default(); let client = client.unwrap_or_default();
let bytes = client.get(url).send().await?.bytes().await?; let bytes = client
.get(url)
.send()
.await?
.error_for_status()?
.bytes()
.await?;
Ok(bytes) Ok(bytes)
} }

View File

@ -11,8 +11,9 @@ use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use reqwest_tracing::TracingMiddleware; use reqwest_tracing::TracingMiddleware;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::serde_as; use serde_with::serde_as;
use async_trait::async_trait;
use crate::fetch::DEFAULT_HTTP_CLIENT_USER_AGENT; use super::get_random_mobile_ua;
#[serde_as] #[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
@ -31,9 +32,9 @@ pub struct HttpClient {
pub config: HttpClientConfig, pub config: HttpClientConfig,
} }
impl Into<ClientWithMiddleware> for HttpClient { impl From<HttpClient> for ClientWithMiddleware {
fn into(self) -> ClientWithMiddleware { fn from(val: HttpClient) -> Self {
self.client val.client
} }
} }
@ -49,7 +50,7 @@ pub struct RateLimiterMiddleware {
rate_limiter: RateLimiter, rate_limiter: RateLimiter,
} }
#[async_trait::async_trait] #[async_trait]
impl reqwest_middleware::Middleware for RateLimiterMiddleware { impl reqwest_middleware::Middleware for RateLimiterMiddleware {
async fn handle( async fn handle(
&self, &self,
@ -68,7 +69,7 @@ impl HttpClient {
config config
.user_agent .user_agent
.as_deref() .as_deref()
.unwrap_or(DEFAULT_HTTP_CLIENT_USER_AGENT), .unwrap_or_else(|| get_random_mobile_ua()),
); );
let reqwest_client = reqwest_client_builder.build()?; let reqwest_client = reqwest_client_builder.build()?;

View File

@ -1 +1,11 @@
pub const DEFAULT_HTTP_CLIENT_USER_AGENT: &str = "Wget/1.13.4 (linux-gnu)"; use lazy_static::lazy_static;
lazy_static! {
static ref DEFAULT_HTTP_CLIENT_USER_AGENT: Vec<String> =
serde_json::from_str::<Vec<String>>(include_str!("./ua.json")).unwrap();
}
pub fn get_random_mobile_ua() -> &'static str {
DEFAULT_HTTP_CLIENT_USER_AGENT[fastrand::usize(0..DEFAULT_HTTP_CLIENT_USER_AGENT.len())]
.as_str()
}

View File

@ -4,7 +4,13 @@ use super::HttpClient;
pub async fn fetch_html<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<String> { pub async fn fetch_html<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<String> {
let client = client.unwrap_or_default(); let client = client.unwrap_or_default();
let content = client.get(url).send().await?.text().await?; let content = client
.get(url)
.send()
.await?
.error_for_status()?
.text()
.await?;
Ok(content) Ok(content)
} }

View File

@ -4,7 +4,7 @@ pub mod core;
pub mod html; pub mod html;
pub mod image; pub mod image;
pub use core::DEFAULT_HTTP_CLIENT_USER_AGENT; pub use core::get_random_mobile_ua;
pub use bytes::fetch_bytes; pub use bytes::fetch_bytes;
pub use client::{HttpClient, HttpClientConfig}; pub use client::{HttpClient, HttpClientConfig};

View File

@ -0,0 +1,15 @@
[
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.1",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.3",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Herring/97.1.8280.8",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 AtContent/95.5.5462.5",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.3",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.3"
]

View File

@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppGraphQLConfig {
pub depth_limit: Option<usize>,
pub complexity_limit: Option<usize>,
}

View File

@ -0,0 +1,5 @@
pub mod query_root;
pub mod service;
pub mod config;
pub use query_root::schema;

View File

@ -0,0 +1,53 @@
use async_graphql::dynamic::*;
use sea_orm::DatabaseConnection;
use seaography::{Builder, BuilderContext};
lazy_static::lazy_static! { static ref CONTEXT : BuilderContext = BuilderContext :: default () ; }
pub fn schema(
database: DatabaseConnection,
depth: Option<usize>,
complexity: Option<usize>,
) -> Result<Schema, SchemaError> {
use crate::models::*;
let mut builder = Builder::new(&CONTEXT, database.clone());
seaography::register_entities!(
builder,
[
auth,
bangumi,
downloaders,
downloads,
episodes,
subscribers,
subscription_bangumi,
subscription_episode,
subscriptions
]
);
{
builder.register_enumeration::<auth::AuthType>();
builder.register_enumeration::<downloads::DownloadStatus>();
builder.register_enumeration::<subscriptions::SubscriptionCategory>();
builder.register_enumeration::<downloaders::DownloaderCategory>();
builder.register_enumeration::<downloads::DownloadMime>();
}
let schema = builder.schema_builder();
let schema = if let Some(depth) = depth {
schema.limit_depth(depth)
} else {
schema
};
let schema = if let Some(complexity) = complexity {
schema.limit_complexity(complexity)
} else {
schema
};
schema
.data(database)
.finish()
.inspect_err(|e| tracing::error!(e = ?e))
}

View File

@ -0,0 +1,51 @@
use async_graphql::dynamic::{Schema, SchemaError};
use async_trait::async_trait;
use loco_rs::app::{AppContext, Initializer};
use once_cell::sync::OnceCell;
use sea_orm::DatabaseConnection;
use super::{config::AppGraphQLConfig, query_root};
use crate::config::AppConfigExt;
static APP_GRAPHQL_SERVICE: OnceCell<AppGraphQLService> = OnceCell::new();
#[derive(Debug)]
pub struct AppGraphQLService {
pub schema: Schema,
}
impl AppGraphQLService {
pub fn new(config: AppGraphQLConfig, db: DatabaseConnection) -> Result<Self, SchemaError> {
let schema = query_root::schema(db, config.depth_limit, config.complexity_limit)?;
Ok(Self { schema })
}
pub fn app_instance() -> &'static Self {
APP_GRAPHQL_SERVICE
.get()
.expect("AppGraphQLService is not initialized")
}
}
#[derive(Debug, Clone)]
pub struct AppGraphQLServiceInitializer;
#[async_trait]
impl Initializer for AppGraphQLServiceInitializer {
fn name(&self) -> String {
String::from("AppGraphQLServiceInitializer")
}
async fn before_run(&self, app_context: &AppContext) -> loco_rs::Result<()> {
APP_GRAPHQL_SERVICE.get_or_try_init(|| {
let config = app_context
.config
.get_app_conf()
.map_err(loco_rs::Error::wrap)?
.graphql;
let db = &app_context.db;
AppGraphQLService::new(config, db.clone()).map_err(loco_rs::Error::wrap)
})?;
Ok(())
}
}

View File

@ -7,6 +7,7 @@ pub mod controllers;
pub mod dal; pub mod dal;
pub mod extract; pub mod extract;
pub mod fetch; pub mod fetch;
pub mod graphql;
pub mod migrations; pub mod migrations;
pub mod models; pub mod models;
pub mod sync; pub mod sync;

View File

@ -1,5 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use async_trait::async_trait;
use sea_orm::{DeriveIden, Statement}; use sea_orm::{DeriveIden, Statement};
use sea_orm_migration::prelude::{extension::postgres::IntoTypeRef, *}; use sea_orm_migration::prelude::{extension::postgres::IntoTypeRef, *};
@ -143,7 +144,7 @@ macro_rules! create_postgres_enum_for_active_enum {
}; };
} }
#[async_trait::async_trait] #[async_trait]
pub trait CustomSchemaManagerExt { pub trait CustomSchemaManagerExt {
async fn create_postgres_auto_update_ts_fn(&self, col_name: &str) -> Result<(), DbErr>; async fn create_postgres_auto_update_ts_fn(&self, col_name: &str) -> Result<(), DbErr>;
async fn create_postgres_auto_update_ts_fn_for_col<C: IntoIden + 'static + Send>( async fn create_postgres_auto_update_ts_fn_for_col<C: IntoIden + 'static + Send>(
@ -250,7 +251,7 @@ pub trait CustomSchemaManagerExt {
) -> Result<HashSet<String>, DbErr>; ) -> Result<HashSet<String>, DbErr>;
} }
#[async_trait::async_trait] #[async_trait]
impl CustomSchemaManagerExt for SchemaManager<'_> { impl CustomSchemaManagerExt for SchemaManager<'_> {
async fn create_postgres_auto_update_ts_fn(&self, col_name: &str) -> Result<(), DbErr> { async fn create_postgres_auto_update_ts_fn(&self, col_name: &str) -> Result<(), DbErr> {
let sql = format!( let sql = format!(

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use loco_rs::schema::jsonb_null; use loco_rs::schema::jsonb_null;
use sea_orm_migration::{prelude::*, schema::*}; use sea_orm_migration::{prelude::*, schema::*};
@ -13,7 +14,7 @@ use crate::models::{
#[derive(DeriveMigrationName)] #[derive(DeriveMigrationName)]
pub struct Migration; pub struct Migration;
#[async_trait::async_trait] #[async_trait]
impl MigrationTrait for Migration { impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager manager

View File

@ -1,25 +1,24 @@
use async_trait::async_trait;
use loco_rs::schema::table_auto; use loco_rs::schema::table_auto;
use sea_orm_migration::{prelude::*, schema::*}; use sea_orm_migration::{prelude::*, schema::*};
use super::defs::*; use super::defs::*;
use crate::models::{ use crate::models::{
downloaders::DownloaderCategoryEnum, downloaders::{DownloaderCategory, DownloaderCategoryEnum},
prelude::{ downloads::{DownloadMime, DownloadMimeEnum, DownloadStatus, DownloadStatusEnum},
downloads::{DownloadMimeEnum, DownloadStatusEnum},
DownloadMime, DownloadStatus, DownloaderCategory,
},
}; };
#[derive(DeriveMigrationName)] #[derive(DeriveMigrationName)]
pub struct Migration; pub struct Migration;
#[async_trait::async_trait] #[async_trait]
impl MigrationTrait for Migration { impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
create_postgres_enum_for_active_enum!( create_postgres_enum_for_active_enum!(
manager, manager,
DownloaderCategoryEnum, DownloaderCategoryEnum,
DownloaderCategory::QBittorrent DownloaderCategory::QBittorrent,
DownloaderCategory::Dandanplay
) )
.await?; .await?;

View File

@ -8,7 +8,7 @@ use crate::{
#[derive(DeriveMigrationName)] #[derive(DeriveMigrationName)]
pub struct Migration; pub struct Migration;
#[async_trait::async_trait] #[async_trait]
impl MigrationTrait for Migration { impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
create_postgres_enum_for_active_enum!( create_postgres_enum_for_active_enum!(

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use sea_orm_migration::{prelude::*, schema::*}; use sea_orm_migration::{prelude::*, schema::*};
use super::defs::Auth; use super::defs::Auth;
@ -12,7 +13,7 @@ use crate::{
#[derive(DeriveMigrationName)] #[derive(DeriveMigrationName)]
pub struct Migration; pub struct Migration;
#[async_trait::async_trait] #[async_trait]
impl MigrationTrait for Migration { impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
create_postgres_enum_for_active_enum!( create_postgres_enum_for_active_enum!(

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
pub use sea_orm_migration::prelude::*; pub use sea_orm_migration::prelude::*;
#[macro_use] #[macro_use]
@ -8,7 +9,7 @@ pub mod m20241231_000001_auth;
pub struct Migrator; pub struct Migrator;
#[async_trait::async_trait] #[async_trait]
impl MigratorTrait for Migrator { impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> { fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![ vec![

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -50,5 +51,5 @@ pub enum RelatedEntity {
Subscriber, Subscriber,
} }
#[async_trait::async_trait] #[async_trait]
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use loco_rs::app::AppContext; use loco_rs::app::AppContext;
use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue, FromJsonQueryResult}; use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue, FromJsonQueryResult};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -59,6 +60,8 @@ pub enum Relation {
Subscriber, Subscriber,
#[sea_orm(has_many = "super::episodes::Entity")] #[sea_orm(has_many = "super::episodes::Entity")]
Episode, Episode,
#[sea_orm(has_many = "super::subscription_bangumi::Entity")]
SubscriptionBangumi,
} }
impl Related<super::episodes::Entity> for Entity { impl Related<super::episodes::Entity> for Entity {
@ -67,6 +70,12 @@ impl Related<super::episodes::Entity> for Entity {
} }
} }
impl Related<super::subscription_bangumi::Entity> for Entity {
fn to() -> RelationDef {
Relation::SubscriptionBangumi.def()
}
}
impl Related<super::subscriptions::Entity> for Entity { impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
super::subscription_bangumi::Relation::Subscription.def() super::subscription_bangumi::Relation::Subscription.def()
@ -83,6 +92,18 @@ impl Related<super::subscribers::Entity> for Entity {
} }
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::subscriptions::Entity")]
Subscription,
#[sea_orm(entity = "super::subscribers::Entity")]
Subscriber,
#[sea_orm(entity = "super::episodes::Entity")]
Episode,
#[sea_orm(entity = "super::subscription_bangumi::Entity")]
SubscriptionBangumi,
}
impl Model { impl Model {
pub async fn get_or_insert_from_mikan<F>( pub async fn get_or_insert_from_mikan<F>(
ctx: &AppContext, ctx: &AppContext,
@ -146,5 +167,5 @@ impl Model {
} }
} }
#[async_trait::async_trait] #[async_trait]
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -5,11 +6,17 @@ use url::Url;
#[derive( #[derive(
Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize,
)] )]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "downloader_type")] #[sea_orm(
rs_type = "String",
db_type = "Enum",
enum_name = "downloader_category"
)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum DownloaderCategory { pub enum DownloaderCategory {
#[sea_orm(string_value = "qbittorrent")] #[sea_orm(string_value = "qbittorrent")]
QBittorrent, QBittorrent,
#[sea_orm(string_value = "dandanplay")]
Dandanplay,
} }
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
@ -55,7 +62,15 @@ impl Related<super::downloads::Entity> for Entity {
} }
} }
#[async_trait::async_trait] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::subscribers::Entity")]
Subscriber,
#[sea_orm(entity = "super::downloads::Entity")]
Download,
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl Model { impl Model {

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -101,7 +102,17 @@ impl Related<super::episodes::Entity> for Entity {
} }
} }
#[async_trait::async_trait] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::subscribers::Entity")]
Subscriber,
#[sea_orm(entity = "super::downloaders::Entity")]
Downloader,
#[sea_orm(entity = "super::episodes::Entity")]
Episode,
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel {} impl ActiveModel {}

View File

@ -1,43 +0,0 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(
Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize,
)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "auth_type")]
#[serde(rename_all = "snake_case")]
pub enum AuthType {
#[sea_orm(string_value = "basic")]
Basic,
#[sea_orm(string_value = "oidc")]
Oidc,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)]
#[sea_orm(table_name = "auth")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
pub pid: String,
pub subscriber_id: i32,
pub auth_type: AuthType,
pub avatar_url: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::subscribers::Entity",
from = "Column::SubscriberId",
to = "super::subscribers::Column::Id"
)]
SubscriberId,
}
impl Related<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::SubscriberId.def()
}
}

View File

@ -1,80 +0,0 @@
use sea_orm::{entity::prelude::*, FromJsonQueryResult};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
pub struct BangumiFilter {
pub name: Option<Vec<String>>,
pub group: Option<Vec<String>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
pub struct BangumiExtra {
pub name_zh: Option<String>,
pub s_name_zh: Option<String>,
pub name_en: Option<String>,
pub s_name_en: Option<String>,
pub name_jp: Option<String>,
pub s_name_jp: Option<String>,
}
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "bangumi")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
pub mikan_bangumi_id: Option<String>,
pub subscription_id: i32,
pub subscriber_id: i32,
pub display_name: String,
pub raw_name: String,
pub season: i32,
pub season_raw: Option<String>,
pub fansub: Option<String>,
pub mikan_fansub_id: Option<String>,
pub filter: Option<BangumiFilter>,
pub rss_link: Option<String>,
pub poster_link: Option<String>,
pub save_path: Option<String>,
#[sea_orm(default = "false")]
pub deleted: bool,
pub homepage: Option<String>,
pub extra: Option<BangumiExtra>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::subscriptions::Entity",
from = "Column::SubscriptionId",
to = "super::subscriptions::Column::Id"
)]
Subscription,
#[sea_orm(
belongs_to = "super::subscribers::Entity",
from = "Column::SubscriberId",
to = "super::subscribers::Column::Id"
)]
Subscriber,
#[sea_orm(has_many = "super::episodes::Entity")]
Episode,
}
impl Related<super::episodes::Entity> for Entity {
fn to() -> RelationDef {
Relation::Episode.def()
}
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscription.def()
}
}
impl Related<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}

View File

@ -1,45 +0,0 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(
Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize,
)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "downloader_type")]
#[serde(rename_all = "snake_case")]
pub enum DownloaderCategory {
#[sea_orm(string_value = "qbittorrent")]
QBittorrent,
}
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "downloaders")]
pub struct Model {
#[sea_orm(column_type = "Timestamp")]
pub created_at: DateTime,
#[sea_orm(column_type = "Timestamp")]
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
pub category: DownloaderCategory,
pub endpoint: String,
pub password: String,
pub username: String,
pub subscriber_id: i32,
pub save_path: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::subscribers::Entity",
from = "Column::SubscriberId",
to = "super::subscribers::Column::Id"
)]
Subscriber,
}
impl Related<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}

View File

@ -1,78 +0,0 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(
Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize,
)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "download_status")]
#[serde(rename_all = "snake_case")]
pub enum DownloadStatus {
#[sea_orm(string_value = "pending")]
Pending,
#[sea_orm(string_value = "downloading")]
Downloading,
#[sea_orm(string_value = "paused")]
Paused,
#[sea_orm(string_value = "completed")]
Completed,
#[sea_orm(string_value = "failed")]
Failed,
#[sea_orm(string_value = "deleted")]
Deleted,
}
#[derive(
Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize,
)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "download_mime")]
pub enum DownloadMime {
#[sea_orm(string_value = "application/octet-stream")]
#[serde(rename = "application/octet-stream")]
OctetStream,
#[sea_orm(string_value = "application/x-bittorrent")]
#[serde(rename = "application/x-bittorrent")]
BitTorrent,
}
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "downloads")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
pub origin_name: String,
pub display_name: String,
pub subscription_id: i32,
pub status: DownloadStatus,
pub mime: DownloadMime,
pub url: String,
pub all_size: Option<u64>,
pub curr_size: Option<u64>,
pub homepage: Option<String>,
pub save_path: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::subscriptions::Entity",
from = "Column::SubscriptionId",
to = "super::subscriptions::Column::Id"
)]
Subscription,
#[sea_orm(has_many = "super::episodes::Entity")]
Episode,
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscription.def()
}
}
impl Related<super::episodes::Entity> for Entity {
fn to() -> RelationDef {
Relation::Episode.def()
}
}

View File

@ -1,95 +0,0 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
use sea_orm::{entity::prelude::*, FromJsonQueryResult};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult, Default)]
pub struct EpisodeExtra {
pub name_zh: Option<String>,
pub s_name_zh: Option<String>,
pub name_en: Option<String>,
pub s_name_en: Option<String>,
pub name_jp: Option<String>,
pub s_name_jp: Option<String>,
}
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "episodes")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(indexed)]
pub mikan_episode_id: Option<String>,
pub raw_name: String,
pub display_name: String,
pub bangumi_id: i32,
pub subscription_id: i32,
pub subscriber_id: i32,
pub download_id: Option<i32>,
pub save_path: Option<String>,
pub resolution: Option<String>,
pub season: i32,
pub season_raw: Option<String>,
pub fansub: Option<String>,
pub poster_link: Option<String>,
pub episode_index: i32,
pub homepage: Option<String>,
pub subtitle: Option<Vec<String>>,
#[sea_orm(default = "false")]
pub deleted: bool,
pub source: Option<String>,
pub extra: EpisodeExtra,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::bangumi::Entity",
from = "Column::BangumiId",
to = "super::bangumi::Column::Id"
)]
Bangumi,
#[sea_orm(
belongs_to = "super::downloads::Entity",
from = "Column::DownloadId",
to = "super::downloads::Column::Id"
)]
Downloads,
#[sea_orm(
belongs_to = "super::subscriptions::Entity",
from = "Column::SubscriptionId",
to = "super::subscriptions::Column::Id"
)]
Subscriptions,
#[sea_orm(
belongs_to = "super::subscribers::Entity",
from = "Column::SubscriberId",
to = "super::subscribers::Column::Id"
)]
Subscriber,
}
impl Related<super::bangumi::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bangumi.def()
}
}
impl Related<super::downloads::Entity> for Entity {
fn to() -> RelationDef {
Relation::Downloads.def()
}
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriptions.def()
}
}
impl Related<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}

View File

@ -1,8 +0,0 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
pub mod auth;
pub mod bangumi;
pub mod downloaders;
pub mod downloads;
pub mod episodes;
pub mod subscribers;
pub mod subscriptions;

View File

@ -1,71 +0,0 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
use sea_orm::{entity::prelude::*, FromJsonQueryResult};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
pub struct SubscriberBangumiConfig {
pub leading_group_tag: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "subscribers")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub pid: String,
pub display_name: String,
pub downloader_id: Option<i32>,
pub bangumi_conf: Option<SubscriberBangumiConfig>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::subscriptions::Entity")]
Subscription,
#[sea_orm(
belongs_to = "super::downloaders::Entity",
from = "Column::DownloaderId",
to = "super::downloaders::Column::Id"
)]
Downloader,
#[sea_orm(has_many = "super::bangumi::Entity")]
Bangumi,
#[sea_orm(has_many = "super::episodes::Entity")]
Episode,
#[sea_orm(has_many = "super::auth::Entity")]
Auth,
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscription.def()
}
}
impl Related<super::downloaders::Entity> for Entity {
fn to() -> RelationDef {
Relation::Downloader.def()
}
}
impl Related<super::bangumi::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bangumi.def()
}
}
impl Related<super::episodes::Entity> for Entity {
fn to() -> RelationDef {
Relation::Episode.def()
}
}
impl Related<super::auth::Entity> for Entity {
fn to() -> RelationDef {
Relation::Auth.def()
}
}

View File

@ -1,66 +0,0 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(
Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, DeriveDisplay,
)]
#[sea_orm(
rs_type = "String",
db_type = "Enum",
enum_name = "subscription_category"
)]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionCategory {
#[sea_orm(string_value = "mikan")]
Mikan,
#[sea_orm(string_value = "manual")]
Manual,
}
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "subscriptions")]
pub struct Model {
#[sea_orm(column_type = "Timestamp")]
pub created_at: DateTime,
#[sea_orm(column_type = "Timestamp")]
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
pub display_name: String,
pub subscriber_id: i32,
pub category: SubscriptionCategory,
pub source_url: String,
pub enabled: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::subscribers::Entity",
from = "Column::SubscriberId",
to = "super::subscribers::Column::Id"
)]
Subscriber,
#[sea_orm(has_many = "super::bangumi::Entity")]
Bangumi,
#[sea_orm(has_many = "super::episodes::Entity")]
Episodes,
}
impl Related<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}
impl Related<super::bangumi::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bangumi.def()
}
}
impl Related<super::episodes::Entity> for Entity {
fn to() -> RelationDef {
Relation::Episodes.def()
}
}

View File

@ -1,5 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
use loco_rs::app::AppContext; use loco_rs::app::AppContext;
use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue, FromJsonQueryResult}; use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue, FromJsonQueryResult};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -70,9 +71,11 @@ pub enum Relation {
)] )]
Bangumi, Bangumi,
#[sea_orm(has_many = "super::subscriptions::Entity")] #[sea_orm(has_many = "super::subscriptions::Entity")]
Subscriptions, Subscription,
#[sea_orm(has_one = "super::downloads::Entity")] #[sea_orm(has_one = "super::downloads::Entity")]
Downloads, Download,
#[sea_orm(has_many = "super::subscription_episode::Entity")]
SubscriptionEpisode,
} }
impl Related<super::bangumi::Entity> for Entity { impl Related<super::bangumi::Entity> for Entity {
@ -83,13 +86,7 @@ impl Related<super::bangumi::Entity> for Entity {
impl Related<super::downloads::Entity> for Entity { impl Related<super::downloads::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::Downloads.def() Relation::Download.def()
}
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriptions.def()
} }
} }
@ -99,6 +96,36 @@ impl Related<super::subscribers::Entity> for Entity {
} }
} }
impl Related<super::subscription_episode::Entity> for Entity {
fn to() -> RelationDef {
Relation::SubscriptionEpisode.def()
}
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
super::subscription_episode::Relation::Subscription.def()
}
fn via() -> Option<RelationDef> {
Some(Relation::Subscription.def())
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::subscribers::Entity")]
Subscriber,
#[sea_orm(entity = "super::downloads::Entity")]
Subscription,
#[sea_orm(entity = "super::bangumi::Entity")]
Bangumi,
#[sea_orm(entity = "super::subscriptions::Entity")]
Download,
#[sea_orm(entity = "super::subscription_episode::Entity")]
SubscriptionEpisode,
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct MikanEpsiodeCreation { pub struct MikanEpsiodeCreation {
pub episode: MikanEpisodeMeta, pub episode: MikanEpisodeMeta,
@ -206,5 +233,5 @@ impl ActiveModel {
} }
} }
#[async_trait::async_trait] #[async_trait]
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View File

@ -3,7 +3,6 @@ pub mod bangumi;
pub mod downloaders; pub mod downloaders;
pub mod downloads; pub mod downloads;
pub mod episodes; pub mod episodes;
pub mod prelude;
pub mod query; pub mod query;
pub mod subscribers; pub mod subscribers;
pub mod subscription_bangumi; pub mod subscription_bangumi;

View File

@ -1,8 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Notification {
season: i32,
episode_size: u32,
poster_url: Option<String>,
}

View File

@ -1,8 +0,0 @@
pub use super::{
bangumi::{self, Entity as Bangumi},
downloaders::{self, DownloaderCategory, Entity as Downloader},
downloads::{self, DownloadMime, DownloadStatus, Entity as Download},
episodes::{self, Entity as Episode},
subscribers::{self, Entity as Subscriber},
subscriptions::{self, Entity as Subscription, SubscriptionCategory},
};

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use sea_orm::{ use sea_orm::{
prelude::Expr, prelude::Expr,
sea_query::{Alias, IntoColumnRef, IntoTableRef, Query, SelectStatement}, sea_query::{Alias, IntoColumnRef, IntoTableRef, Query, SelectStatement},
@ -26,7 +27,7 @@ pub fn filter_values_in<
.to_owned() .to_owned()
} }
#[async_trait::async_trait] #[async_trait]
pub trait InsertManyReturningExt<A>: Sized pub trait InsertManyReturningExt<A>: Sized
where where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>, <A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
@ -49,7 +50,7 @@ where
I: IntoIterator<Item = <A::Entity as EntityTrait>::Column> + Send; I: IntoIterator<Item = <A::Entity as EntityTrait>::Column> + Send;
} }
#[async_trait::async_trait] #[async_trait]
impl<A> InsertManyReturningExt<A> for Insert<A> impl<A> InsertManyReturningExt<A> for Insert<A>
where where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>, <A::Entity as EntityTrait>::Model: IntoActiveModel<A>,

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use loco_rs::{ use loco_rs::{
app::AppContext, app::AppContext,
model::{ModelError, ModelResult}, model::{ModelError, ModelResult},
@ -69,12 +70,26 @@ impl Related<super::auth::Entity> for Entity {
} }
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::subscriptions::Entity")]
Subscription,
#[sea_orm(entity = "super::downloaders::Entity")]
Downloader,
#[sea_orm(entity = "super::bangumi::Entity")]
Bangumi,
#[sea_orm(entity = "super::episodes::Entity")]
Episode,
#[sea_orm(entity = "super::auth::Entity")]
Auth,
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct SubscriberIdParams { pub struct SubscriberIdParams {
pub id: String, pub id: String,
} }
#[async_trait::async_trait] #[async_trait]
impl ActiveModelBehavior for ActiveModel { impl ActiveModelBehavior for ActiveModel {
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr> async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
where where

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use sea_orm::{entity::prelude::*, ActiveValue}; use sea_orm::{entity::prelude::*, ActiveValue};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -42,7 +43,15 @@ impl Related<super::bangumi::Entity> for Entity {
} }
} }
#[async_trait::async_trait] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::subscriptions::Entity")]
Subscription,
#[sea_orm(entity = "super::bangumi::Entity")]
Bangumi,
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel { impl ActiveModel {

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use sea_orm::{entity::prelude::*, ActiveValue}; use sea_orm::{entity::prelude::*, ActiveValue};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -42,7 +43,15 @@ impl Related<super::episodes::Entity> for Entity {
} }
} }
#[async_trait::async_trait] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::subscriptions::Entity")]
Subscription,
#[sea_orm(entity = "super::episodes::Entity")]
Episode,
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel { impl ActiveModel {

View File

@ -1,5 +1,6 @@
use std::{collections::HashSet, sync::Arc}; use std::{collections::HashSet, sync::Arc};
use async_trait::async_trait;
use itertools::Itertools; use itertools::Itertools;
use loco_rs::app::AppContext; use loco_rs::app::AppContext;
use sea_orm::{entity::prelude::*, ActiveValue}; use sea_orm::{entity::prelude::*, ActiveValue};
@ -81,6 +82,18 @@ impl Related<super::subscribers::Entity> for Entity {
} }
} }
impl Related<super::subscription_bangumi::Entity> for Entity {
fn to() -> RelationDef {
Relation::SubscriptionBangumi.def()
}
}
impl Related<super::subscription_episode::Entity> for Entity {
fn to() -> RelationDef {
Relation::SubscriptionEpisode.def()
}
}
impl Related<super::bangumi::Entity> for Entity { impl Related<super::bangumi::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
super::subscription_bangumi::Relation::Bangumi.def() super::subscription_bangumi::Relation::Bangumi.def()
@ -109,6 +122,20 @@ impl Related<super::episodes::Entity> for Entity {
} }
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
pub enum RelatedEntity {
#[sea_orm(entity = "super::subscribers::Entity")]
Subscriber,
#[sea_orm(entity = "super::bangumi::Entity")]
Bangumi,
#[sea_orm(entity = "super::episodes::Entity")]
Episode,
#[sea_orm(entity = "super::subscription_episode::Entity")]
SubscriptionEpisode,
#[sea_orm(entity = "super::subscription_bangumi::Entity")]
SubscriptionBangumi,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SubscriptionCreateFromRssDto { pub struct SubscriptionCreateFromRssDto {
pub rss_link: String, pub rss_link: String,
@ -122,7 +149,7 @@ pub enum SubscriptionCreateDto {
Mikan(SubscriptionCreateFromRssDto), Mikan(SubscriptionCreateFromRssDto),
} }
#[async_trait::async_trait] #[async_trait]
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel { impl ActiveModel {

View File

@ -1,5 +1,6 @@
use std::fmt::Debug; use std::fmt::Debug;
use async_trait::async_trait;
use itertools::Itertools; use itertools::Itertools;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use librqbit_core::{ use librqbit_core::{
@ -238,7 +239,7 @@ impl Torrent {
} }
} }
#[async_trait::async_trait] #[async_trait]
pub trait TorrentDownloader { pub trait TorrentDownloader {
async fn get_torrents_info( async fn get_torrents_info(
&self, &self,

View File

@ -2,6 +2,7 @@ use std::{
borrow::Cow, collections::HashSet, fmt::Debug, future::Future, sync::Arc, time::Duration, borrow::Cow, collections::HashSet, fmt::Debug, future::Future, sync::Arc, time::Duration,
}; };
use async_trait::async_trait;
use eyre::OptionExt; use eyre::OptionExt;
use futures::future::try_join_all; use futures::future::try_join_all;
pub use qbit_rs::model::{ pub use qbit_rs::model::{
@ -218,7 +219,7 @@ impl QBittorrentDownloader {
} }
} }
#[async_trait::async_trait] #[async_trait]
impl TorrentDownloader for QBittorrentDownloader { impl TorrentDownloader for QBittorrentDownloader {
#[instrument(level = "debug", skip(self))] #[instrument(level = "debug", skip(self))]
async fn get_torrents_info( async fn get_torrents_info(
@ -472,7 +473,6 @@ impl Debug for QBittorrentDownloader {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use itertools::Itertools; use itertools::Itertools;
use testcontainers_modules::testcontainers::ImageExt;
use super::*; use super::*;
@ -495,6 +495,7 @@ pub mod tests {
}, },
GenericImage, GenericImage,
}; };
use testcontainers_modules::testcontainers::ImageExt;
use crate::test_utils::testcontainers::ContainerRequestEnhancedExt; use crate::test_utils::testcontainers::ContainerRequestEnhancedExt;

View File

@ -1,107 +0,0 @@
#[cfg(feature = "testcontainers")]
pub mod testcontainers {
use bollard::container::ListContainersOptions;
use itertools::Itertools;
use testcontainers::{
core::logs::consumer::logging_consumer::LoggingConsumer, ContainerRequest, Image, ImageExt,
};
pub const TESTCONTAINERS_PROJECT_KEY: &str = "tech.enfw.testcontainers.project";
pub const TESTCONTAINERS_CONTAINER_KEY: &str = "tech.enfw.testcontainers.container";
pub const TESTCONTAINERS_PRUNE_KEY: &str = "tech.enfw.testcontainers.prune";
#[async_trait::async_trait]
pub trait ContainerRequestEnhancedExt<I>: Sized + ImageExt<I>
where
I: Image,
{
async fn with_prune_existed_label(
self,
container_label: &str,
prune: bool,
force: bool,
) -> eyre::Result<Self>;
fn with_default_log_consumer(self) -> Self;
}
#[async_trait::async_trait]
impl<I> ContainerRequestEnhancedExt<I> for ContainerRequest<I>
where
I: Image,
{
async fn with_prune_existed_label(
self,
container_label: &str,
prune: bool,
force: bool,
) -> eyre::Result<Self> {
use std::collections::HashMap;
use bollard::container::PruneContainersOptions;
use testcontainers::core::client::docker_client_instance;
if prune {
let client = docker_client_instance().await?;
let mut filters = HashMap::<String, Vec<String>>::new();
filters.insert(
String::from("label"),
vec![
format!("{TESTCONTAINERS_PRUNE_KEY}=true"),
format!("{}={}", TESTCONTAINERS_PROJECT_KEY, "konobangu"),
format!("{}={}", TESTCONTAINERS_CONTAINER_KEY, container_label),
],
);
if force {
let result = client
.list_containers(Some(ListContainersOptions {
all: false,
filters: filters.clone(),
..Default::default()
}))
.await?;
let remove_containers = result
.iter()
.filter(|c| matches!(c.state.as_deref(), Some("running")))
.flat_map(|c| c.id.as_deref())
.collect_vec();
futures::future::try_join_all(
remove_containers
.iter()
.map(|c| client.stop_container(c, None)),
)
.await?;
tracing::warn!(name = "stop running containers", result = ?remove_containers);
}
let result = client
.prune_containers(Some(PruneContainersOptions { filters }))
.await?;
tracing::warn!(name = "prune existed containers", result = ?result);
}
let result = self.with_labels([
(TESTCONTAINERS_PRUNE_KEY, "true"),
(TESTCONTAINERS_PROJECT_KEY, "konobangu"),
(TESTCONTAINERS_CONTAINER_KEY, container_label),
]);
Ok(result)
}
fn with_default_log_consumer(self) -> Self {
self.with_log_consumer(
LoggingConsumer::new()
.with_stdout_level(log::Level::Info)
.with_stderr_level(log::Level::Error),
)
}
}
}

View File

@ -0,0 +1,2 @@
#[cfg(feature = "testcontainers")]
pub mod testcontainers;

View File

@ -0,0 +1,105 @@
use async_trait::async_trait;
use bollard::container::ListContainersOptions;
use itertools::Itertools;
use testcontainers::{
core::logs::consumer::logging_consumer::LoggingConsumer, ContainerRequest, Image, ImageExt,
};
pub const TESTCONTAINERS_PROJECT_KEY: &str = "tech.enfw.testcontainers.project";
pub const TESTCONTAINERS_CONTAINER_KEY: &str = "tech.enfw.testcontainers.container";
pub const TESTCONTAINERS_PRUNE_KEY: &str = "tech.enfw.testcontainers.prune";
#[async_trait]
pub trait ContainerRequestEnhancedExt<I>: Sized + ImageExt<I>
where
I: Image,
{
async fn with_prune_existed_label(
self,
container_label: &str,
prune: bool,
force: bool,
) -> eyre::Result<Self>;
fn with_default_log_consumer(self) -> Self;
}
#[async_trait]
impl<I> ContainerRequestEnhancedExt<I> for ContainerRequest<I>
where
I: Image,
{
async fn with_prune_existed_label(
self,
container_label: &str,
prune: bool,
force: bool,
) -> eyre::Result<Self> {
use std::collections::HashMap;
use bollard::container::PruneContainersOptions;
use testcontainers::core::client::docker_client_instance;
if prune {
let client = docker_client_instance().await?;
let mut filters = HashMap::<String, Vec<String>>::new();
filters.insert(
String::from("label"),
vec![
format!("{TESTCONTAINERS_PRUNE_KEY}=true"),
format!("{}={}", TESTCONTAINERS_PROJECT_KEY, "konobangu"),
format!("{}={}", TESTCONTAINERS_CONTAINER_KEY, container_label),
],
);
if force {
let result = client
.list_containers(Some(ListContainersOptions {
all: false,
filters: filters.clone(),
..Default::default()
}))
.await?;
let remove_containers = result
.iter()
.filter(|c| matches!(c.state.as_deref(), Some("running")))
.flat_map(|c| c.id.as_deref())
.collect_vec();
futures::future::try_join_all(
remove_containers
.iter()
.map(|c| client.stop_container(c, None)),
)
.await?;
tracing::warn!(name = "stop running containers", result = ?remove_containers);
}
let result = client
.prune_containers(Some(PruneContainersOptions { filters }))
.await?;
tracing::warn!(name = "prune existed containers", result = ?result);
}
let result = self.with_labels([
(TESTCONTAINERS_PRUNE_KEY, "true"),
(TESTCONTAINERS_PROJECT_KEY, "konobangu"),
(TESTCONTAINERS_CONTAINER_KEY, container_label),
]);
Ok(result)
}
fn with_default_log_consumer(self) -> Self {
self.with_log_consumer(
LoggingConsumer::new()
.with_stdout_level(log::Level::Info)
.with_stderr_level(log::Level::Error),
)
}
}

3487
apps/recorder/user_agents Normal file

File diff suppressed because it is too large Load Diff

View File

@ -117,14 +117,13 @@ settings:
data_dir: ./data data_dir: ./data
mikan: mikan:
base_url: "https://mikanani.me/"
http_client: http_client:
exponential_backoff_max_retries: 3 exponential_backoff_max_retries: 3
leaky_bucket_max_tokens: 2 leaky_bucket_max_tokens: 2
leaky_bucket_initial_tokens: 0 leaky_bucket_initial_tokens: 0
leaky_bucket_refill_tokens: 1 leaky_bucket_refill_tokens: 1
leaky_bucket_refill_interval: 500 leaky_bucket_refill_interval: 500
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
base_url: "https://mikanani.me/"
auth: auth:
auth_type: "oidc" # or "basic" auth_type: "oidc" # or "basic"

View File

@ -30,13 +30,14 @@
"commander": "^12.1.0" "commander": "^12.1.0"
}, },
"devDependencies": { "devDependencies": {
"rimraf": "^6.0.1",
"shx": "^0.3.4",
"@auto-it/all-contributors": "^11.3.0", "@auto-it/all-contributors": "^11.3.0",
"@auto-it/first-time-contributor": "^11.3.0", "@auto-it/first-time-contributor": "^11.3.0",
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@konobangu/typescript-config": "workspace:*", "@konobangu/typescript-config": "workspace:*",
"@turbo/gen": "^2.3.3", "@turbo/gen": "^2.3.3",
"@types/jsdom": "^21.1.7",
"rimraf": "^6.0.1",
"shx": "^0.3.4",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"turbo": "^2.3.3", "turbo": "^2.3.3",
"typescript": "^5.7.2", "typescript": "^5.7.2",

17
pnpm-lock.yaml generated
View File

@ -30,6 +30,9 @@ importers:
'@turbo/gen': '@turbo/gen':
specifier: ^2.3.3 specifier: ^2.3.3
version: 2.3.3(@types/node@22.10.1)(typescript@5.7.2) version: 2.3.3(@types/node@22.10.1)(typescript@5.7.2)
'@types/jsdom':
specifier: ^21.1.7
version: 21.1.7
rimraf: rimraf:
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1 version: 6.0.1
@ -4605,6 +4608,9 @@ packages:
'@types/inquirer@6.5.0': '@types/inquirer@6.5.0':
resolution: {integrity: sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==} resolution: {integrity: sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==}
'@types/jsdom@21.1.7':
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@ -4676,6 +4682,9 @@ packages:
'@types/tinycolor2@1.4.6': '@types/tinycolor2@1.4.6':
resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==}
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
'@types/unist@2.0.11': '@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@ -14170,6 +14179,12 @@ snapshots:
'@types/through': 0.0.33 '@types/through': 0.0.33
rxjs: 6.6.7 rxjs: 6.6.7
'@types/jsdom@21.1.7':
dependencies:
'@types/node': 22.10.1
'@types/tough-cookie': 4.0.5
parse5: 7.2.1
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/lodash.merge@4.6.9': '@types/lodash.merge@4.6.9':
@ -14244,6 +14259,8 @@ snapshots:
'@types/tinycolor2@1.4.6': {} '@types/tinycolor2@1.4.6': {}
'@types/tough-cookie@4.0.5': {}
'@types/unist@2.0.11': {} '@types/unist@2.0.11': {}
'@types/unist@3.0.3': {} '@types/unist@3.0.3': {}