feat: add basic graphql support
This commit is contained in:
parent
caaa5dc0cc
commit
40cbf86f0f
40
Cargo.lock
generated
40
Cargo.lock
generated
@ -220,18 +220,22 @@ dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"fast_chemail",
|
||||
"fnv",
|
||||
"futures-channel",
|
||||
"futures-timer",
|
||||
"futures-util",
|
||||
"handlebars",
|
||||
"http 1.2.0",
|
||||
"indexmap 2.7.0",
|
||||
"lru",
|
||||
"mime",
|
||||
"multer",
|
||||
"num-traits",
|
||||
"pin-project-lite",
|
||||
"regex",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@ -1760,6 +1764,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@ -2071,6 +2081,11 @@ name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
@ -3315,6 +3330,15 @@ version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "lru-cache"
|
||||
version = "0.1.2"
|
||||
@ -4522,6 +4546,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"eyre",
|
||||
"fancy-regex",
|
||||
"fastrand",
|
||||
"figment",
|
||||
"futures",
|
||||
"html-escape",
|
||||
@ -4548,6 +4573,7 @@ dependencies = [
|
||||
"scraper",
|
||||
"sea-orm",
|
||||
"sea-orm-migration",
|
||||
"seaography",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
@ -5286,6 +5312,20 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
|
@ -85,8 +85,10 @@ testcontainers-modules = { version = "0.11.4", optional = true }
|
||||
log = "0.4.22"
|
||||
anyhow = "1.0.95"
|
||||
bollard = { version = "0.18", optional = true }
|
||||
async-graphql = "7.0.13"
|
||||
async-graphql = { version = "7.0.13", features = [] }
|
||||
async-graphql-axum = "7.0.13"
|
||||
fastrand = "2.3.0"
|
||||
seaography = "1.1.2"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -16,9 +16,10 @@ use sea_orm::DatabaseConnection;
|
||||
|
||||
use crate::{
|
||||
auth::service::AppAuthService,
|
||||
controllers,
|
||||
controllers::{self},
|
||||
dal::{AppDalClient, AppDalInitalizer},
|
||||
extract::mikan::{client::AppMikanClientInitializer, AppMikanClient},
|
||||
graphql::service::{AppGraphQLService, AppGraphQLServiceInitializer},
|
||||
migrations::Migrator,
|
||||
models::subscribers,
|
||||
workers::subscription_worker::SubscriptionWorker,
|
||||
@ -36,6 +37,10 @@ pub trait AppContextExt {
|
||||
fn get_auth_service(&self) -> &AppAuthService {
|
||||
AppAuthService::app_instance()
|
||||
}
|
||||
|
||||
fn get_graphql_service(&self) -> &AppGraphQLService {
|
||||
AppGraphQLService::app_instance()
|
||||
}
|
||||
}
|
||||
|
||||
impl AppContextExt for AppContext {}
|
||||
@ -52,6 +57,7 @@ impl Hooks for App {
|
||||
let initializers: Vec<Box<dyn Initializer>> = vec![
|
||||
Box::new(AppDalInitalizer),
|
||||
Box::new(AppMikanClientInitializer),
|
||||
Box::new(AppGraphQLServiceInitializer),
|
||||
];
|
||||
|
||||
Ok(initializers)
|
||||
@ -71,10 +77,11 @@ impl Hooks for App {
|
||||
create_app::<Self, Migrator>(mode, environment).await
|
||||
}
|
||||
|
||||
fn routes(_ctx: &AppContext) -> AppRoutes {
|
||||
fn routes(ctx: &AppContext) -> AppRoutes {
|
||||
AppRoutes::with_default_routes()
|
||||
.prefix("/api")
|
||||
.add_route(controllers::subscribers::routes())
|
||||
.add_route(controllers::graphql::routes(ctx.get_graphql_service()))
|
||||
}
|
||||
|
||||
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use axum::{http::request::Parts, RequestPartsExt};
|
||||
use axum_auth::AuthBasic;
|
||||
|
||||
@ -13,7 +14,7 @@ pub struct BasicAuthService {
|
||||
pub config: BasicAuthConfig,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl AuthService for BasicAuthService {
|
||||
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
|
||||
if let Ok(AuthBasic((found_user, found_password))) = request.extract().await {
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::http::request::Parts;
|
||||
use itertools::Itertools;
|
||||
use jwt_authorizer::{authorizer::Authorizer, NumericDate, OneOrArray};
|
||||
@ -80,7 +81,7 @@ pub struct OidcAuthService {
|
||||
pub authorizer: Authorizer<OidcAuthClaims>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl AuthService for OidcAuthService {
|
||||
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
|
||||
let config = &self.config;
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
extract::FromRequestParts,
|
||||
http::request::Parts,
|
||||
@ -21,7 +22,7 @@ pub struct AuthUserInfo {
|
||||
pub auth_type: AuthType,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for AuthUserInfo
|
||||
where
|
||||
S: Send + Sync,
|
||||
@ -42,7 +43,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
pub trait AuthService {
|
||||
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 {
|
||||
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
|
||||
match self {
|
||||
@ -96,7 +97,7 @@ impl AuthService for AppAuthService {
|
||||
|
||||
pub struct AppAuthServiceInitializer;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl Initializer for AppAuthServiceInitializer {
|
||||
fn name(&self) -> String {
|
||||
String::from("AppAuthServiceInitializer")
|
||||
|
@ -4,7 +4,10 @@ use figment::{
|
||||
};
|
||||
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");
|
||||
|
||||
@ -13,6 +16,7 @@ pub struct AppConfig {
|
||||
pub auth: AppAuthConfig,
|
||||
pub dal: AppDalConfig,
|
||||
pub mikan: AppMikanConfig,
|
||||
pub graphql: AppGraphQLConfig,
|
||||
}
|
||||
|
||||
pub fn deserialize_key_path_from_json_value<T: DeserializeOwned>(
|
||||
|
@ -4,9 +4,12 @@ dal:
|
||||
mikan:
|
||||
http_client:
|
||||
exponential_backoff_max_retries: 3
|
||||
leaky_bucket_max_tokens: 3
|
||||
leaky_bucket_max_tokens: 2
|
||||
leaky_bucket_initial_tokens: 0
|
||||
leaky_bucket_refill_tokens: 1
|
||||
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/"
|
||||
|
||||
graphql:
|
||||
depth_limit: null
|
||||
complexity_limit: null
|
||||
|
19
apps/recorder/src/controllers/graphql.rs
Normal file
19
apps/recorder/src/controllers/graphql.rs
Normal 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())),
|
||||
)
|
||||
}
|
@ -1 +1,2 @@
|
||||
pub mod graphql;
|
||||
pub mod subscribers;
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::fmt;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use loco_rs::app::{AppContext, Initializer};
|
||||
use once_cell::sync::OnceCell;
|
||||
@ -184,7 +185,7 @@ impl AppDalClient {
|
||||
|
||||
pub struct AppDalInitalizer;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl Initializer for AppDalInitalizer {
|
||||
fn name(&self) -> String {
|
||||
String::from("AppDalInitalizer")
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use loco_rs::app::{AppContext, Initializer};
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
@ -45,7 +46,7 @@ impl Deref for AppMikanClient {
|
||||
|
||||
pub struct AppMikanClientInitializer;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl Initializer for AppMikanClientInitializer {
|
||||
fn name(&self) -> String {
|
||||
"AppMikanClientInitializer".to_string()
|
||||
|
@ -6,6 +6,12 @@ use super::HttpClient;
|
||||
pub async fn fetch_bytes<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<Bytes> {
|
||||
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)
|
||||
}
|
||||
|
@ -11,8 +11,9 @@ use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
use reqwest_tracing::TracingMiddleware;
|
||||
use serde::{Deserialize, Serialize};
|
||||
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]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
@ -31,9 +32,9 @@ pub struct HttpClient {
|
||||
pub config: HttpClientConfig,
|
||||
}
|
||||
|
||||
impl Into<ClientWithMiddleware> for HttpClient {
|
||||
fn into(self) -> ClientWithMiddleware {
|
||||
self.client
|
||||
impl From<HttpClient> for ClientWithMiddleware {
|
||||
fn from(val: HttpClient) -> Self {
|
||||
val.client
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +50,7 @@ pub struct RateLimiterMiddleware {
|
||||
rate_limiter: RateLimiter,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl reqwest_middleware::Middleware for RateLimiterMiddleware {
|
||||
async fn handle(
|
||||
&self,
|
||||
@ -68,7 +69,7 @@ impl HttpClient {
|
||||
config
|
||||
.user_agent
|
||||
.as_deref()
|
||||
.unwrap_or(DEFAULT_HTTP_CLIENT_USER_AGENT),
|
||||
.unwrap_or_else(|| get_random_mobile_ua()),
|
||||
);
|
||||
|
||||
let reqwest_client = reqwest_client_builder.build()?;
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -4,7 +4,13 @@ use super::HttpClient;
|
||||
|
||||
pub async fn fetch_html<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<String> {
|
||||
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)
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ pub mod core;
|
||||
pub mod html;
|
||||
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 client::{HttpClient, HttpClientConfig};
|
||||
|
15
apps/recorder/src/fetch/ua.json
Normal file
15
apps/recorder/src/fetch/ua.json
Normal 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"
|
||||
]
|
7
apps/recorder/src/graphql/config.rs
Normal file
7
apps/recorder/src/graphql/config.rs
Normal 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>,
|
||||
}
|
5
apps/recorder/src/graphql/mod.rs
Normal file
5
apps/recorder/src/graphql/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod query_root;
|
||||
pub mod service;
|
||||
pub mod config;
|
||||
|
||||
pub use query_root::schema;
|
53
apps/recorder/src/graphql/query_root.rs
Normal file
53
apps/recorder/src/graphql/query_root.rs
Normal 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))
|
||||
}
|
51
apps/recorder/src/graphql/service.rs
Normal file
51
apps/recorder/src/graphql/service.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ pub mod controllers;
|
||||
pub mod dal;
|
||||
pub mod extract;
|
||||
pub mod fetch;
|
||||
pub mod graphql;
|
||||
pub mod migrations;
|
||||
pub mod models;
|
||||
pub mod sync;
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{DeriveIden, Statement};
|
||||
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 {
|
||||
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>(
|
||||
@ -250,7 +251,7 @@ pub trait CustomSchemaManagerExt {
|
||||
) -> Result<HashSet<String>, DbErr>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl CustomSchemaManagerExt for SchemaManager<'_> {
|
||||
async fn create_postgres_auto_update_ts_fn(&self, col_name: &str) -> Result<(), DbErr> {
|
||||
let sql = format!(
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use loco_rs::schema::jsonb_null;
|
||||
use sea_orm_migration::{prelude::*, schema::*};
|
||||
|
||||
@ -13,7 +14,7 @@ use crate::models::{
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
|
@ -1,25 +1,24 @@
|
||||
use async_trait::async_trait;
|
||||
use loco_rs::schema::table_auto;
|
||||
use sea_orm_migration::{prelude::*, schema::*};
|
||||
|
||||
use super::defs::*;
|
||||
use crate::models::{
|
||||
downloaders::DownloaderCategoryEnum,
|
||||
prelude::{
|
||||
downloads::{DownloadMimeEnum, DownloadStatusEnum},
|
||||
DownloadMime, DownloadStatus, DownloaderCategory,
|
||||
},
|
||||
downloaders::{DownloaderCategory, DownloaderCategoryEnum},
|
||||
downloads::{DownloadMime, DownloadMimeEnum, DownloadStatus, DownloadStatusEnum},
|
||||
};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
create_postgres_enum_for_active_enum!(
|
||||
manager,
|
||||
DownloaderCategoryEnum,
|
||||
DownloaderCategory::QBittorrent
|
||||
DownloaderCategory::QBittorrent,
|
||||
DownloaderCategory::Dandanplay
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -8,7 +8,7 @@ use crate::{
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
create_postgres_enum_for_active_enum!(
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use sea_orm_migration::{prelude::*, schema::*};
|
||||
|
||||
use super::defs::Auth;
|
||||
@ -12,7 +13,7 @@ use crate::{
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
create_postgres_enum_for_active_enum!(
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
pub use sea_orm_migration::prelude::*;
|
||||
|
||||
#[macro_use]
|
||||
@ -8,7 +9,7 @@ pub mod m20241231_000001_auth;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -50,5 +51,5 @@ pub enum RelatedEntity {
|
||||
Subscriber,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use loco_rs::app::AppContext;
|
||||
use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue, FromJsonQueryResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -59,6 +60,8 @@ pub enum Relation {
|
||||
Subscriber,
|
||||
#[sea_orm(has_many = "super::episodes::Entity")]
|
||||
Episode,
|
||||
#[sea_orm(has_many = "super::subscription_bangumi::Entity")]
|
||||
SubscriptionBangumi,
|
||||
}
|
||||
|
||||
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 {
|
||||
fn to() -> RelationDef {
|
||||
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 {
|
||||
pub async fn get_or_insert_from_mikan<F>(
|
||||
ctx: &AppContext,
|
||||
@ -146,5 +167,5 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
@ -5,11 +6,17 @@ use url::Url;
|
||||
#[derive(
|
||||
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")]
|
||||
pub enum DownloaderCategory {
|
||||
#[sea_orm(string_value = "qbittorrent")]
|
||||
QBittorrent,
|
||||
#[sea_orm(string_value = "dandanplay")]
|
||||
Dandanplay,
|
||||
}
|
||||
|
||||
#[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 Model {
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::entity::prelude::*;
|
||||
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 ActiveModel {}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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;
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use loco_rs::app::AppContext;
|
||||
use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue, FromJsonQueryResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -70,9 +71,11 @@ pub enum Relation {
|
||||
)]
|
||||
Bangumi,
|
||||
#[sea_orm(has_many = "super::subscriptions::Entity")]
|
||||
Subscriptions,
|
||||
Subscription,
|
||||
#[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 {
|
||||
@ -83,13 +86,7 @@ impl Related<super::bangumi::Entity> for Entity {
|
||||
|
||||
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()
|
||||
Relation::Download.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)]
|
||||
pub struct MikanEpsiodeCreation {
|
||||
pub episode: MikanEpisodeMeta,
|
||||
@ -206,5 +233,5 @@ impl ActiveModel {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
@ -3,7 +3,6 @@ pub mod bangumi;
|
||||
pub mod downloaders;
|
||||
pub mod downloads;
|
||||
pub mod episodes;
|
||||
pub mod prelude;
|
||||
pub mod query;
|
||||
pub mod subscribers;
|
||||
pub mod subscription_bangumi;
|
||||
|
@ -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>,
|
||||
}
|
@ -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},
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{
|
||||
prelude::Expr,
|
||||
sea_query::{Alias, IntoColumnRef, IntoTableRef, Query, SelectStatement},
|
||||
@ -26,7 +27,7 @@ pub fn filter_values_in<
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
pub trait InsertManyReturningExt<A>: Sized
|
||||
where
|
||||
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
|
||||
@ -49,7 +50,7 @@ where
|
||||
I: IntoIterator<Item = <A::Entity as EntityTrait>::Column> + Send;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl<A> InsertManyReturningExt<A> for Insert<A>
|
||||
where
|
||||
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use loco_rs::{
|
||||
app::AppContext,
|
||||
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)]
|
||||
pub struct SubscriberIdParams {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
|
||||
where
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{entity::prelude::*, ActiveValue};
|
||||
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 ActiveModel {
|
||||
|
@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{entity::prelude::*, ActiveValue};
|
||||
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 ActiveModel {
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use itertools::Itertools;
|
||||
use loco_rs::app::AppContext;
|
||||
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 {
|
||||
fn to() -> RelationDef {
|
||||
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)]
|
||||
pub struct SubscriptionCreateFromRssDto {
|
||||
pub rss_link: String,
|
||||
@ -122,7 +149,7 @@ pub enum SubscriptionCreateDto {
|
||||
Mikan(SubscriptionCreateFromRssDto),
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl ActiveModel {
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use librqbit_core::{
|
||||
@ -238,7 +239,7 @@ impl Torrent {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
pub trait TorrentDownloader {
|
||||
async fn get_torrents_info(
|
||||
&self,
|
||||
|
@ -2,6 +2,7 @@ use std::{
|
||||
borrow::Cow, collections::HashSet, fmt::Debug, future::Future, sync::Arc, time::Duration,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use eyre::OptionExt;
|
||||
use futures::future::try_join_all;
|
||||
pub use qbit_rs::model::{
|
||||
@ -218,7 +219,7 @@ impl QBittorrentDownloader {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl TorrentDownloader for QBittorrentDownloader {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_torrents_info(
|
||||
@ -472,7 +473,6 @@ impl Debug for QBittorrentDownloader {
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use itertools::Itertools;
|
||||
use testcontainers_modules::testcontainers::ImageExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -495,6 +495,7 @@ pub mod tests {
|
||||
},
|
||||
GenericImage,
|
||||
};
|
||||
use testcontainers_modules::testcontainers::ImageExt;
|
||||
|
||||
use crate::test_utils::testcontainers::ContainerRequestEnhancedExt;
|
||||
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
2
apps/recorder/src/test_utils/mod.rs
Normal file
2
apps/recorder/src/test_utils/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
#[cfg(feature = "testcontainers")]
|
||||
pub mod testcontainers;
|
105
apps/recorder/src/test_utils/testcontainers.rs
Normal file
105
apps/recorder/src/test_utils/testcontainers.rs
Normal 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
3487
apps/recorder/user_agents
Normal file
File diff suppressed because it is too large
Load Diff
@ -117,14 +117,13 @@ settings:
|
||||
data_dir: ./data
|
||||
|
||||
mikan:
|
||||
base_url: "https://mikanani.me/"
|
||||
http_client:
|
||||
exponential_backoff_max_retries: 3
|
||||
leaky_bucket_max_tokens: 2
|
||||
leaky_bucket_initial_tokens: 0
|
||||
leaky_bucket_refill_tokens: 1
|
||||
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_type: "oidc" # or "basic"
|
||||
|
@ -30,13 +30,14 @@
|
||||
"commander": "^12.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^6.0.1",
|
||||
"shx": "^0.3.4",
|
||||
"@auto-it/all-contributors": "^11.3.0",
|
||||
"@auto-it/first-time-contributor": "^11.3.0",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@konobangu/typescript-config": "workspace:*",
|
||||
"@turbo/gen": "^2.3.3",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"rimraf": "^6.0.1",
|
||||
"shx": "^0.3.4",
|
||||
"tsx": "^4.19.2",
|
||||
"turbo": "^2.3.3",
|
||||
"typescript": "^5.7.2",
|
||||
|
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@ -30,6 +30,9 @@ importers:
|
||||
'@turbo/gen':
|
||||
specifier: ^2.3.3
|
||||
version: 2.3.3(@types/node@22.10.1)(typescript@5.7.2)
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.7
|
||||
version: 21.1.7
|
||||
rimraf:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
@ -4605,6 +4608,9 @@ packages:
|
||||
'@types/inquirer@6.5.0':
|
||||
resolution: {integrity: sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==}
|
||||
|
||||
'@types/jsdom@21.1.7':
|
||||
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
@ -4676,6 +4682,9 @@ packages:
|
||||
'@types/tinycolor2@1.4.6':
|
||||
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':
|
||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||
|
||||
@ -14170,6 +14179,12 @@ snapshots:
|
||||
'@types/through': 0.0.33
|
||||
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/lodash.merge@4.6.9':
|
||||
@ -14244,6 +14259,8 @@ snapshots:
|
||||
|
||||
'@types/tinycolor2@1.4.6': {}
|
||||
|
||||
'@types/tough-cookie@4.0.5': {}
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
Loading…
Reference in New Issue
Block a user