Compare commits
No commits in common. "6726cafff4839871be1e489a047127c1d1c3284e" and "721eee9c88431d5269c0ef36d647c97d75cc45ed" have entirely different histories.
6726cafff4
...
721eee9c88
26
Cargo.lock
generated
26
Cargo.lock
generated
@ -575,7 +575,6 @@ dependencies = [
|
|||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"headers",
|
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
@ -2544,30 +2543,6 @@ 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"
|
||||||
@ -5261,7 +5236,6 @@ dependencies = [
|
|||||||
"lightningcss",
|
"lightningcss",
|
||||||
"log",
|
"log",
|
||||||
"maplit",
|
"maplit",
|
||||||
"mime_guess",
|
|
||||||
"mockito",
|
"mockito",
|
||||||
"moka",
|
"moka",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
|
@ -26,6 +26,7 @@ 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",
|
||||||
@ -61,7 +62,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 = { version = "0.10", features = ["typed-header"] }
|
axum-extra = "0.10"
|
||||||
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 excludeFilter://^**/***.svg excludeFilter://^**/***.css excludeFilter://^**/***.js
|
^https://mikanani.me/*** http://127.0.0.1:5005/$1
|
@ -128,7 +128,6 @@ 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,9 +4,17 @@ use tokio::sync::OnceCell;
|
|||||||
|
|
||||||
use super::{Environment, config::AppConfig};
|
use super::{Environment, config::AppConfig};
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::AuthService, cache::CacheService, crypto::CryptoService, database::DatabaseService,
|
auth::AuthService,
|
||||||
errors::RecorderResult, extract::mikan::MikanClient, graphql::GraphQLService,
|
cache::CacheService,
|
||||||
logger::LoggerService, message::MessageService, storage::StorageService, task::TaskService,
|
crypto::CryptoService,
|
||||||
|
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 {
|
||||||
@ -17,7 +25,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) -> &StorageService;
|
fn storage(&self) -> &dyn StorageServiceTrait;
|
||||||
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;
|
||||||
@ -118,7 +126,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) -> &StorageService {
|
fn storage(&self) -> &dyn StorageServiceTrait {
|
||||||
&self.storage
|
&self.storage
|
||||||
}
|
}
|
||||||
fn working_dir(&self) -> &String {
|
fn working_dir(&self) -> &String {
|
||||||
|
@ -6,6 +6,7 @@ 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},
|
||||||
@ -51,14 +52,13 @@ 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, static_c) = futures::try_join!(
|
let (graphql_c, oidc_c, metadata_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, static_c] {
|
for c in [graphql_c, oidc_c, metadata_c] {
|
||||||
router = c.apply_to(router);
|
router = c.apply_to(router);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +86,9 @@ 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;
|
||||||
@ -94,6 +97,7 @@ impl App {
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok::<(), RecorderError>(())
|
Ok::<(), RecorderError>(())
|
||||||
},
|
},
|
||||||
|
@ -11,14 +11,13 @@ 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>,
|
||||||
@ -94,6 +93,12 @@ 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,7 +5,8 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use fetch::{FetchError, HttpClientError, reqwest, reqwest_middleware};
|
use fetch::{FetchError, HttpClientError, reqwest, reqwest_middleware};
|
||||||
use http::{HeaderMap, StatusCode};
|
use http::StatusCode;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -18,19 +19,6 @@ 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)))]
|
||||||
@ -40,10 +28,12 @@ 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(display("Invalid method"))]
|
#[snafu(transparent)]
|
||||||
InvalidMethodError,
|
InvalidMethodError { source: http::method::InvalidMethod },
|
||||||
#[snafu(display("Invalid header name"))]
|
#[snafu(transparent)]
|
||||||
InvalidHeaderNameError,
|
InvalidHeaderNameError {
|
||||||
|
source: http::header::InvalidHeaderName,
|
||||||
|
},
|
||||||
#[snafu(transparent)]
|
#[snafu(transparent)]
|
||||||
TracingAppenderInitError {
|
TracingAppenderInitError {
|
||||||
source: tracing_appender::rolling::InitError,
|
source: tracing_appender::rolling::InitError,
|
||||||
@ -82,8 +72,10 @@ 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(display("Invalid header value"))]
|
#[snafu(transparent)]
|
||||||
InvalidHeaderValueError,
|
InvalidHeaderValueError {
|
||||||
|
source: http::header::InvalidHeaderValue,
|
||||||
|
},
|
||||||
#[snafu(transparent)]
|
#[snafu(transparent)]
|
||||||
HttpClientError { source: HttpClientError },
|
HttpClientError { source: HttpClientError },
|
||||||
#[cfg(feature = "testcontainers")]
|
#[cfg(feature = "testcontainers")]
|
||||||
@ -136,22 +128,6 @@ 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,
|
||||||
@ -201,48 +177,10 @@ 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())),
|
||||||
@ -252,6 +190,28 @@ 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()
|
||||||
@ -264,22 +224,4 @@ 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(origin_name: &str, expected: &str) {
|
pub fn test_torrent_ep_parser(raw_name: &str, expected: &str) {
|
||||||
let extname = Path::new(origin_name)
|
let extname = Path::new(raw_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(origin_name), None, None);
|
parse_episode_subtitle_meta_from_torrent(Path::new(raw_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,8 +299,7 @@ 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 =
|
let found_raw = parse_episode_media_meta_from_torrent(Path::new(raw_name), None, None);
|
||||||
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, StorageService},
|
storage::{StorageContentCategory, StorageServiceTrait},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[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: &StorageService,
|
storage_service: &dyn StorageServiceTrait,
|
||||||
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(
|
.exists_object(
|
||||||
storage_service.build_public_object_path(
|
|
||||||
StorageContentCategory::Image,
|
StorageContentCategory::Image,
|
||||||
MIKAN_POSTER_BUCKET_KEY,
|
subscriber_id,
|
||||||
|
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,14 +765,13 @@ pub async fn scrape_mikan_poster_meta_from_image_url(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let poster_str = storage_service
|
let poster_str = storage_service
|
||||||
.write(
|
.store_object(
|
||||||
storage_service.build_public_object_path(
|
|
||||||
StorageContentCategory::Image,
|
StorageContentCategory::Image,
|
||||||
MIKAN_POSTER_BUCKET_KEY,
|
subscriber_id,
|
||||||
|
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?;
|
||||||
@ -1081,14 +1080,16 @@ 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.build_public_object_path(
|
let storage_fullname = storage_service.get_fullname(
|
||||||
StorageContentCategory::Image,
|
StorageContentCategory::Image,
|
||||||
MIKAN_POSTER_BUCKET_KEY,
|
1,
|
||||||
|
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 origin;
|
pub mod rawname;
|
||||||
|
pub mod bittorrent;
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
pub mod parser;
|
|
||||||
|
|
||||||
pub use parser::{
|
|
||||||
RawEpisodeMeta, extract_episode_meta_from_origin_name, extract_season_from_title_body,
|
|
||||||
};
|
|
5
apps/recorder/src/extract/rawname/mod.rs
Normal file
5
apps/recorder/src/extract/rawname/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod parser;
|
||||||
|
|
||||||
|
pub use parser::{
|
||||||
|
RawEpisodeMeta, extract_episode_meta_from_raw_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_origin_name(s: &str) -> RecorderResult<RawEpisodeMeta> {
|
pub fn extract_episode_meta_from_raw_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_origin_name(s: &str) -> RecorderResult<RawEpiso
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::{RawEpisodeMeta, extract_episode_meta_from_origin_name};
|
use super::{RawEpisodeMeta, extract_episode_meta_from_raw_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_origin_name(raw_name).ok();
|
let found = extract_episode_meta_from_raw_name(raw_name).ok();
|
||||||
|
|
||||||
if expected != found {
|
if expected != found {
|
||||||
println!(
|
println!(
|
@ -43,7 +43,7 @@ pub enum Bangumi {
|
|||||||
MikanBangumiId,
|
MikanBangumiId,
|
||||||
DisplayName,
|
DisplayName,
|
||||||
SubscriberId,
|
SubscriberId,
|
||||||
OriginName,
|
RawName,
|
||||||
Season,
|
Season,
|
||||||
SeasonRaw,
|
SeasonRaw,
|
||||||
Fansub,
|
Fansub,
|
||||||
@ -51,7 +51,6 @@ pub enum Bangumi {
|
|||||||
Filter,
|
Filter,
|
||||||
RssLink,
|
RssLink,
|
||||||
PosterLink,
|
PosterLink,
|
||||||
OriginPosterLink,
|
|
||||||
SavePath,
|
SavePath,
|
||||||
Homepage,
|
Homepage,
|
||||||
}
|
}
|
||||||
@ -70,7 +69,7 @@ pub enum Episodes {
|
|||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
MikanEpisodeId,
|
MikanEpisodeId,
|
||||||
OriginName,
|
RawName,
|
||||||
DisplayName,
|
DisplayName,
|
||||||
BangumiId,
|
BangumiId,
|
||||||
SubscriberId,
|
SubscriberId,
|
||||||
@ -81,7 +80,6 @@ pub enum Episodes {
|
|||||||
SeasonRaw,
|
SeasonRaw,
|
||||||
Fansub,
|
Fansub,
|
||||||
PosterLink,
|
PosterLink,
|
||||||
OriginPosterLink,
|
|
||||||
EpisodeIndex,
|
EpisodeIndex,
|
||||||
Homepage,
|
Homepage,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
@ -102,7 +100,7 @@ pub enum SubscriptionEpisode {
|
|||||||
pub enum Downloads {
|
pub enum Downloads {
|
||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
OriginName,
|
RawName,
|
||||||
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::OriginName))
|
.col(text(Bangumi::RawName))
|
||||||
.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,7 +104,6 @@ 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(
|
||||||
@ -221,7 +220,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::OriginName))
|
.col(text(Episodes::RawName))
|
||||||
.col(text(Episodes::DisplayName))
|
.col(text(Episodes::DisplayName))
|
||||||
.col(integer(Episodes::BangumiId))
|
.col(integer(Episodes::BangumiId))
|
||||||
.col(integer(Episodes::SubscriberId))
|
.col(integer(Episodes::SubscriberId))
|
||||||
@ -231,7 +230,6 @@ 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::OriginName))
|
.col(string(Downloads::RawName))
|
||||||
.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,
|
||||||
},
|
},
|
||||||
origin::extract_season_from_title_body,
|
rawname::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 origin_name: String,
|
pub raw_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,7 +49,6 @@ 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>,
|
||||||
}
|
}
|
||||||
@ -131,11 +130,12 @@ 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.clone() {
|
let poster_link = if let Some(origin_poster_src) = meta.origin_poster_src {
|
||||||
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,12 +148,11 @@ 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()),
|
||||||
origin_name: ActiveValue::Set(meta.bangumi_title),
|
raw_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()
|
||||||
@ -229,7 +228,7 @@ impl Model {
|
|||||||
Column::SubscriberId,
|
Column::SubscriberId,
|
||||||
])
|
])
|
||||||
.update_columns([
|
.update_columns([
|
||||||
Column::OriginName,
|
Column::RawName,
|
||||||
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 origin_name: String,
|
pub raw_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},
|
||||||
origin::extract_episode_meta_from_origin_name,
|
rawname::extract_episode_meta_from_raw_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 origin_name: String,
|
pub raw_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,7 +35,6 @@ 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>,
|
||||||
@ -124,7 +123,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_origin_name(&episode.episode_title)
|
let episode_extention_meta = extract_episode_meta_from_raw_name(&episode.episode_title)
|
||||||
.inspect_err(|err| {
|
.inspect_err(|err| {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
err = ?err,
|
err = ?err,
|
||||||
@ -137,7 +136,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)),
|
||||||
origin_name: ActiveValue::Set(episode.episode_title.clone()),
|
raw_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),
|
||||||
@ -146,7 +145,6 @@ 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()
|
||||||
};
|
};
|
||||||
@ -233,7 +231,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::OriginName, Column::PosterLink, Column::Homepage])
|
.update_columns([Column::RawName, Column::PosterLink, Column::Homepage])
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.exec_with_returning_columns(db, [Column::Id])
|
.exec_with_returning_columns(db, [Column::Id])
|
||||||
|
@ -1,23 +1,13 @@
|
|||||||
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 futures::{Stream, StreamExt};
|
use opendal::{Buffer, Operator, layers::LoggingLayer};
|
||||||
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::{
|
use crate::errors::app_error::RecorderResult;
|
||||||
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")]
|
||||||
@ -53,6 +43,88 @@ 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,
|
||||||
@ -64,224 +136,15 @@ impl StorageService {
|
|||||||
data_dir: config.data_dir.to_string(),
|
data_dir: config.data_dir.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_operator(&self) -> Result<Operator, opendal::Error> {
|
#[async_trait::async_trait]
|
||||||
let op = if cfg!(test) {
|
impl StorageServiceTrait for StorageService {
|
||||||
Operator::new(opendal::services::Memory::default())?
|
fn get_operator(&self) -> RecorderResult<Operator> {
|
||||||
|
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(op)
|
Ok(fs_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, StorageStoredUrl};
|
pub use client::{StorageContentCategory, StorageService, StorageServiceTrait, 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;
|
use crate::{app::AppContextTrait, test_utils::storage::TestingStorageService};
|
||||||
|
|
||||||
#[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<crate::storage::StorageService>,
|
storage: Option<TestingStorageService>,
|
||||||
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) -> &crate::storage::StorageService {
|
fn storage(&self) -> &dyn crate::storage::StorageServiceTrait {
|
||||||
self.storage.as_ref().expect("should set storage")
|
self.storage.as_ref().expect("should set storage")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,28 @@
|
|||||||
use crate::{
|
use opendal::{Operator, layers::LoggingLayer};
|
||||||
errors::RecorderResult,
|
|
||||||
storage::{StorageConfig, StorageService},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn build_testing_storage_service() -> RecorderResult<StorageService> {
|
use crate::{errors::RecorderResult, storage::StorageServiceTrait};
|
||||||
let service = StorageService::from_config(StorageConfig {
|
|
||||||
data_dir: "tests/data".to_string(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(service)
|
pub struct TestingStorageService {
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
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,2 +1 @@
|
|||||||
pub mod http;
|
|
||||||
pub mod json;
|
pub mod json;
|
||||||
|
@ -2,6 +2,5 @@ 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};
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
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 { Img } from '@/components/ui/img';
|
import { Image } from '@/components/ui/image';
|
||||||
|
|
||||||
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">
|
||||||
<Img
|
<Image
|
||||||
src="/assets/favicon.png"
|
src="/assets/favicon.png"
|
||||||
alt="App Logo"
|
alt="App Logo"
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
|
9
apps/webui/src/components/ui/image.tsx
Normal file
9
apps/webui/src/components/ui/image.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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} />;
|
||||||
|
};
|
@ -1,9 +0,0 @@
|
|||||||
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,6 +106,7 @@ 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 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 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,
|
||||||
"\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 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 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,
|
||||||
"\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 season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"];
|
export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n 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"];
|
||||||
/**
|
/**
|
||||||
* 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,9 +30,8 @@ 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'];
|
||||||
@ -75,9 +74,8 @@ 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'];
|
||||||
@ -110,9 +108,8 @@ 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>;
|
||||||
@ -130,9 +127,8 @@ 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'];
|
||||||
@ -150,9 +146,8 @@ 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>;
|
||||||
@ -170,9 +165,8 @@ 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']>;
|
||||||
@ -498,7 +492,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;
|
||||||
originName: Scalars['String']['output'];
|
rawName: Scalars['String']['output'];
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
status: DownloadStatusEnum;
|
status: DownloadStatusEnum;
|
||||||
subscriber?: Maybe<Subscribers>;
|
subscriber?: Maybe<Subscribers>;
|
||||||
@ -518,7 +512,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;
|
||||||
originName: Scalars['String']['output'];
|
rawName: 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'];
|
||||||
@ -552,7 +546,7 @@ export type DownloadsFilterInput = {
|
|||||||
id?: InputMaybe<IntegerFilterInput>;
|
id?: InputMaybe<IntegerFilterInput>;
|
||||||
mime?: InputMaybe<DownloadMimeEnumFilterInput>;
|
mime?: InputMaybe<DownloadMimeEnumFilterInput>;
|
||||||
or?: InputMaybe<Array<DownloadsFilterInput>>;
|
or?: InputMaybe<Array<DownloadsFilterInput>>;
|
||||||
originName?: InputMaybe<StringFilterInput>;
|
rawName?: InputMaybe<StringFilterInput>;
|
||||||
savePath?: InputMaybe<StringFilterInput>;
|
savePath?: InputMaybe<StringFilterInput>;
|
||||||
status?: InputMaybe<DownloadStatusEnumFilterInput>;
|
status?: InputMaybe<DownloadStatusEnumFilterInput>;
|
||||||
subscriberId?: InputMaybe<SubscriberIdFilterInput>;
|
subscriberId?: InputMaybe<SubscriberIdFilterInput>;
|
||||||
@ -570,7 +564,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;
|
||||||
originName: Scalars['String']['input'];
|
rawName: 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']>;
|
||||||
@ -588,7 +582,7 @@ export type DownloadsOrderInput = {
|
|||||||
homepage?: InputMaybe<OrderByEnum>;
|
homepage?: InputMaybe<OrderByEnum>;
|
||||||
id?: InputMaybe<OrderByEnum>;
|
id?: InputMaybe<OrderByEnum>;
|
||||||
mime?: InputMaybe<OrderByEnum>;
|
mime?: InputMaybe<OrderByEnum>;
|
||||||
originName?: InputMaybe<OrderByEnum>;
|
rawName?: InputMaybe<OrderByEnum>;
|
||||||
savePath?: InputMaybe<OrderByEnum>;
|
savePath?: InputMaybe<OrderByEnum>;
|
||||||
status?: InputMaybe<OrderByEnum>;
|
status?: InputMaybe<OrderByEnum>;
|
||||||
subscriberId?: InputMaybe<OrderByEnum>;
|
subscriberId?: InputMaybe<OrderByEnum>;
|
||||||
@ -606,7 +600,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>;
|
||||||
originName?: InputMaybe<Scalars['String']['input']>;
|
rawName?: 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']>;
|
||||||
@ -625,9 +619,8 @@ 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'];
|
||||||
@ -672,9 +665,8 @@ 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'];
|
||||||
@ -710,9 +702,8 @@ 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>;
|
||||||
@ -732,9 +723,8 @@ 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'];
|
||||||
@ -754,9 +744,8 @@ 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>;
|
||||||
@ -776,9 +765,8 @@ 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']>;
|
||||||
@ -1720,7 +1708,7 @@ export type GetSubscriptionDetailQueryVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, savePath?: string | null, homepage?: string | null }> } }> } };
|
export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, 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 SyncSubscriptionFeedsIncrementalMutationVariables = Exact<{
|
export type SyncSubscriptionFeedsIncrementalMutationVariables = Exact<{
|
||||||
filter: SubscriptionsFilterInput;
|
filter: SubscriptionsFilterInput;
|
||||||
@ -1777,7 +1765,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":"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":"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 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,7 +10,6 @@ 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';
|
||||||
@ -325,18 +324,7 @@ 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-2 gap-4 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<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
|
||||||
@ -345,14 +333,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">
|
|
||||||
Fansub
|
|
||||||
</Label>
|
|
||||||
<div className="text-sm">
|
|
||||||
{bangumi.fansub || '-'}
|
|
||||||
</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">
|
||||||
Season
|
Season
|
||||||
@ -363,13 +343,18 @@ 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">
|
||||||
Updated At
|
Fansub
|
||||||
|
</Label>
|
||||||
|
<div className="text-sm">
|
||||||
|
{bangumi.fansub || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-muted-foreground text-xs">
|
||||||
|
Save Path
|
||||||
</Label>
|
</Label>
|
||||||
<div className="font-mono text-sm">
|
<div className="font-mono text-sm">
|
||||||
{format(
|
{bangumi.savePath || '-'}
|
||||||
new Date(bangumi.updatedAt),
|
|
||||||
'yyyy-MM-dd'
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,10 +21,6 @@ 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