Compare commits
2 Commits
721eee9c88
...
6726cafff4
Author | SHA1 | Date | |
---|---|---|---|
6726cafff4 | |||
35312ea1ff |
26
Cargo.lock
generated
26
Cargo.lock
generated
@ -575,6 +575,7 @@ dependencies = [
|
|||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
@ -2543,6 +2544,30 @@ dependencies = [
|
|||||||
"hashbrown 0.15.4",
|
"hashbrown 0.15.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"headers-core",
|
||||||
|
"http",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"sha1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers-core"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@ -5236,6 +5261,7 @@ dependencies = [
|
|||||||
"lightningcss",
|
"lightningcss",
|
||||||
"log",
|
"log",
|
||||||
"maplit",
|
"maplit",
|
||||||
|
"mime_guess",
|
||||||
"mockito",
|
"mockito",
|
||||||
"moka",
|
"moka",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
|
@ -26,7 +26,6 @@ util-derive = { path = "./packages/util-derive" }
|
|||||||
fetch = { path = "./packages/fetch" }
|
fetch = { path = "./packages/fetch" }
|
||||||
downloader = { path = "./packages/downloader" }
|
downloader = { path = "./packages/downloader" }
|
||||||
recorder = { path = "./apps/recorder" }
|
recorder = { path = "./apps/recorder" }
|
||||||
proxy = { path = "./apps/proxy" }
|
|
||||||
|
|
||||||
reqwest = { version = "0.12.20", features = [
|
reqwest = { version = "0.12.20", features = [
|
||||||
"charset",
|
"charset",
|
||||||
@ -62,7 +61,7 @@ regex = "1.11"
|
|||||||
lazy_static = "1.5"
|
lazy_static = "1.5"
|
||||||
axum = { version = "0.8.3", features = ["macros"] }
|
axum = { version = "0.8.3", features = ["macros"] }
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||||
axum-extra = "0.10"
|
axum-extra = { version = "0.10", features = ["typed-header"] }
|
||||||
mockito = { version = "1.6.1" }
|
mockito = { version = "1.6.1" }
|
||||||
convert_case = "0.8"
|
convert_case = "0.8"
|
||||||
color-eyre = "0.6.5"
|
color-eyre = "0.6.5"
|
||||||
|
@ -1 +1 @@
|
|||||||
^https://mikanani.me/*** http://127.0.0.1:5005/$1
|
^https://mikanani.me/*** http://127.0.0.1:5005/$1 excludeFilter://^**/***.svg excludeFilter://^**/***.css excludeFilter://^**/***.js
|
@ -128,6 +128,7 @@ reqwest_cookie_store = "0.8.0"
|
|||||||
nanoid = "0.4.0"
|
nanoid = "0.4.0"
|
||||||
jwtk = "0.4.0"
|
jwtk = "0.4.0"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
|
mime_guess = "2.0.5"
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -4,17 +4,9 @@ use tokio::sync::OnceCell;
|
|||||||
|
|
||||||
use super::{Environment, config::AppConfig};
|
use super::{Environment, config::AppConfig};
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::AuthService,
|
auth::AuthService, cache::CacheService, crypto::CryptoService, database::DatabaseService,
|
||||||
cache::CacheService,
|
errors::RecorderResult, extract::mikan::MikanClient, graphql::GraphQLService,
|
||||||
crypto::CryptoService,
|
logger::LoggerService, message::MessageService, storage::StorageService, task::TaskService,
|
||||||
database::DatabaseService,
|
|
||||||
errors::RecorderResult,
|
|
||||||
extract::mikan::MikanClient,
|
|
||||||
graphql::GraphQLService,
|
|
||||||
logger::LoggerService,
|
|
||||||
message::MessageService,
|
|
||||||
storage::{StorageService, StorageServiceTrait},
|
|
||||||
task::TaskService,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait AppContextTrait: Send + Sync + Debug {
|
pub trait AppContextTrait: Send + Sync + Debug {
|
||||||
@ -25,7 +17,7 @@ pub trait AppContextTrait: Send + Sync + Debug {
|
|||||||
fn mikan(&self) -> &MikanClient;
|
fn mikan(&self) -> &MikanClient;
|
||||||
fn auth(&self) -> &AuthService;
|
fn auth(&self) -> &AuthService;
|
||||||
fn graphql(&self) -> &GraphQLService;
|
fn graphql(&self) -> &GraphQLService;
|
||||||
fn storage(&self) -> &dyn StorageServiceTrait;
|
fn storage(&self) -> &StorageService;
|
||||||
fn working_dir(&self) -> &String;
|
fn working_dir(&self) -> &String;
|
||||||
fn environment(&self) -> &Environment;
|
fn environment(&self) -> &Environment;
|
||||||
fn crypto(&self) -> &CryptoService;
|
fn crypto(&self) -> &CryptoService;
|
||||||
@ -126,7 +118,7 @@ impl AppContextTrait for AppContext {
|
|||||||
fn graphql(&self) -> &GraphQLService {
|
fn graphql(&self) -> &GraphQLService {
|
||||||
self.graphql.get().expect("graphql should be set")
|
self.graphql.get().expect("graphql should be set")
|
||||||
}
|
}
|
||||||
fn storage(&self) -> &dyn StorageServiceTrait {
|
fn storage(&self) -> &StorageService {
|
||||||
&self.storage
|
&self.storage
|
||||||
}
|
}
|
||||||
fn working_dir(&self) -> &String {
|
fn working_dir(&self) -> &String {
|
||||||
|
@ -6,7 +6,6 @@ use tracing::instrument;
|
|||||||
|
|
||||||
use super::{builder::AppBuilder, context::AppContextTrait};
|
use super::{builder::AppBuilder, context::AppContextTrait};
|
||||||
use crate::{
|
use crate::{
|
||||||
app::Environment,
|
|
||||||
errors::{RecorderError, RecorderResult},
|
errors::{RecorderError, RecorderResult},
|
||||||
web::{
|
web::{
|
||||||
controller::{self, core::ControllerTrait},
|
controller::{self, core::ControllerTrait},
|
||||||
@ -52,13 +51,14 @@ impl App {
|
|||||||
|
|
||||||
let mut router = Router::<Arc<dyn AppContextTrait>>::new();
|
let mut router = Router::<Arc<dyn AppContextTrait>>::new();
|
||||||
|
|
||||||
let (graphql_c, oidc_c, metadata_c) = futures::try_join!(
|
let (graphql_c, oidc_c, metadata_c, static_c) = futures::try_join!(
|
||||||
controller::graphql::create(context.clone()),
|
controller::graphql::create(context.clone()),
|
||||||
controller::oidc::create(context.clone()),
|
controller::oidc::create(context.clone()),
|
||||||
controller::metadata::create(context.clone())
|
controller::metadata::create(context.clone()),
|
||||||
|
controller::r#static::create(context.clone()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for c in [graphql_c, oidc_c, metadata_c] {
|
for c in [graphql_c, oidc_c, metadata_c, static_c] {
|
||||||
router = c.apply_to(router);
|
router = c.apply_to(router);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,9 +86,6 @@ impl App {
|
|||||||
async {
|
async {
|
||||||
{
|
{
|
||||||
let monitor = task.setup_monitor().await?;
|
let monitor = task.setup_monitor().await?;
|
||||||
if matches!(context.environment(), Environment::Development) {
|
|
||||||
monitor.run().await?;
|
|
||||||
} else {
|
|
||||||
monitor
|
monitor
|
||||||
.run_with_signal(async move {
|
.run_with_signal(async move {
|
||||||
Self::shutdown_signal().await;
|
Self::shutdown_signal().await;
|
||||||
@ -97,7 +94,6 @@ impl App {
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<(), RecorderError>(())
|
Ok::<(), RecorderError>(())
|
||||||
},
|
},
|
||||||
|
@ -11,13 +11,14 @@ use openidconnect::{
|
|||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use snafu::prelude::*;
|
use snafu::prelude::*;
|
||||||
use util::OptDynErr;
|
|
||||||
|
|
||||||
use crate::models::auth::AuthType;
|
use crate::models::auth::AuthType;
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
#[snafu(visibility(pub(crate)))]
|
#[snafu(visibility(pub(crate)))]
|
||||||
pub enum AuthError {
|
pub enum AuthError {
|
||||||
|
#[snafu(display("Permission denied"))]
|
||||||
|
PermissionError,
|
||||||
#[snafu(display("Not support auth method"))]
|
#[snafu(display("Not support auth method"))]
|
||||||
NotSupportAuthMethod {
|
NotSupportAuthMethod {
|
||||||
supported: Vec<AuthType>,
|
supported: Vec<AuthType>,
|
||||||
@ -93,12 +94,6 @@ pub enum AuthError {
|
|||||||
column: String,
|
column: String,
|
||||||
context_path: String,
|
context_path: String,
|
||||||
},
|
},
|
||||||
#[snafu(display("GraphQL permission denied since {field}"))]
|
|
||||||
GraphqlStaticPermissionError {
|
|
||||||
#[snafu(source)]
|
|
||||||
source: OptDynErr,
|
|
||||||
field: String,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthError {
|
impl AuthError {
|
||||||
|
@ -5,8 +5,7 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use fetch::{FetchError, HttpClientError, reqwest, reqwest_middleware};
|
use fetch::{FetchError, HttpClientError, reqwest, reqwest_middleware};
|
||||||
use http::StatusCode;
|
use http::{HeaderMap, StatusCode};
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
|
||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -19,6 +18,19 @@ use crate::{
|
|||||||
#[derive(Snafu, Debug)]
|
#[derive(Snafu, Debug)]
|
||||||
#[snafu(visibility(pub(crate)))]
|
#[snafu(visibility(pub(crate)))]
|
||||||
pub enum RecorderError {
|
pub enum RecorderError {
|
||||||
|
#[snafu(display(
|
||||||
|
"HTTP {status} {reason}, source = {source:?}",
|
||||||
|
status = status,
|
||||||
|
reason = status.canonical_reason().unwrap_or("Unknown")
|
||||||
|
))]
|
||||||
|
HttpResponseError {
|
||||||
|
status: StatusCode,
|
||||||
|
headers: Option<HeaderMap>,
|
||||||
|
#[snafu(source(from(Box<dyn std::error::Error + Send + Sync>, OptDynErr::some)))]
|
||||||
|
source: OptDynErr,
|
||||||
|
},
|
||||||
|
#[snafu(transparent, context(false))]
|
||||||
|
HttpError { source: http::Error },
|
||||||
#[snafu(transparent, context(false))]
|
#[snafu(transparent, context(false))]
|
||||||
FancyRegexError {
|
FancyRegexError {
|
||||||
#[snafu(source(from(fancy_regex::Error, Box::new)))]
|
#[snafu(source(from(fancy_regex::Error, Box::new)))]
|
||||||
@ -28,12 +40,10 @@ pub enum RecorderError {
|
|||||||
NetAddrParseError { source: std::net::AddrParseError },
|
NetAddrParseError { source: std::net::AddrParseError },
|
||||||
#[snafu(transparent)]
|
#[snafu(transparent)]
|
||||||
RegexError { source: regex::Error },
|
RegexError { source: regex::Error },
|
||||||
#[snafu(transparent)]
|
#[snafu(display("Invalid method"))]
|
||||||
InvalidMethodError { source: http::method::InvalidMethod },
|
InvalidMethodError,
|
||||||
#[snafu(transparent)]
|
#[snafu(display("Invalid header name"))]
|
||||||
InvalidHeaderNameError {
|
InvalidHeaderNameError,
|
||||||
source: http::header::InvalidHeaderName,
|
|
||||||
},
|
|
||||||
#[snafu(transparent)]
|
#[snafu(transparent)]
|
||||||
TracingAppenderInitError {
|
TracingAppenderInitError {
|
||||||
source: tracing_appender::rolling::InitError,
|
source: tracing_appender::rolling::InitError,
|
||||||
@ -72,10 +82,8 @@ pub enum RecorderError {
|
|||||||
#[snafu(source(from(opendal::Error, Box::new)))]
|
#[snafu(source(from(opendal::Error, Box::new)))]
|
||||||
source: Box<opendal::Error>,
|
source: Box<opendal::Error>,
|
||||||
},
|
},
|
||||||
#[snafu(transparent)]
|
#[snafu(display("Invalid header value"))]
|
||||||
InvalidHeaderValueError {
|
InvalidHeaderValueError,
|
||||||
source: http::header::InvalidHeaderValue,
|
|
||||||
},
|
|
||||||
#[snafu(transparent)]
|
#[snafu(transparent)]
|
||||||
HttpClientError { source: HttpClientError },
|
HttpClientError { source: HttpClientError },
|
||||||
#[cfg(feature = "testcontainers")]
|
#[cfg(feature = "testcontainers")]
|
||||||
@ -128,6 +136,22 @@ pub enum RecorderError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RecorderError {
|
impl RecorderError {
|
||||||
|
pub fn from_status(status: StatusCode) -> Self {
|
||||||
|
Self::HttpResponseError {
|
||||||
|
status,
|
||||||
|
headers: None,
|
||||||
|
source: None.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_status_and_headers(status: StatusCode, headers: HeaderMap) -> Self {
|
||||||
|
Self::HttpResponseError {
|
||||||
|
status,
|
||||||
|
headers: Some(headers),
|
||||||
|
source: None.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_mikan_meta_missing_field(field: Cow<'static, str>) -> Self {
|
pub fn from_mikan_meta_missing_field(field: Cow<'static, str>) -> Self {
|
||||||
Self::MikanMetaMissingFieldError {
|
Self::MikanMetaMissingFieldError {
|
||||||
field,
|
field,
|
||||||
@ -177,10 +201,48 @@ impl snafu::FromString for RecorderError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<StatusCode> for RecorderError {
|
||||||
|
fn from(status: StatusCode) -> Self {
|
||||||
|
Self::HttpResponseError {
|
||||||
|
status,
|
||||||
|
headers: None,
|
||||||
|
source: None.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(StatusCode, HeaderMap)> for RecorderError {
|
||||||
|
fn from((status, headers): (StatusCode, HeaderMap)) -> Self {
|
||||||
|
Self::HttpResponseError {
|
||||||
|
status,
|
||||||
|
headers: Some(headers),
|
||||||
|
source: None.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IntoResponse for RecorderError {
|
impl IntoResponse for RecorderError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
match self {
|
match self {
|
||||||
Self::AuthError { source: auth_error } => auth_error.into_response(),
|
Self::AuthError { source: auth_error } => auth_error.into_response(),
|
||||||
|
Self::HttpResponseError {
|
||||||
|
status,
|
||||||
|
headers,
|
||||||
|
source,
|
||||||
|
} => {
|
||||||
|
let message = source
|
||||||
|
.into_inner()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
String::from(status.canonical_reason().unwrap_or("Unknown"))
|
||||||
|
});
|
||||||
|
(
|
||||||
|
status,
|
||||||
|
headers,
|
||||||
|
Json::<StandardErrorResponse>(StandardErrorResponse::from(message)),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
err => (
|
err => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json::<StandardErrorResponse>(StandardErrorResponse::from(err.to_string())),
|
Json::<StandardErrorResponse>(StandardErrorResponse::from(err.to_string())),
|
||||||
@ -190,28 +252,6 @@ impl IntoResponse for RecorderError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for RecorderError {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for RecorderError {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s = String::deserialize(deserializer)?;
|
|
||||||
Ok(Self::Whatever {
|
|
||||||
message: s,
|
|
||||||
source: None.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<reqwest::Error> for RecorderError {
|
impl From<reqwest::Error> for RecorderError {
|
||||||
fn from(error: reqwest::Error) -> Self {
|
fn from(error: reqwest::Error) -> Self {
|
||||||
FetchError::from(error).into()
|
FetchError::from(error).into()
|
||||||
@ -224,4 +264,22 @@ impl From<reqwest_middleware::Error> for RecorderError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<http::header::InvalidHeaderValue> for RecorderError {
|
||||||
|
fn from(_error: http::header::InvalidHeaderValue) -> Self {
|
||||||
|
Self::InvalidHeaderValueError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<http::header::InvalidHeaderName> for RecorderError {
|
||||||
|
fn from(_error: http::header::InvalidHeaderName) -> Self {
|
||||||
|
Self::InvalidHeaderNameError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<http::method::InvalidMethod> for RecorderError {
|
||||||
|
fn from(_error: http::method::InvalidMethod) -> Self {
|
||||||
|
Self::InvalidMethodError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type RecorderResult<T> = Result<T, RecorderError>;
|
pub type RecorderResult<T> = Result<T, RecorderError>;
|
||||||
|
@ -268,8 +268,8 @@ mod tests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_torrent_ep_parser(raw_name: &str, expected: &str) {
|
pub fn test_torrent_ep_parser(origin_name: &str, expected: &str) {
|
||||||
let extname = Path::new(raw_name)
|
let extname = Path::new(origin_name)
|
||||||
.extension()
|
.extension()
|
||||||
.map(|e| format!(".{e}"))
|
.map(|e| format!(".{e}"))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@ -278,7 +278,7 @@ mod tests {
|
|||||||
if extname == ".srt" || extname == ".ass" {
|
if extname == ".srt" || extname == ".ass" {
|
||||||
let expected: Option<TorrentEpisodeSubtitleMeta> = serde_json::from_str(expected).ok();
|
let expected: Option<TorrentEpisodeSubtitleMeta> = serde_json::from_str(expected).ok();
|
||||||
let found_raw =
|
let found_raw =
|
||||||
parse_episode_subtitle_meta_from_torrent(Path::new(raw_name), None, None);
|
parse_episode_subtitle_meta_from_torrent(Path::new(origin_name), None, None);
|
||||||
let found = found_raw.as_ref().ok().cloned();
|
let found = found_raw.as_ref().ok().cloned();
|
||||||
|
|
||||||
if expected != found {
|
if expected != found {
|
||||||
@ -299,7 +299,8 @@ mod tests {
|
|||||||
assert_eq!(expected, found);
|
assert_eq!(expected, found);
|
||||||
} else {
|
} else {
|
||||||
let expected: Option<TorrentEpisodeMediaMeta> = serde_json::from_str(expected).ok();
|
let expected: Option<TorrentEpisodeMediaMeta> = serde_json::from_str(expected).ok();
|
||||||
let found_raw = parse_episode_media_meta_from_torrent(Path::new(raw_name), None, None);
|
let found_raw =
|
||||||
|
parse_episode_media_meta_from_torrent(Path::new(origin_name), None, None);
|
||||||
let found = found_raw.as_ref().ok().cloned();
|
let found = found_raw.as_ref().ok().cloned();
|
||||||
|
|
||||||
if expected != found {
|
if expected != found {
|
||||||
|
@ -28,7 +28,7 @@ use crate::{
|
|||||||
MIKAN_YEAR_QUERY_KEY, MikanClient,
|
MIKAN_YEAR_QUERY_KEY, MikanClient,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
storage::{StorageContentCategory, StorageServiceTrait},
|
storage::{StorageContentCategory, StorageService},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
@ -739,18 +739,18 @@ pub async fn scrape_mikan_poster_data_from_image_url(
|
|||||||
#[instrument(skip_all, fields(origin_poster_src_url = origin_poster_src_url.as_str()))]
|
#[instrument(skip_all, fields(origin_poster_src_url = origin_poster_src_url.as_str()))]
|
||||||
pub async fn scrape_mikan_poster_meta_from_image_url(
|
pub async fn scrape_mikan_poster_meta_from_image_url(
|
||||||
mikan_client: &MikanClient,
|
mikan_client: &MikanClient,
|
||||||
storage_service: &dyn StorageServiceTrait,
|
storage_service: &StorageService,
|
||||||
origin_poster_src_url: Url,
|
origin_poster_src_url: Url,
|
||||||
subscriber_id: i32,
|
|
||||||
) -> RecorderResult<MikanBangumiPosterMeta> {
|
) -> RecorderResult<MikanBangumiPosterMeta> {
|
||||||
if let Some(poster_src) = storage_service
|
if let Some(poster_src) = storage_service
|
||||||
.exists_object(
|
.exists(
|
||||||
|
storage_service.build_public_object_path(
|
||||||
StorageContentCategory::Image,
|
StorageContentCategory::Image,
|
||||||
subscriber_id,
|
MIKAN_POSTER_BUCKET_KEY,
|
||||||
Some(MIKAN_POSTER_BUCKET_KEY),
|
|
||||||
&origin_poster_src_url
|
&origin_poster_src_url
|
||||||
.path()
|
.path()
|
||||||
.replace(&format!("{MIKAN_BANGUMI_POSTER_PATH}/"), ""),
|
.replace(&format!("{MIKAN_BANGUMI_POSTER_PATH}/"), ""),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
@ -765,13 +765,14 @@ pub async fn scrape_mikan_poster_meta_from_image_url(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let poster_str = storage_service
|
let poster_str = storage_service
|
||||||
.store_object(
|
.write(
|
||||||
|
storage_service.build_public_object_path(
|
||||||
StorageContentCategory::Image,
|
StorageContentCategory::Image,
|
||||||
subscriber_id,
|
MIKAN_POSTER_BUCKET_KEY,
|
||||||
Some(MIKAN_POSTER_BUCKET_KEY),
|
|
||||||
&origin_poster_src_url
|
&origin_poster_src_url
|
||||||
.path()
|
.path()
|
||||||
.replace(&format!("{MIKAN_BANGUMI_POSTER_PATH}/"), ""),
|
.replace(&format!("{MIKAN_BANGUMI_POSTER_PATH}/"), ""),
|
||||||
|
),
|
||||||
poster_data,
|
poster_data,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -1080,16 +1081,14 @@ mod test {
|
|||||||
&mikan_client,
|
&mikan_client,
|
||||||
&storage_service,
|
&storage_service,
|
||||||
bangumi_poster_url,
|
bangumi_poster_url,
|
||||||
1,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
resources_mock.shared_resource_mock.expect(1);
|
resources_mock.shared_resource_mock.expect(1);
|
||||||
|
|
||||||
let storage_fullname = storage_service.get_fullname(
|
let storage_fullname = storage_service.build_public_object_path(
|
||||||
StorageContentCategory::Image,
|
StorageContentCategory::Image,
|
||||||
1,
|
MIKAN_POSTER_BUCKET_KEY,
|
||||||
Some(MIKAN_POSTER_BUCKET_KEY),
|
|
||||||
"202309/5ce9fed1.jpg",
|
"202309/5ce9fed1.jpg",
|
||||||
);
|
);
|
||||||
let storage_fullename_str = storage_fullname.as_str();
|
let storage_fullename_str = storage_fullname.as_str();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
pub mod bittorrent;
|
||||||
pub mod defs;
|
pub mod defs;
|
||||||
pub mod html;
|
pub mod html;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod media;
|
pub mod media;
|
||||||
pub mod mikan;
|
pub mod mikan;
|
||||||
pub mod rawname;
|
pub mod origin;
|
||||||
pub mod bittorrent;
|
|
||||||
|
5
apps/recorder/src/extract/origin/mod.rs
Normal file
5
apps/recorder/src/extract/origin/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod parser;
|
||||||
|
|
||||||
|
pub use parser::{
|
||||||
|
RawEpisodeMeta, extract_episode_meta_from_origin_name, extract_season_from_title_body,
|
||||||
|
};
|
@ -261,7 +261,7 @@ pub fn check_is_movie(title: &str) -> bool {
|
|||||||
MOVIE_TITLE_RE.is_match(title)
|
MOVIE_TITLE_RE.is_match(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_episode_meta_from_raw_name(s: &str) -> RecorderResult<RawEpisodeMeta> {
|
pub fn extract_episode_meta_from_origin_name(s: &str) -> RecorderResult<RawEpisodeMeta> {
|
||||||
let raw_title = s.trim();
|
let raw_title = s.trim();
|
||||||
let raw_title_without_ch_brackets = replace_ch_bracket_to_en(raw_title);
|
let raw_title_without_ch_brackets = replace_ch_bracket_to_en(raw_title);
|
||||||
let fansub = extract_fansub(&raw_title_without_ch_brackets);
|
let fansub = extract_fansub(&raw_title_without_ch_brackets);
|
||||||
@ -321,11 +321,11 @@ pub fn extract_episode_meta_from_raw_name(s: &str) -> RecorderResult<RawEpisodeM
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::{RawEpisodeMeta, extract_episode_meta_from_raw_name};
|
use super::{RawEpisodeMeta, extract_episode_meta_from_origin_name};
|
||||||
|
|
||||||
fn test_raw_ep_parser_case(raw_name: &str, expected: &str) {
|
fn test_raw_ep_parser_case(raw_name: &str, expected: &str) {
|
||||||
let expected: Option<RawEpisodeMeta> = serde_json::from_str(expected).unwrap_or_default();
|
let expected: Option<RawEpisodeMeta> = serde_json::from_str(expected).unwrap_or_default();
|
||||||
let found = extract_episode_meta_from_raw_name(raw_name).ok();
|
let found = extract_episode_meta_from_origin_name(raw_name).ok();
|
||||||
|
|
||||||
if expected != found {
|
if expected != found {
|
||||||
println!(
|
println!(
|
@ -1,5 +0,0 @@
|
|||||||
pub mod parser;
|
|
||||||
|
|
||||||
pub use parser::{
|
|
||||||
RawEpisodeMeta, extract_episode_meta_from_raw_name, extract_season_from_title_body,
|
|
||||||
};
|
|
@ -43,7 +43,7 @@ pub enum Bangumi {
|
|||||||
MikanBangumiId,
|
MikanBangumiId,
|
||||||
DisplayName,
|
DisplayName,
|
||||||
SubscriberId,
|
SubscriberId,
|
||||||
RawName,
|
OriginName,
|
||||||
Season,
|
Season,
|
||||||
SeasonRaw,
|
SeasonRaw,
|
||||||
Fansub,
|
Fansub,
|
||||||
@ -51,6 +51,7 @@ pub enum Bangumi {
|
|||||||
Filter,
|
Filter,
|
||||||
RssLink,
|
RssLink,
|
||||||
PosterLink,
|
PosterLink,
|
||||||
|
OriginPosterLink,
|
||||||
SavePath,
|
SavePath,
|
||||||
Homepage,
|
Homepage,
|
||||||
}
|
}
|
||||||
@ -69,7 +70,7 @@ pub enum Episodes {
|
|||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
MikanEpisodeId,
|
MikanEpisodeId,
|
||||||
RawName,
|
OriginName,
|
||||||
DisplayName,
|
DisplayName,
|
||||||
BangumiId,
|
BangumiId,
|
||||||
SubscriberId,
|
SubscriberId,
|
||||||
@ -80,6 +81,7 @@ pub enum Episodes {
|
|||||||
SeasonRaw,
|
SeasonRaw,
|
||||||
Fansub,
|
Fansub,
|
||||||
PosterLink,
|
PosterLink,
|
||||||
|
OriginPosterLink,
|
||||||
EpisodeIndex,
|
EpisodeIndex,
|
||||||
Homepage,
|
Homepage,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
@ -100,7 +102,7 @@ pub enum SubscriptionEpisode {
|
|||||||
pub enum Downloads {
|
pub enum Downloads {
|
||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
RawName,
|
OriginName,
|
||||||
DisplayName,
|
DisplayName,
|
||||||
SubscriberId,
|
SubscriberId,
|
||||||
DownloaderId,
|
DownloaderId,
|
||||||
|
@ -96,7 +96,7 @@ impl MigrationTrait for Migration {
|
|||||||
.col(text_null(Bangumi::MikanBangumiId))
|
.col(text_null(Bangumi::MikanBangumiId))
|
||||||
.col(integer(Bangumi::SubscriberId))
|
.col(integer(Bangumi::SubscriberId))
|
||||||
.col(text(Bangumi::DisplayName))
|
.col(text(Bangumi::DisplayName))
|
||||||
.col(text(Bangumi::RawName))
|
.col(text(Bangumi::OriginName))
|
||||||
.col(integer(Bangumi::Season))
|
.col(integer(Bangumi::Season))
|
||||||
.col(text_null(Bangumi::SeasonRaw))
|
.col(text_null(Bangumi::SeasonRaw))
|
||||||
.col(text_null(Bangumi::Fansub))
|
.col(text_null(Bangumi::Fansub))
|
||||||
@ -104,6 +104,7 @@ impl MigrationTrait for Migration {
|
|||||||
.col(json_binary_null(Bangumi::Filter))
|
.col(json_binary_null(Bangumi::Filter))
|
||||||
.col(text_null(Bangumi::RssLink))
|
.col(text_null(Bangumi::RssLink))
|
||||||
.col(text_null(Bangumi::PosterLink))
|
.col(text_null(Bangumi::PosterLink))
|
||||||
|
.col(text_null(Bangumi::OriginPosterLink))
|
||||||
.col(text_null(Bangumi::SavePath))
|
.col(text_null(Bangumi::SavePath))
|
||||||
.col(text_null(Bangumi::Homepage))
|
.col(text_null(Bangumi::Homepage))
|
||||||
.foreign_key(
|
.foreign_key(
|
||||||
@ -220,7 +221,7 @@ impl MigrationTrait for Migration {
|
|||||||
table_auto_z(Episodes::Table)
|
table_auto_z(Episodes::Table)
|
||||||
.col(pk_auto(Episodes::Id))
|
.col(pk_auto(Episodes::Id))
|
||||||
.col(text_null(Episodes::MikanEpisodeId))
|
.col(text_null(Episodes::MikanEpisodeId))
|
||||||
.col(text(Episodes::RawName))
|
.col(text(Episodes::OriginName))
|
||||||
.col(text(Episodes::DisplayName))
|
.col(text(Episodes::DisplayName))
|
||||||
.col(integer(Episodes::BangumiId))
|
.col(integer(Episodes::BangumiId))
|
||||||
.col(integer(Episodes::SubscriberId))
|
.col(integer(Episodes::SubscriberId))
|
||||||
@ -230,6 +231,7 @@ impl MigrationTrait for Migration {
|
|||||||
.col(text_null(Episodes::SeasonRaw))
|
.col(text_null(Episodes::SeasonRaw))
|
||||||
.col(text_null(Episodes::Fansub))
|
.col(text_null(Episodes::Fansub))
|
||||||
.col(text_null(Episodes::PosterLink))
|
.col(text_null(Episodes::PosterLink))
|
||||||
|
.col(text_null(Episodes::OriginPosterLink))
|
||||||
.col(integer(Episodes::EpisodeIndex))
|
.col(integer(Episodes::EpisodeIndex))
|
||||||
.col(text_null(Episodes::Homepage))
|
.col(text_null(Episodes::Homepage))
|
||||||
.col(text_null(Episodes::Subtitle))
|
.col(text_null(Episodes::Subtitle))
|
||||||
|
@ -80,7 +80,7 @@ impl MigrationTrait for Migration {
|
|||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(Downloads::Table)
|
table_auto_z(Downloads::Table)
|
||||||
.col(pk_auto(Downloads::Id))
|
.col(pk_auto(Downloads::Id))
|
||||||
.col(string(Downloads::RawName))
|
.col(string(Downloads::OriginName))
|
||||||
.col(string(Downloads::DisplayName))
|
.col(string(Downloads::DisplayName))
|
||||||
.col(integer(Downloads::SubscriberId))
|
.col(integer(Downloads::SubscriberId))
|
||||||
.col(integer(Downloads::DownloaderId))
|
.col(integer(Downloads::DownloaderId))
|
||||||
|
@ -17,7 +17,7 @@ use crate::{
|
|||||||
MikanBangumiHash, MikanBangumiMeta, build_mikan_bangumi_subscription_rss_url,
|
MikanBangumiHash, MikanBangumiMeta, build_mikan_bangumi_subscription_rss_url,
|
||||||
scrape_mikan_poster_meta_from_image_url,
|
scrape_mikan_poster_meta_from_image_url,
|
||||||
},
|
},
|
||||||
rawname::extract_season_from_title_body,
|
origin::extract_season_from_title_body,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ pub struct Model {
|
|||||||
pub mikan_bangumi_id: Option<String>,
|
pub mikan_bangumi_id: Option<String>,
|
||||||
pub subscriber_id: i32,
|
pub subscriber_id: i32,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub raw_name: String,
|
pub origin_name: String,
|
||||||
pub season: i32,
|
pub season: i32,
|
||||||
pub season_raw: Option<String>,
|
pub season_raw: Option<String>,
|
||||||
pub fansub: Option<String>,
|
pub fansub: Option<String>,
|
||||||
@ -49,6 +49,7 @@ pub struct Model {
|
|||||||
pub filter: Option<BangumiFilter>,
|
pub filter: Option<BangumiFilter>,
|
||||||
pub rss_link: Option<String>,
|
pub rss_link: Option<String>,
|
||||||
pub poster_link: Option<String>,
|
pub poster_link: Option<String>,
|
||||||
|
pub origin_poster_link: Option<String>,
|
||||||
pub save_path: Option<String>,
|
pub save_path: Option<String>,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
}
|
}
|
||||||
@ -130,12 +131,11 @@ impl ActiveModel {
|
|||||||
Some(&meta.mikan_fansub_id),
|
Some(&meta.mikan_fansub_id),
|
||||||
);
|
);
|
||||||
|
|
||||||
let poster_link = if let Some(origin_poster_src) = meta.origin_poster_src {
|
let poster_link = if let Some(origin_poster_src) = meta.origin_poster_src.clone() {
|
||||||
let poster_meta = scrape_mikan_poster_meta_from_image_url(
|
let poster_meta = scrape_mikan_poster_meta_from_image_url(
|
||||||
mikan_client,
|
mikan_client,
|
||||||
storage_service,
|
storage_service,
|
||||||
origin_poster_src,
|
origin_poster_src,
|
||||||
subscriber_id,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
poster_meta.poster_src
|
poster_meta.poster_src
|
||||||
@ -148,11 +148,12 @@ impl ActiveModel {
|
|||||||
mikan_fansub_id: ActiveValue::Set(Some(meta.mikan_fansub_id)),
|
mikan_fansub_id: ActiveValue::Set(Some(meta.mikan_fansub_id)),
|
||||||
subscriber_id: ActiveValue::Set(subscriber_id),
|
subscriber_id: ActiveValue::Set(subscriber_id),
|
||||||
display_name: ActiveValue::Set(meta.bangumi_title.clone()),
|
display_name: ActiveValue::Set(meta.bangumi_title.clone()),
|
||||||
raw_name: ActiveValue::Set(meta.bangumi_title),
|
origin_name: ActiveValue::Set(meta.bangumi_title),
|
||||||
season: ActiveValue::Set(season_index),
|
season: ActiveValue::Set(season_index),
|
||||||
season_raw: ActiveValue::Set(season_raw),
|
season_raw: ActiveValue::Set(season_raw),
|
||||||
fansub: ActiveValue::Set(Some(meta.fansub)),
|
fansub: ActiveValue::Set(Some(meta.fansub)),
|
||||||
poster_link: ActiveValue::Set(poster_link),
|
poster_link: ActiveValue::Set(poster_link),
|
||||||
|
origin_poster_link: ActiveValue::Set(meta.origin_poster_src.map(|src| src.to_string())),
|
||||||
homepage: ActiveValue::Set(Some(meta.homepage.to_string())),
|
homepage: ActiveValue::Set(Some(meta.homepage.to_string())),
|
||||||
rss_link: ActiveValue::Set(Some(rss_url.to_string())),
|
rss_link: ActiveValue::Set(Some(rss_url.to_string())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@ -228,7 +229,7 @@ impl Model {
|
|||||||
Column::SubscriberId,
|
Column::SubscriberId,
|
||||||
])
|
])
|
||||||
.update_columns([
|
.update_columns([
|
||||||
Column::RawName,
|
Column::OriginName,
|
||||||
Column::Fansub,
|
Column::Fansub,
|
||||||
Column::PosterLink,
|
Column::PosterLink,
|
||||||
Column::Season,
|
Column::Season,
|
||||||
|
@ -44,7 +44,7 @@ pub struct Model {
|
|||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub raw_name: String,
|
pub origin_name: String,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub downloader_id: i32,
|
pub downloader_id: i32,
|
||||||
pub episode_id: i32,
|
pub episode_id: i32,
|
||||||
|
@ -10,7 +10,7 @@ use crate::{
|
|||||||
errors::RecorderResult,
|
errors::RecorderResult,
|
||||||
extract::{
|
extract::{
|
||||||
mikan::{MikanEpisodeHash, MikanEpisodeMeta, build_mikan_episode_homepage_url},
|
mikan::{MikanEpisodeHash, MikanEpisodeMeta, build_mikan_episode_homepage_url},
|
||||||
rawname::extract_episode_meta_from_raw_name,
|
origin::extract_episode_meta_from_origin_name,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ pub struct Model {
|
|||||||
pub id: i32,
|
pub id: i32,
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub mikan_episode_id: Option<String>,
|
pub mikan_episode_id: Option<String>,
|
||||||
pub raw_name: String,
|
pub origin_name: String,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub bangumi_id: i32,
|
pub bangumi_id: i32,
|
||||||
pub subscriber_id: i32,
|
pub subscriber_id: i32,
|
||||||
@ -35,6 +35,7 @@ pub struct Model {
|
|||||||
pub season_raw: Option<String>,
|
pub season_raw: Option<String>,
|
||||||
pub fansub: Option<String>,
|
pub fansub: Option<String>,
|
||||||
pub poster_link: Option<String>,
|
pub poster_link: Option<String>,
|
||||||
|
pub origin_poster_link: Option<String>,
|
||||||
pub episode_index: i32,
|
pub episode_index: i32,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
pub subtitle: Option<String>,
|
pub subtitle: Option<String>,
|
||||||
@ -123,7 +124,7 @@ impl ActiveModel {
|
|||||||
episode: MikanEpisodeMeta,
|
episode: MikanEpisodeMeta,
|
||||||
) -> RecorderResult<Self> {
|
) -> RecorderResult<Self> {
|
||||||
let mikan_base_url = ctx.mikan().base_url().clone();
|
let mikan_base_url = ctx.mikan().base_url().clone();
|
||||||
let episode_extention_meta = extract_episode_meta_from_raw_name(&episode.episode_title)
|
let episode_extention_meta = extract_episode_meta_from_origin_name(&episode.episode_title)
|
||||||
.inspect_err(|err| {
|
.inspect_err(|err| {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
err = ?err,
|
err = ?err,
|
||||||
@ -136,7 +137,7 @@ impl ActiveModel {
|
|||||||
|
|
||||||
let mut episode_active_model = Self {
|
let mut episode_active_model = Self {
|
||||||
mikan_episode_id: ActiveValue::Set(Some(episode.mikan_episode_id)),
|
mikan_episode_id: ActiveValue::Set(Some(episode.mikan_episode_id)),
|
||||||
raw_name: ActiveValue::Set(episode.episode_title.clone()),
|
origin_name: ActiveValue::Set(episode.episode_title.clone()),
|
||||||
display_name: ActiveValue::Set(episode.episode_title.clone()),
|
display_name: ActiveValue::Set(episode.episode_title.clone()),
|
||||||
bangumi_id: ActiveValue::Set(bangumi.id),
|
bangumi_id: ActiveValue::Set(bangumi.id),
|
||||||
subscriber_id: ActiveValue::Set(bangumi.subscriber_id),
|
subscriber_id: ActiveValue::Set(bangumi.subscriber_id),
|
||||||
@ -145,6 +146,7 @@ impl ActiveModel {
|
|||||||
season: ActiveValue::Set(bangumi.season),
|
season: ActiveValue::Set(bangumi.season),
|
||||||
fansub: ActiveValue::Set(bangumi.fansub.clone()),
|
fansub: ActiveValue::Set(bangumi.fansub.clone()),
|
||||||
poster_link: ActiveValue::Set(bangumi.poster_link.clone()),
|
poster_link: ActiveValue::Set(bangumi.poster_link.clone()),
|
||||||
|
origin_poster_link: ActiveValue::Set(bangumi.origin_poster_link.clone()),
|
||||||
episode_index: ActiveValue::Set(0),
|
episode_index: ActiveValue::Set(0),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@ -231,7 +233,7 @@ impl Model {
|
|||||||
let new_episode_ids = Entity::insert_many(new_episode_active_modes)
|
let new_episode_ids = Entity::insert_many(new_episode_active_modes)
|
||||||
.on_conflict(
|
.on_conflict(
|
||||||
OnConflict::columns([Column::MikanEpisodeId, Column::SubscriberId])
|
OnConflict::columns([Column::MikanEpisodeId, Column::SubscriberId])
|
||||||
.update_columns([Column::RawName, Column::PosterLink, Column::Homepage])
|
.update_columns([Column::OriginName, Column::PosterLink, Column::Homepage])
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.exec_with_returning_columns(db, [Column::Id])
|
.exec_with_returning_columns(db, [Column::Id])
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use async_stream::try_stream;
|
||||||
|
use axum::{body::Body, response::Response};
|
||||||
|
use axum_extra::{TypedHeader, headers::Range};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use opendal::{Buffer, Operator, layers::LoggingLayer};
|
use futures::{Stream, StreamExt};
|
||||||
|
use http::{HeaderValue, StatusCode, header};
|
||||||
|
use opendal::{Buffer, Metadata, Operator, Reader, Writer, layers::LoggingLayer};
|
||||||
use quirks_path::{Path, PathBuf};
|
use quirks_path::{Path, PathBuf};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::instrument;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::StorageConfig;
|
use super::StorageConfig;
|
||||||
use crate::errors::app_error::RecorderResult;
|
use crate::{
|
||||||
|
errors::{RecorderError, RecorderResult},
|
||||||
|
utils::http::{bound_range_to_content_range, build_no_satisfiable_content_range},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@ -43,88 +53,6 @@ impl fmt::Display for StorageStoredUrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
pub trait StorageServiceTrait: Sync {
|
|
||||||
fn get_operator(&self) -> RecorderResult<Operator>;
|
|
||||||
|
|
||||||
fn get_fullname(
|
|
||||||
&self,
|
|
||||||
content_category: StorageContentCategory,
|
|
||||||
subscriber_id: i32,
|
|
||||||
bucket: Option<&str>,
|
|
||||||
filename: &str,
|
|
||||||
) -> PathBuf {
|
|
||||||
[
|
|
||||||
&subscriber_id.to_string(),
|
|
||||||
content_category.as_ref(),
|
|
||||||
bucket.unwrap_or_default(),
|
|
||||||
filename,
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.map(Path::new)
|
|
||||||
.collect::<PathBuf>()
|
|
||||||
}
|
|
||||||
async fn store_object(
|
|
||||||
&self,
|
|
||||||
content_category: StorageContentCategory,
|
|
||||||
subscriber_id: i32,
|
|
||||||
bucket: Option<&str>,
|
|
||||||
filename: &str,
|
|
||||||
data: Bytes,
|
|
||||||
) -> RecorderResult<StorageStoredUrl> {
|
|
||||||
let fullname = self.get_fullname(content_category, subscriber_id, bucket, filename);
|
|
||||||
|
|
||||||
let operator = self.get_operator()?;
|
|
||||||
|
|
||||||
if let Some(dirname) = fullname.parent() {
|
|
||||||
let dirname = dirname.join("/");
|
|
||||||
operator.create_dir(dirname.as_str()).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
operator.write(fullname.as_str(), data).await?;
|
|
||||||
|
|
||||||
Ok(StorageStoredUrl::RelativePath {
|
|
||||||
path: fullname.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn exists_object(
|
|
||||||
&self,
|
|
||||||
content_category: StorageContentCategory,
|
|
||||||
subscriber_id: i32,
|
|
||||||
bucket: Option<&str>,
|
|
||||||
filename: &str,
|
|
||||||
) -> RecorderResult<Option<StorageStoredUrl>> {
|
|
||||||
let fullname = self.get_fullname(content_category, subscriber_id, bucket, filename);
|
|
||||||
|
|
||||||
let operator = self.get_operator()?;
|
|
||||||
|
|
||||||
if operator.exists(fullname.as_str()).await? {
|
|
||||||
Ok(Some(StorageStoredUrl::RelativePath {
|
|
||||||
path: fullname.to_string(),
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_object(
|
|
||||||
&self,
|
|
||||||
content_category: StorageContentCategory,
|
|
||||||
subscriber_id: i32,
|
|
||||||
bucket: Option<&str>,
|
|
||||||
filename: &str,
|
|
||||||
) -> RecorderResult<Buffer> {
|
|
||||||
let fullname = self.get_fullname(content_category, subscriber_id, bucket, filename);
|
|
||||||
|
|
||||||
let operator = self.get_operator()?;
|
|
||||||
|
|
||||||
let data = operator.read(fullname.as_str()).await?;
|
|
||||||
|
|
||||||
Ok(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct StorageService {
|
pub struct StorageService {
|
||||||
pub data_dir: String,
|
pub data_dir: String,
|
||||||
@ -136,15 +64,224 @@ impl StorageService {
|
|||||||
data_dir: config.data_dir.to_string(),
|
data_dir: config.data_dir.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
pub fn get_operator(&self) -> Result<Operator, opendal::Error> {
|
||||||
impl StorageServiceTrait for StorageService {
|
let op = if cfg!(test) {
|
||||||
fn get_operator(&self) -> RecorderResult<Operator> {
|
Operator::new(opendal::services::Memory::default())?
|
||||||
let fs_op = Operator::new(opendal::services::Fs::default().root(&self.data_dir))?
|
|
||||||
.layer(LoggingLayer::default())
|
.layer(LoggingLayer::default())
|
||||||
.finish();
|
.finish()
|
||||||
|
} else {
|
||||||
|
Operator::new(opendal::services::Fs::default().root(&self.data_dir))?
|
||||||
|
.layer(LoggingLayer::default())
|
||||||
|
.finish()
|
||||||
|
};
|
||||||
|
|
||||||
Ok(fs_op)
|
Ok(op)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_subscriber_path(&self, subscriber_id: i32, path: impl AsRef<Path>) -> PathBuf {
|
||||||
|
let mut p = PathBuf::from("/subscribers");
|
||||||
|
p.push(subscriber_id.to_string());
|
||||||
|
p.push(path);
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_public_path(&self, path: impl AsRef<Path>) -> PathBuf {
|
||||||
|
let mut p = PathBuf::from("/public");
|
||||||
|
p.push(path);
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_subscriber_object_path(
|
||||||
|
&self,
|
||||||
|
subscriber_id: i32,
|
||||||
|
content_category: StorageContentCategory,
|
||||||
|
bucket: &str,
|
||||||
|
object_name: &str,
|
||||||
|
) -> PathBuf {
|
||||||
|
self.build_subscriber_path(
|
||||||
|
subscriber_id,
|
||||||
|
[content_category.as_ref(), bucket, object_name]
|
||||||
|
.iter()
|
||||||
|
.collect::<PathBuf>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_public_object_path(
|
||||||
|
&self,
|
||||||
|
content_category: StorageContentCategory,
|
||||||
|
bucket: &str,
|
||||||
|
object_name: &str,
|
||||||
|
) -> PathBuf {
|
||||||
|
self.build_public_path(
|
||||||
|
[content_category.as_ref(), bucket, object_name]
|
||||||
|
.iter()
|
||||||
|
.collect::<PathBuf>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write<P: Into<PathBuf> + Send>(
|
||||||
|
&self,
|
||||||
|
path: P,
|
||||||
|
data: Bytes,
|
||||||
|
) -> Result<StorageStoredUrl, opendal::Error> {
|
||||||
|
let operator = self.get_operator()?;
|
||||||
|
|
||||||
|
let path = path.into();
|
||||||
|
|
||||||
|
if let Some(dirname) = path.parent() {
|
||||||
|
let dirname = dirname.join("/");
|
||||||
|
operator.create_dir(dirname.as_str()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
operator.write(path.as_str(), data).await?;
|
||||||
|
|
||||||
|
Ok(StorageStoredUrl::RelativePath {
|
||||||
|
path: path.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn exists<P: ToString + Send>(
|
||||||
|
&self,
|
||||||
|
path: P,
|
||||||
|
) -> Result<Option<StorageStoredUrl>, opendal::Error> {
|
||||||
|
let operator = self.get_operator()?;
|
||||||
|
|
||||||
|
let path = path.to_string();
|
||||||
|
|
||||||
|
if operator.exists(&path).await? {
|
||||||
|
Ok(Some(StorageStoredUrl::RelativePath { path }))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read(&self, path: impl AsRef<str>) -> Result<Buffer, opendal::Error> {
|
||||||
|
let operator = self.get_operator()?;
|
||||||
|
|
||||||
|
let data = operator.read(path.as_ref()).await?;
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reader(&self, path: impl AsRef<str>) -> Result<Reader, opendal::Error> {
|
||||||
|
let operator = self.get_operator()?;
|
||||||
|
|
||||||
|
let reader = operator.reader(path.as_ref()).await?;
|
||||||
|
|
||||||
|
Ok(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn writer(&self, path: impl AsRef<str>) -> Result<Writer, opendal::Error> {
|
||||||
|
let operator = self.get_operator()?;
|
||||||
|
|
||||||
|
let writer = operator.writer(path.as_ref()).await?;
|
||||||
|
|
||||||
|
Ok(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stat(&self, path: impl AsRef<str>) -> Result<Metadata, opendal::Error> {
|
||||||
|
let operator = self.get_operator()?;
|
||||||
|
|
||||||
|
let metadata = operator.stat(path.as_ref()).await?;
|
||||||
|
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, err, fields(storage_path = %storage_path.as_ref(), range = ?range))]
|
||||||
|
pub async fn serve_file(
|
||||||
|
&self,
|
||||||
|
storage_path: impl AsRef<str>,
|
||||||
|
range: Option<TypedHeader<Range>>,
|
||||||
|
) -> RecorderResult<Response> {
|
||||||
|
let metadata = self
|
||||||
|
.stat(&storage_path)
|
||||||
|
.await
|
||||||
|
.map_err(|_| RecorderError::from_status(StatusCode::NOT_FOUND))?;
|
||||||
|
|
||||||
|
if !metadata.is_file() {
|
||||||
|
return Err(RecorderError::from_status(StatusCode::NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mime_type = mime_guess::from_path(storage_path.as_ref()).first_or_octet_stream();
|
||||||
|
|
||||||
|
let content_type = HeaderValue::from_str(mime_type.as_ref())?;
|
||||||
|
|
||||||
|
let response = if let Some(TypedHeader(range)) = range {
|
||||||
|
let ranges = range
|
||||||
|
.satisfiable_ranges(metadata.content_length())
|
||||||
|
.map(|r| -> Option<(_, _)> {
|
||||||
|
let a = bound_range_to_content_range(&r, metadata.content_length())?;
|
||||||
|
Some((r, a))
|
||||||
|
})
|
||||||
|
.collect::<Option<Vec<_>>>();
|
||||||
|
|
||||||
|
if let Some(mut ranges) = ranges {
|
||||||
|
if ranges.len() > 1 {
|
||||||
|
let boundary = Uuid::new_v4().to_string();
|
||||||
|
let reader = self.reader(storage_path.as_ref()).await?;
|
||||||
|
let stream: impl Stream<Item = Result<Bytes, RecorderError>> = {
|
||||||
|
let boundary = boundary.clone();
|
||||||
|
try_stream! {
|
||||||
|
for (r, content_range) in ranges {
|
||||||
|
let part_header = format!("--{boundary}\r\nContent-Type: {}\r\nContent-Range: {}\r\n\r\n",
|
||||||
|
mime_type.as_ref(),
|
||||||
|
content_range.clone().to_str().unwrap(),
|
||||||
|
);
|
||||||
|
yield part_header.into();
|
||||||
|
let mut part_stream = reader.clone().into_bytes_stream(r).await?;
|
||||||
|
while let Some(chunk) = part_stream.next().await {
|
||||||
|
yield chunk?;
|
||||||
|
}
|
||||||
|
yield "\r\n".into();
|
||||||
|
}
|
||||||
|
yield format!("--{boundary}--").into();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let body = Body::from_stream(stream);
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::PARTIAL_CONTENT)
|
||||||
|
.header(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_str(
|
||||||
|
format!("multipart/byteranges; boundary={boundary}").as_str(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.body(body)?
|
||||||
|
} else if let Some((r, content_range)) = ranges.pop() {
|
||||||
|
let reader = self.reader(storage_path.as_ref()).await?;
|
||||||
|
let stream = reader.into_bytes_stream(r).await?;
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::PARTIAL_CONTENT)
|
||||||
|
.header(header::CONTENT_TYPE, content_type.clone())
|
||||||
|
.header(header::CONTENT_RANGE, content_range)
|
||||||
|
.body(Body::from_stream(stream))?
|
||||||
|
} else {
|
||||||
|
unreachable!("ranges length should be greater than 0")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::RANGE_NOT_SATISFIABLE)
|
||||||
|
.header(header::CONTENT_TYPE, content_type)
|
||||||
|
.header(
|
||||||
|
header::CONTENT_RANGE,
|
||||||
|
build_no_satisfiable_content_range(metadata.content_length()),
|
||||||
|
)
|
||||||
|
.body(Body::empty())?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let reader = self.reader(storage_path.as_ref()).await?;
|
||||||
|
let stream = reader.into_bytes_stream(..).await?;
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(header::CONTENT_TYPE, content_type)
|
||||||
|
.body(Body::from_stream(stream))?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mod client;
|
mod client;
|
||||||
mod config;
|
mod config;
|
||||||
pub use client::{StorageContentCategory, StorageService, StorageServiceTrait, StorageStoredUrl};
|
pub use client::{StorageContentCategory, StorageService, StorageStoredUrl};
|
||||||
pub use config::StorageConfig;
|
pub use config::StorageConfig;
|
||||||
|
@ -3,7 +3,7 @@ use std::{fmt::Debug, sync::Arc};
|
|||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use typed_builder::TypedBuilder;
|
use typed_builder::TypedBuilder;
|
||||||
|
|
||||||
use crate::{app::AppContextTrait, test_utils::storage::TestingStorageService};
|
use crate::app::AppContextTrait;
|
||||||
|
|
||||||
#[derive(TypedBuilder)]
|
#[derive(TypedBuilder)]
|
||||||
#[builder(field_defaults(default, setter(strip_option)))]
|
#[builder(field_defaults(default, setter(strip_option)))]
|
||||||
@ -15,7 +15,7 @@ pub struct TestingAppContext {
|
|||||||
mikan: Option<crate::extract::mikan::MikanClient>,
|
mikan: Option<crate::extract::mikan::MikanClient>,
|
||||||
auth: Option<crate::auth::AuthService>,
|
auth: Option<crate::auth::AuthService>,
|
||||||
graphql: Option<crate::graphql::GraphQLService>,
|
graphql: Option<crate::graphql::GraphQLService>,
|
||||||
storage: Option<TestingStorageService>,
|
storage: Option<crate::storage::StorageService>,
|
||||||
crypto: Option<crate::crypto::CryptoService>,
|
crypto: Option<crate::crypto::CryptoService>,
|
||||||
#[builder(default = Arc::new(OnceCell::new()), setter(!strip_option))]
|
#[builder(default = Arc::new(OnceCell::new()), setter(!strip_option))]
|
||||||
task: Arc<OnceCell<crate::task::TaskService>>,
|
task: Arc<OnceCell<crate::task::TaskService>>,
|
||||||
@ -67,7 +67,7 @@ impl AppContextTrait for TestingAppContext {
|
|||||||
self.graphql.as_ref().expect("should set graphql")
|
self.graphql.as_ref().expect("should set graphql")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn storage(&self) -> &dyn crate::storage::StorageServiceTrait {
|
fn storage(&self) -> &crate::storage::StorageService {
|
||||||
self.storage.as_ref().expect("should set storage")
|
self.storage.as_ref().expect("should set storage")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,28 +1,13 @@
|
|||||||
use opendal::{Operator, layers::LoggingLayer};
|
use crate::{
|
||||||
|
errors::RecorderResult,
|
||||||
|
storage::{StorageConfig, StorageService},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{errors::RecorderResult, storage::StorageServiceTrait};
|
pub async fn build_testing_storage_service() -> RecorderResult<StorageService> {
|
||||||
|
let service = StorageService::from_config(StorageConfig {
|
||||||
|
data_dir: "tests/data".to_string(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
pub struct TestingStorageService {
|
Ok(service)
|
||||||
operator: Operator,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestingStorageService {
|
|
||||||
pub fn new() -> RecorderResult<Self> {
|
|
||||||
let op = Operator::new(opendal::services::Memory::default())?
|
|
||||||
.layer(LoggingLayer::default())
|
|
||||||
.finish();
|
|
||||||
|
|
||||||
Ok(Self { operator: op })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl StorageServiceTrait for TestingStorageService {
|
|
||||||
fn get_operator(&self) -> RecorderResult<Operator> {
|
|
||||||
Ok(self.operator.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn build_testing_storage_service() -> RecorderResult<TestingStorageService> {
|
|
||||||
TestingStorageService::new()
|
|
||||||
}
|
}
|
||||||
|
23
apps/recorder/src/utils/http.rs
Normal file
23
apps/recorder/src/utils/http.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use std::ops::Bound;
|
||||||
|
|
||||||
|
use http::HeaderValue;
|
||||||
|
|
||||||
|
pub fn build_no_satisfiable_content_range(len: u64) -> HeaderValue {
|
||||||
|
HeaderValue::from_str(&format!("bytes */{len}"))
|
||||||
|
.unwrap_or_else(|e| unreachable!("Invalid content range: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bound_range_to_content_range(r: &(Bound<u64>, Bound<u64>), l: u64) -> Option<HeaderValue> {
|
||||||
|
match r {
|
||||||
|
(Bound::Included(start), Bound::Included(end)) => Some(format!("bytes {start}-{end}/{l}")),
|
||||||
|
(Bound::Included(start), Bound::Excluded(end)) => {
|
||||||
|
Some(format!("bytes {start}-{}/{l}", end - 1))
|
||||||
|
}
|
||||||
|
(Bound::Included(start), Bound::Unbounded) => Some(format!(
|
||||||
|
"bytes {start}-{}/{l}",
|
||||||
|
if l > 0 { l - 1 } else { 0 }
|
||||||
|
)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
.and_then(|s| HeaderValue::from_str(&s).ok())
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
|
pub mod http;
|
||||||
pub mod json;
|
pub mod json;
|
||||||
|
@ -2,5 +2,6 @@ pub mod core;
|
|||||||
pub mod graphql;
|
pub mod graphql;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
pub mod oidc;
|
pub mod oidc;
|
||||||
|
pub mod r#static;
|
||||||
|
|
||||||
pub use core::{Controller, ControllerTrait, PrefixController};
|
pub use core::{Controller, ControllerTrait, PrefixController};
|
||||||
|
59
apps/recorder/src/web/controller/static/mod.rs
Normal file
59
apps/recorder/src/web/controller/static/mod.rs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Extension, Router,
|
||||||
|
extract::{Path, State},
|
||||||
|
middleware::from_fn_with_state,
|
||||||
|
response::Response,
|
||||||
|
routing::get,
|
||||||
|
};
|
||||||
|
use axum_extra::{TypedHeader, headers::Range};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::AppContextTrait,
|
||||||
|
auth::{AuthError, AuthUserInfo, auth_middleware},
|
||||||
|
errors::RecorderResult,
|
||||||
|
web::controller::Controller,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CONTROLLER_PREFIX: &str = "/api/static";
|
||||||
|
|
||||||
|
async fn serve_subscriber_static(
|
||||||
|
State(ctx): State<Arc<dyn AppContextTrait>>,
|
||||||
|
Path((subscriber_id, path)): Path<(i32, String)>,
|
||||||
|
Extension(auth_user_info): Extension<AuthUserInfo>,
|
||||||
|
range: Option<TypedHeader<Range>>,
|
||||||
|
) -> RecorderResult<Response> {
|
||||||
|
if subscriber_id != auth_user_info.subscriber_auth.id {
|
||||||
|
Err(AuthError::PermissionError)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let storage = ctx.storage();
|
||||||
|
|
||||||
|
let storage_path = storage.build_subscriber_path(subscriber_id, &path);
|
||||||
|
|
||||||
|
storage.serve_file(storage_path, range).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve_public_static(
|
||||||
|
State(ctx): State<Arc<dyn AppContextTrait>>,
|
||||||
|
Path(path): Path<String>,
|
||||||
|
range: Option<TypedHeader<Range>>,
|
||||||
|
) -> RecorderResult<Response> {
|
||||||
|
let storage = ctx.storage();
|
||||||
|
|
||||||
|
let storage_path = storage.build_public_path(&path);
|
||||||
|
|
||||||
|
storage.serve_file(storage_path, range).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create(ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller> {
|
||||||
|
let router = Router::<Arc<dyn AppContextTrait>>::new()
|
||||||
|
.route(
|
||||||
|
"/subscribers/{subscriber_id}/{*path}",
|
||||||
|
get(serve_subscriber_static).layer(from_fn_with_state(ctx, auth_middleware)),
|
||||||
|
)
|
||||||
|
.route("/public/{*path}", get(serve_public_static));
|
||||||
|
|
||||||
|
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router))
|
||||||
|
}
|
@ -4,7 +4,7 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
|
|
||||||
import { Image } from '@/components/ui/image';
|
import { Img } from '@/components/ui/img';
|
||||||
|
|
||||||
export function AppIcon() {
|
export function AppIcon() {
|
||||||
return (
|
return (
|
||||||
@ -16,7 +16,7 @@ export function AppIcon() {
|
|||||||
>
|
>
|
||||||
<div className="flex size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
<div className="flex size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||||
<div className="relative size-8">
|
<div className="relative size-8">
|
||||||
<Image
|
<Img
|
||||||
src="/assets/favicon.png"
|
src="/assets/favicon.png"
|
||||||
alt="App Logo"
|
alt="App Logo"
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import type { ComponentProps } from 'react';
|
|
||||||
|
|
||||||
export type ImageProps = Omit<ComponentProps<'img'>, 'alt'> &
|
|
||||||
Required<Pick<ComponentProps<'img'>, 'alt'>>;
|
|
||||||
|
|
||||||
export const Image = (props: ImageProps) => {
|
|
||||||
// biome-ignore lint/nursery/noImgElement: <explanation>
|
|
||||||
return <img {...props} alt={props.alt} />;
|
|
||||||
};
|
|
9
apps/webui/src/components/ui/img.tsx
Normal file
9
apps/webui/src/components/ui/img.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
|
export type ImgProps = Omit<ComponentProps<"img">, "alt"> &
|
||||||
|
Required<Pick<ComponentProps<"img">, "alt">>;
|
||||||
|
|
||||||
|
export const Img = (props: ImgProps) => {
|
||||||
|
// biome-ignore lint/nursery/noImgElement: <explanation>
|
||||||
|
return <img {...props} alt={props.alt} />;
|
||||||
|
};
|
@ -106,7 +106,6 @@ query GetSubscriptionDetail ($id: Int!) {
|
|||||||
id
|
id
|
||||||
mikanBangumiId
|
mikanBangumiId
|
||||||
displayName
|
displayName
|
||||||
rawName
|
|
||||||
season
|
season
|
||||||
seasonRaw
|
seasonRaw
|
||||||
fansub
|
fansub
|
||||||
|
@ -24,7 +24,7 @@ type Documents = {
|
|||||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument,
|
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument,
|
||||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
|
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
|
||||||
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": typeof types.DeleteSubscriptionsDocument,
|
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": typeof types.DeleteSubscriptionsDocument,
|
||||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
||||||
"\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsIncrementalDocument,
|
"\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsIncrementalDocument,
|
||||||
"\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsFullDocument,
|
"\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsFullDocument,
|
||||||
"\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionSourcesDocument,
|
"\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionSourcesDocument,
|
||||||
@ -43,7 +43,7 @@ const documents: Documents = {
|
|||||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument,
|
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument,
|
||||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
|
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
|
||||||
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": types.DeleteSubscriptionsDocument,
|
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": types.DeleteSubscriptionsDocument,
|
||||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
||||||
"\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsIncrementalDocument,
|
"\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsIncrementalDocument,
|
||||||
"\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsFullDocument,
|
"\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsFullDocument,
|
||||||
"\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionSourcesDocument,
|
"\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionSourcesDocument,
|
||||||
@ -109,7 +109,7 @@ export function gql(source: "\n mutation DeleteSubscriptions($filters: Subscr
|
|||||||
/**
|
/**
|
||||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"];
|
export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"];
|
||||||
/**
|
/**
|
||||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
@ -30,8 +30,9 @@ export type Bangumi = {
|
|||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
||||||
mikanFansubId?: Maybe<Scalars['String']['output']>;
|
mikanFansubId?: Maybe<Scalars['String']['output']>;
|
||||||
|
originName: Scalars['String']['output'];
|
||||||
|
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||||
posterLink?: Maybe<Scalars['String']['output']>;
|
posterLink?: Maybe<Scalars['String']['output']>;
|
||||||
rawName: Scalars['String']['output'];
|
|
||||||
rssLink?: Maybe<Scalars['String']['output']>;
|
rssLink?: Maybe<Scalars['String']['output']>;
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
season: Scalars['Int']['output'];
|
season: Scalars['Int']['output'];
|
||||||
@ -74,8 +75,9 @@ export type BangumiBasic = {
|
|||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
||||||
mikanFansubId?: Maybe<Scalars['String']['output']>;
|
mikanFansubId?: Maybe<Scalars['String']['output']>;
|
||||||
|
originName: Scalars['String']['output'];
|
||||||
|
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||||
posterLink?: Maybe<Scalars['String']['output']>;
|
posterLink?: Maybe<Scalars['String']['output']>;
|
||||||
rawName: Scalars['String']['output'];
|
|
||||||
rssLink?: Maybe<Scalars['String']['output']>;
|
rssLink?: Maybe<Scalars['String']['output']>;
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
season: Scalars['Int']['output'];
|
season: Scalars['Int']['output'];
|
||||||
@ -108,8 +110,9 @@ export type BangumiFilterInput = {
|
|||||||
mikanBangumiId?: InputMaybe<StringFilterInput>;
|
mikanBangumiId?: InputMaybe<StringFilterInput>;
|
||||||
mikanFansubId?: InputMaybe<StringFilterInput>;
|
mikanFansubId?: InputMaybe<StringFilterInput>;
|
||||||
or?: InputMaybe<Array<BangumiFilterInput>>;
|
or?: InputMaybe<Array<BangumiFilterInput>>;
|
||||||
|
originName?: InputMaybe<StringFilterInput>;
|
||||||
|
originPosterLink?: InputMaybe<StringFilterInput>;
|
||||||
posterLink?: InputMaybe<StringFilterInput>;
|
posterLink?: InputMaybe<StringFilterInput>;
|
||||||
rawName?: InputMaybe<StringFilterInput>;
|
|
||||||
rssLink?: InputMaybe<StringFilterInput>;
|
rssLink?: InputMaybe<StringFilterInput>;
|
||||||
savePath?: InputMaybe<StringFilterInput>;
|
savePath?: InputMaybe<StringFilterInput>;
|
||||||
season?: InputMaybe<IntegerFilterInput>;
|
season?: InputMaybe<IntegerFilterInput>;
|
||||||
@ -127,8 +130,9 @@ export type BangumiInsertInput = {
|
|||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
||||||
mikanFansubId?: InputMaybe<Scalars['String']['input']>;
|
mikanFansubId?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originName: Scalars['String']['input'];
|
||||||
|
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
rawName: Scalars['String']['input'];
|
|
||||||
rssLink?: InputMaybe<Scalars['String']['input']>;
|
rssLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
season: Scalars['Int']['input'];
|
season: Scalars['Int']['input'];
|
||||||
@ -146,8 +150,9 @@ export type BangumiOrderInput = {
|
|||||||
id?: InputMaybe<OrderByEnum>;
|
id?: InputMaybe<OrderByEnum>;
|
||||||
mikanBangumiId?: InputMaybe<OrderByEnum>;
|
mikanBangumiId?: InputMaybe<OrderByEnum>;
|
||||||
mikanFansubId?: InputMaybe<OrderByEnum>;
|
mikanFansubId?: InputMaybe<OrderByEnum>;
|
||||||
|
originName?: InputMaybe<OrderByEnum>;
|
||||||
|
originPosterLink?: InputMaybe<OrderByEnum>;
|
||||||
posterLink?: InputMaybe<OrderByEnum>;
|
posterLink?: InputMaybe<OrderByEnum>;
|
||||||
rawName?: InputMaybe<OrderByEnum>;
|
|
||||||
rssLink?: InputMaybe<OrderByEnum>;
|
rssLink?: InputMaybe<OrderByEnum>;
|
||||||
savePath?: InputMaybe<OrderByEnum>;
|
savePath?: InputMaybe<OrderByEnum>;
|
||||||
season?: InputMaybe<OrderByEnum>;
|
season?: InputMaybe<OrderByEnum>;
|
||||||
@ -165,8 +170,9 @@ export type BangumiUpdateInput = {
|
|||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
||||||
mikanFansubId?: InputMaybe<Scalars['String']['input']>;
|
mikanFansubId?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originName?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
rawName?: InputMaybe<Scalars['String']['input']>;
|
|
||||||
rssLink?: InputMaybe<Scalars['String']['input']>;
|
rssLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
season?: InputMaybe<Scalars['Int']['input']>;
|
season?: InputMaybe<Scalars['Int']['input']>;
|
||||||
@ -492,7 +498,7 @@ export type Downloads = {
|
|||||||
homepage?: Maybe<Scalars['String']['output']>;
|
homepage?: Maybe<Scalars['String']['output']>;
|
||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mime: DownloadMimeEnum;
|
mime: DownloadMimeEnum;
|
||||||
rawName: Scalars['String']['output'];
|
originName: Scalars['String']['output'];
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
status: DownloadStatusEnum;
|
status: DownloadStatusEnum;
|
||||||
subscriber?: Maybe<Subscribers>;
|
subscriber?: Maybe<Subscribers>;
|
||||||
@ -512,7 +518,7 @@ export type DownloadsBasic = {
|
|||||||
homepage?: Maybe<Scalars['String']['output']>;
|
homepage?: Maybe<Scalars['String']['output']>;
|
||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mime: DownloadMimeEnum;
|
mime: DownloadMimeEnum;
|
||||||
rawName: Scalars['String']['output'];
|
originName: Scalars['String']['output'];
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
status: DownloadStatusEnum;
|
status: DownloadStatusEnum;
|
||||||
subscriberId: Scalars['Int']['output'];
|
subscriberId: Scalars['Int']['output'];
|
||||||
@ -546,7 +552,7 @@ export type DownloadsFilterInput = {
|
|||||||
id?: InputMaybe<IntegerFilterInput>;
|
id?: InputMaybe<IntegerFilterInput>;
|
||||||
mime?: InputMaybe<DownloadMimeEnumFilterInput>;
|
mime?: InputMaybe<DownloadMimeEnumFilterInput>;
|
||||||
or?: InputMaybe<Array<DownloadsFilterInput>>;
|
or?: InputMaybe<Array<DownloadsFilterInput>>;
|
||||||
rawName?: InputMaybe<StringFilterInput>;
|
originName?: InputMaybe<StringFilterInput>;
|
||||||
savePath?: InputMaybe<StringFilterInput>;
|
savePath?: InputMaybe<StringFilterInput>;
|
||||||
status?: InputMaybe<DownloadStatusEnumFilterInput>;
|
status?: InputMaybe<DownloadStatusEnumFilterInput>;
|
||||||
subscriberId?: InputMaybe<SubscriberIdFilterInput>;
|
subscriberId?: InputMaybe<SubscriberIdFilterInput>;
|
||||||
@ -564,7 +570,7 @@ export type DownloadsInsertInput = {
|
|||||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mime: DownloadMimeEnum;
|
mime: DownloadMimeEnum;
|
||||||
rawName: Scalars['String']['input'];
|
originName: Scalars['String']['input'];
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
status: DownloadStatusEnum;
|
status: DownloadStatusEnum;
|
||||||
subscriberId?: InputMaybe<Scalars['Int']['input']>;
|
subscriberId?: InputMaybe<Scalars['Int']['input']>;
|
||||||
@ -582,7 +588,7 @@ export type DownloadsOrderInput = {
|
|||||||
homepage?: InputMaybe<OrderByEnum>;
|
homepage?: InputMaybe<OrderByEnum>;
|
||||||
id?: InputMaybe<OrderByEnum>;
|
id?: InputMaybe<OrderByEnum>;
|
||||||
mime?: InputMaybe<OrderByEnum>;
|
mime?: InputMaybe<OrderByEnum>;
|
||||||
rawName?: InputMaybe<OrderByEnum>;
|
originName?: InputMaybe<OrderByEnum>;
|
||||||
savePath?: InputMaybe<OrderByEnum>;
|
savePath?: InputMaybe<OrderByEnum>;
|
||||||
status?: InputMaybe<OrderByEnum>;
|
status?: InputMaybe<OrderByEnum>;
|
||||||
subscriberId?: InputMaybe<OrderByEnum>;
|
subscriberId?: InputMaybe<OrderByEnum>;
|
||||||
@ -600,7 +606,7 @@ export type DownloadsUpdateInput = {
|
|||||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mime?: InputMaybe<DownloadMimeEnum>;
|
mime?: InputMaybe<DownloadMimeEnum>;
|
||||||
rawName?: InputMaybe<Scalars['String']['input']>;
|
originName?: InputMaybe<Scalars['String']['input']>;
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
status?: InputMaybe<DownloadStatusEnum>;
|
status?: InputMaybe<DownloadStatusEnum>;
|
||||||
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
||||||
@ -619,8 +625,9 @@ export type Episodes = {
|
|||||||
homepage?: Maybe<Scalars['String']['output']>;
|
homepage?: Maybe<Scalars['String']['output']>;
|
||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mikanEpisodeId?: Maybe<Scalars['String']['output']>;
|
mikanEpisodeId?: Maybe<Scalars['String']['output']>;
|
||||||
|
originName: Scalars['String']['output'];
|
||||||
|
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||||
posterLink?: Maybe<Scalars['String']['output']>;
|
posterLink?: Maybe<Scalars['String']['output']>;
|
||||||
rawName: Scalars['String']['output'];
|
|
||||||
resolution?: Maybe<Scalars['String']['output']>;
|
resolution?: Maybe<Scalars['String']['output']>;
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
season: Scalars['Int']['output'];
|
season: Scalars['Int']['output'];
|
||||||
@ -665,8 +672,9 @@ export type EpisodesBasic = {
|
|||||||
homepage?: Maybe<Scalars['String']['output']>;
|
homepage?: Maybe<Scalars['String']['output']>;
|
||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mikanEpisodeId?: Maybe<Scalars['String']['output']>;
|
mikanEpisodeId?: Maybe<Scalars['String']['output']>;
|
||||||
|
originName: Scalars['String']['output'];
|
||||||
|
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||||
posterLink?: Maybe<Scalars['String']['output']>;
|
posterLink?: Maybe<Scalars['String']['output']>;
|
||||||
rawName: Scalars['String']['output'];
|
|
||||||
resolution?: Maybe<Scalars['String']['output']>;
|
resolution?: Maybe<Scalars['String']['output']>;
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
season: Scalars['Int']['output'];
|
season: Scalars['Int']['output'];
|
||||||
@ -702,8 +710,9 @@ export type EpisodesFilterInput = {
|
|||||||
id?: InputMaybe<IntegerFilterInput>;
|
id?: InputMaybe<IntegerFilterInput>;
|
||||||
mikanEpisodeId?: InputMaybe<StringFilterInput>;
|
mikanEpisodeId?: InputMaybe<StringFilterInput>;
|
||||||
or?: InputMaybe<Array<EpisodesFilterInput>>;
|
or?: InputMaybe<Array<EpisodesFilterInput>>;
|
||||||
|
originName?: InputMaybe<StringFilterInput>;
|
||||||
|
originPosterLink?: InputMaybe<StringFilterInput>;
|
||||||
posterLink?: InputMaybe<StringFilterInput>;
|
posterLink?: InputMaybe<StringFilterInput>;
|
||||||
rawName?: InputMaybe<StringFilterInput>;
|
|
||||||
resolution?: InputMaybe<StringFilterInput>;
|
resolution?: InputMaybe<StringFilterInput>;
|
||||||
savePath?: InputMaybe<StringFilterInput>;
|
savePath?: InputMaybe<StringFilterInput>;
|
||||||
season?: InputMaybe<IntegerFilterInput>;
|
season?: InputMaybe<IntegerFilterInput>;
|
||||||
@ -723,8 +732,9 @@ export type EpisodesInsertInput = {
|
|||||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mikanEpisodeId?: InputMaybe<Scalars['String']['input']>;
|
mikanEpisodeId?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originName: Scalars['String']['input'];
|
||||||
|
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
rawName: Scalars['String']['input'];
|
|
||||||
resolution?: InputMaybe<Scalars['String']['input']>;
|
resolution?: InputMaybe<Scalars['String']['input']>;
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
season: Scalars['Int']['input'];
|
season: Scalars['Int']['input'];
|
||||||
@ -744,8 +754,9 @@ export type EpisodesOrderInput = {
|
|||||||
homepage?: InputMaybe<OrderByEnum>;
|
homepage?: InputMaybe<OrderByEnum>;
|
||||||
id?: InputMaybe<OrderByEnum>;
|
id?: InputMaybe<OrderByEnum>;
|
||||||
mikanEpisodeId?: InputMaybe<OrderByEnum>;
|
mikanEpisodeId?: InputMaybe<OrderByEnum>;
|
||||||
|
originName?: InputMaybe<OrderByEnum>;
|
||||||
|
originPosterLink?: InputMaybe<OrderByEnum>;
|
||||||
posterLink?: InputMaybe<OrderByEnum>;
|
posterLink?: InputMaybe<OrderByEnum>;
|
||||||
rawName?: InputMaybe<OrderByEnum>;
|
|
||||||
resolution?: InputMaybe<OrderByEnum>;
|
resolution?: InputMaybe<OrderByEnum>;
|
||||||
savePath?: InputMaybe<OrderByEnum>;
|
savePath?: InputMaybe<OrderByEnum>;
|
||||||
season?: InputMaybe<OrderByEnum>;
|
season?: InputMaybe<OrderByEnum>;
|
||||||
@ -765,8 +776,9 @@ export type EpisodesUpdateInput = {
|
|||||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mikanEpisodeId?: InputMaybe<Scalars['String']['input']>;
|
mikanEpisodeId?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originName?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
rawName?: InputMaybe<Scalars['String']['input']>;
|
|
||||||
resolution?: InputMaybe<Scalars['String']['input']>;
|
resolution?: InputMaybe<Scalars['String']['input']>;
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
season?: InputMaybe<Scalars['Int']['input']>;
|
season?: InputMaybe<Scalars['Int']['input']>;
|
||||||
@ -1708,7 +1720,7 @@ export type GetSubscriptionDetailQueryVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, rawName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, savePath?: string | null, homepage?: string | null }> } }> } };
|
export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, savePath?: string | null, homepage?: string | null }> } }> } };
|
||||||
|
|
||||||
export type SyncSubscriptionFeedsIncrementalMutationVariables = Exact<{
|
export type SyncSubscriptionFeedsIncrementalMutationVariables = Exact<{
|
||||||
filter: SubscriptionsFilterInput;
|
filter: SubscriptionsFilterInput;
|
||||||
@ -1765,7 +1777,7 @@ export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind
|
|||||||
export const InsertSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credentialId"}}]}}]}}]} as unknown as DocumentNode<InsertSubscriptionMutation, InsertSubscriptionMutationVariables>;
|
export const InsertSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credentialId"}}]}}]}}]} as unknown as DocumentNode<InsertSubscriptionMutation, InsertSubscriptionMutationVariables>;
|
||||||
export const UpdateSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}}]}}]} as unknown as DocumentNode<UpdateSubscriptionsMutation, UpdateSubscriptionsMutationVariables>;
|
export const UpdateSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}}]}}]} as unknown as DocumentNode<UpdateSubscriptionsMutation, UpdateSubscriptionsMutationVariables>;
|
||||||
export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode<DeleteSubscriptionsMutation, DeleteSubscriptionsMutationVariables>;
|
export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode<DeleteSubscriptionsMutation, DeleteSubscriptionsMutationVariables>;
|
||||||
export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"rawName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"savePath"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetSubscriptionDetailQuery, GetSubscriptionDetailQueryVariables>;
|
export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"savePath"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetSubscriptionDetailQuery, GetSubscriptionDetailQueryVariables>;
|
||||||
export const SyncSubscriptionFeedsIncrementalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsIncremental"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsIncremental"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionFeedsIncrementalMutation, SyncSubscriptionFeedsIncrementalMutationVariables>;
|
export const SyncSubscriptionFeedsIncrementalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsIncremental"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsIncremental"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionFeedsIncrementalMutation, SyncSubscriptionFeedsIncrementalMutationVariables>;
|
||||||
export const SyncSubscriptionFeedsFullDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsFull"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsFull"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionFeedsFullMutation, SyncSubscriptionFeedsFullMutationVariables>;
|
export const SyncSubscriptionFeedsFullDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsFull"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsFull"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionFeedsFullMutation, SyncSubscriptionFeedsFullMutationVariables>;
|
||||||
export const SyncSubscriptionSourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionSources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneSources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionSourcesMutation, SyncSubscriptionSourcesMutationVariables>;
|
export const SyncSubscriptionSourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionSources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneSources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionSourcesMutation, SyncSubscriptionSourcesMutationVariables>;
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import { Img } from '@/components/ui/img';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
@ -324,7 +325,18 @@ function SubscriptionDetailRouteComponent() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{subscription.bangumi.nodes.map((bangumi) => (
|
{subscription.bangumi.nodes.map((bangumi) => (
|
||||||
<Card key={bangumi.id} className="p-4">
|
<Card key={bangumi.id} className="p-4">
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
|
||||||
|
<div className="col-span-1 row-span-2 space-y-2">
|
||||||
|
<div className="flex h-full items-center justify-center overflow-hidden rounded-md bg-muted">
|
||||||
|
{bangumi.posterLink && (
|
||||||
|
<Img
|
||||||
|
src={`/api/static${bangumi.posterLink}`}
|
||||||
|
alt="Poster"
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium text-muted-foreground text-xs">
|
<Label className="font-medium text-muted-foreground text-xs">
|
||||||
Display Name
|
Display Name
|
||||||
@ -333,14 +345,6 @@ function SubscriptionDetailRouteComponent() {
|
|||||||
{bangumi.displayName}
|
{bangumi.displayName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="font-medium text-muted-foreground text-xs">
|
|
||||||
Season
|
|
||||||
</Label>
|
|
||||||
<div className="text-sm">
|
|
||||||
{bangumi.season || '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium text-muted-foreground text-xs">
|
<Label className="font-medium text-muted-foreground text-xs">
|
||||||
Fansub
|
Fansub
|
||||||
@ -351,10 +355,21 @@ function SubscriptionDetailRouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium text-muted-foreground text-xs">
|
<Label className="font-medium text-muted-foreground text-xs">
|
||||||
Save Path
|
Season
|
||||||
|
</Label>
|
||||||
|
<div className="text-sm">
|
||||||
|
{bangumi.season || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-muted-foreground text-xs">
|
||||||
|
Updated At
|
||||||
</Label>
|
</Label>
|
||||||
<div className="font-mono text-sm">
|
<div className="font-mono text-sm">
|
||||||
{bangumi.savePath || '-'}
|
{format(
|
||||||
|
new Date(bangumi.updatedAt),
|
||||||
|
'yyyy-MM-dd'
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,6 +21,10 @@ impl OptDynErr {
|
|||||||
pub fn none() -> Self {
|
pub fn none() -> Self {
|
||||||
Self(None)
|
Self(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for OptDynErr {
|
impl Display for OptDynErr {
|
||||||
|
Loading…
Reference in New Issue
Block a user