feat: support static server
This commit is contained in:
parent
35312ea1ff
commit
6726cafff4
@ -1 +1 @@
|
|||||||
^https://mikanani.me/*** http://127.0.0.1:5005/$1
|
^https://mikanani.me/*** http://127.0.0.1:5005/$1 excludeFilter://^**/***.svg excludeFilter://^**/***.css excludeFilter://^**/***.js
|
@ -6,7 +6,6 @@ use tracing::instrument;
|
|||||||
|
|
||||||
use super::{builder::AppBuilder, context::AppContextTrait};
|
use super::{builder::AppBuilder, context::AppContextTrait};
|
||||||
use crate::{
|
use crate::{
|
||||||
app::Environment,
|
|
||||||
errors::{RecorderError, RecorderResult},
|
errors::{RecorderError, RecorderResult},
|
||||||
web::{
|
web::{
|
||||||
controller::{self, core::ControllerTrait},
|
controller::{self, core::ControllerTrait},
|
||||||
@ -52,13 +51,14 @@ impl App {
|
|||||||
|
|
||||||
let mut router = Router::<Arc<dyn AppContextTrait>>::new();
|
let mut router = Router::<Arc<dyn AppContextTrait>>::new();
|
||||||
|
|
||||||
let (graphql_c, oidc_c, metadata_c) = futures::try_join!(
|
let (graphql_c, oidc_c, metadata_c, static_c) = futures::try_join!(
|
||||||
controller::graphql::create(context.clone()),
|
controller::graphql::create(context.clone()),
|
||||||
controller::oidc::create(context.clone()),
|
controller::oidc::create(context.clone()),
|
||||||
controller::metadata::create(context.clone())
|
controller::metadata::create(context.clone()),
|
||||||
|
controller::r#static::create(context.clone()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for c in [graphql_c, oidc_c, metadata_c] {
|
for c in [graphql_c, oidc_c, metadata_c, static_c] {
|
||||||
router = c.apply_to(router);
|
router = c.apply_to(router);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,9 +86,6 @@ impl App {
|
|||||||
async {
|
async {
|
||||||
{
|
{
|
||||||
let monitor = task.setup_monitor().await?;
|
let monitor = task.setup_monitor().await?;
|
||||||
if matches!(context.environment(), Environment::Development) {
|
|
||||||
monitor.run().await?;
|
|
||||||
} else {
|
|
||||||
monitor
|
monitor
|
||||||
.run_with_signal(async move {
|
.run_with_signal(async move {
|
||||||
Self::shutdown_signal().await;
|
Self::shutdown_signal().await;
|
||||||
@ -97,7 +94,6 @@ impl App {
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<(), RecorderError>(())
|
Ok::<(), RecorderError>(())
|
||||||
},
|
},
|
||||||
|
@ -40,12 +40,10 @@ pub enum RecorderError {
|
|||||||
NetAddrParseError { source: std::net::AddrParseError },
|
NetAddrParseError { source: std::net::AddrParseError },
|
||||||
#[snafu(transparent)]
|
#[snafu(transparent)]
|
||||||
RegexError { source: regex::Error },
|
RegexError { source: regex::Error },
|
||||||
#[snafu(transparent)]
|
#[snafu(display("Invalid method"))]
|
||||||
InvalidMethodError { source: http::method::InvalidMethod },
|
InvalidMethodError,
|
||||||
#[snafu(transparent)]
|
#[snafu(display("Invalid header name"))]
|
||||||
InvalidHeaderNameError {
|
InvalidHeaderNameError,
|
||||||
source: http::header::InvalidHeaderName,
|
|
||||||
},
|
|
||||||
#[snafu(transparent)]
|
#[snafu(transparent)]
|
||||||
TracingAppenderInitError {
|
TracingAppenderInitError {
|
||||||
source: tracing_appender::rolling::InitError,
|
source: tracing_appender::rolling::InitError,
|
||||||
@ -84,10 +82,8 @@ pub enum RecorderError {
|
|||||||
#[snafu(source(from(opendal::Error, Box::new)))]
|
#[snafu(source(from(opendal::Error, Box::new)))]
|
||||||
source: Box<opendal::Error>,
|
source: Box<opendal::Error>,
|
||||||
},
|
},
|
||||||
#[snafu(transparent)]
|
#[snafu(display("Invalid header value"))]
|
||||||
InvalidHeaderValueError {
|
InvalidHeaderValueError,
|
||||||
source: http::header::InvalidHeaderValue,
|
|
||||||
},
|
|
||||||
#[snafu(transparent)]
|
#[snafu(transparent)]
|
||||||
HttpClientError { source: HttpClientError },
|
HttpClientError { source: HttpClientError },
|
||||||
#[cfg(feature = "testcontainers")]
|
#[cfg(feature = "testcontainers")]
|
||||||
@ -234,7 +230,8 @@ impl IntoResponse for RecorderError {
|
|||||||
headers,
|
headers,
|
||||||
source,
|
source,
|
||||||
} => {
|
} => {
|
||||||
let message = Option::<_>::from(source)
|
let message = source
|
||||||
|
.into_inner()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
String::from(status.canonical_reason().unwrap_or("Unknown"))
|
String::from(status.canonical_reason().unwrap_or("Unknown"))
|
||||||
@ -267,4 +264,22 @@ impl From<reqwest_middleware::Error> for RecorderError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<http::header::InvalidHeaderValue> for RecorderError {
|
||||||
|
fn from(_error: http::header::InvalidHeaderValue) -> Self {
|
||||||
|
Self::InvalidHeaderValueError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<http::header::InvalidHeaderName> for RecorderError {
|
||||||
|
fn from(_error: http::header::InvalidHeaderName) -> Self {
|
||||||
|
Self::InvalidHeaderNameError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<http::method::InvalidMethod> for RecorderError {
|
||||||
|
fn from(_error: http::method::InvalidMethod) -> Self {
|
||||||
|
Self::InvalidMethodError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type RecorderResult<T> = Result<T, RecorderError>;
|
pub type RecorderResult<T> = Result<T, RecorderError>;
|
||||||
|
@ -268,8 +268,8 @@ mod tests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_torrent_ep_parser(raw_name: &str, expected: &str) {
|
pub fn test_torrent_ep_parser(origin_name: &str, expected: &str) {
|
||||||
let extname = Path::new(raw_name)
|
let extname = Path::new(origin_name)
|
||||||
.extension()
|
.extension()
|
||||||
.map(|e| format!(".{e}"))
|
.map(|e| format!(".{e}"))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@ -278,7 +278,7 @@ mod tests {
|
|||||||
if extname == ".srt" || extname == ".ass" {
|
if extname == ".srt" || extname == ".ass" {
|
||||||
let expected: Option<TorrentEpisodeSubtitleMeta> = serde_json::from_str(expected).ok();
|
let expected: Option<TorrentEpisodeSubtitleMeta> = serde_json::from_str(expected).ok();
|
||||||
let found_raw =
|
let found_raw =
|
||||||
parse_episode_subtitle_meta_from_torrent(Path::new(raw_name), None, None);
|
parse_episode_subtitle_meta_from_torrent(Path::new(origin_name), None, None);
|
||||||
let found = found_raw.as_ref().ok().cloned();
|
let found = found_raw.as_ref().ok().cloned();
|
||||||
|
|
||||||
if expected != found {
|
if expected != found {
|
||||||
@ -299,7 +299,8 @@ mod tests {
|
|||||||
assert_eq!(expected, found);
|
assert_eq!(expected, found);
|
||||||
} else {
|
} else {
|
||||||
let expected: Option<TorrentEpisodeMediaMeta> = serde_json::from_str(expected).ok();
|
let expected: Option<TorrentEpisodeMediaMeta> = serde_json::from_str(expected).ok();
|
||||||
let found_raw = parse_episode_media_meta_from_torrent(Path::new(raw_name), None, None);
|
let found_raw =
|
||||||
|
parse_episode_media_meta_from_torrent(Path::new(origin_name), None, None);
|
||||||
let found = found_raw.as_ref().ok().cloned();
|
let found = found_raw.as_ref().ok().cloned();
|
||||||
|
|
||||||
if expected != found {
|
if expected != found {
|
||||||
|
@ -741,13 +741,11 @@ pub async fn scrape_mikan_poster_meta_from_image_url(
|
|||||||
mikan_client: &MikanClient,
|
mikan_client: &MikanClient,
|
||||||
storage_service: &StorageService,
|
storage_service: &StorageService,
|
||||||
origin_poster_src_url: Url,
|
origin_poster_src_url: Url,
|
||||||
subscriber_id: i32,
|
|
||||||
) -> RecorderResult<MikanBangumiPosterMeta> {
|
) -> RecorderResult<MikanBangumiPosterMeta> {
|
||||||
if let Some(poster_src) = storage_service
|
if let Some(poster_src) = storage_service
|
||||||
.exists(
|
.exists(
|
||||||
storage_service.build_subscriber_object_path(
|
storage_service.build_public_object_path(
|
||||||
StorageContentCategory::Image,
|
StorageContentCategory::Image,
|
||||||
subscriber_id,
|
|
||||||
MIKAN_POSTER_BUCKET_KEY,
|
MIKAN_POSTER_BUCKET_KEY,
|
||||||
&origin_poster_src_url
|
&origin_poster_src_url
|
||||||
.path()
|
.path()
|
||||||
@ -768,9 +766,8 @@ pub async fn scrape_mikan_poster_meta_from_image_url(
|
|||||||
|
|
||||||
let poster_str = storage_service
|
let poster_str = storage_service
|
||||||
.write(
|
.write(
|
||||||
storage_service.build_subscriber_object_path(
|
storage_service.build_public_object_path(
|
||||||
StorageContentCategory::Image,
|
StorageContentCategory::Image,
|
||||||
subscriber_id,
|
|
||||||
MIKAN_POSTER_BUCKET_KEY,
|
MIKAN_POSTER_BUCKET_KEY,
|
||||||
&origin_poster_src_url
|
&origin_poster_src_url
|
||||||
.path()
|
.path()
|
||||||
@ -1084,15 +1081,13 @@ 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_subscriber_object_path(
|
let storage_fullname = storage_service.build_public_object_path(
|
||||||
StorageContentCategory::Image,
|
StorageContentCategory::Image,
|
||||||
1,
|
|
||||||
MIKAN_POSTER_BUCKET_KEY,
|
MIKAN_POSTER_BUCKET_KEY,
|
||||||
"202309/5ce9fed1.jpg",
|
"202309/5ce9fed1.jpg",
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
pub mod bittorrent;
|
||||||
pub mod defs;
|
pub mod defs;
|
||||||
pub mod html;
|
pub mod html;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod media;
|
pub mod media;
|
||||||
pub mod mikan;
|
pub mod mikan;
|
||||||
pub mod rawname;
|
pub mod origin;
|
||||||
pub mod bittorrent;
|
|
||||||
|
5
apps/recorder/src/extract/origin/mod.rs
Normal file
5
apps/recorder/src/extract/origin/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod parser;
|
||||||
|
|
||||||
|
pub use parser::{
|
||||||
|
RawEpisodeMeta, extract_episode_meta_from_origin_name, extract_season_from_title_body,
|
||||||
|
};
|
@ -261,7 +261,7 @@ pub fn check_is_movie(title: &str) -> bool {
|
|||||||
MOVIE_TITLE_RE.is_match(title)
|
MOVIE_TITLE_RE.is_match(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_episode_meta_from_raw_name(s: &str) -> RecorderResult<RawEpisodeMeta> {
|
pub fn extract_episode_meta_from_origin_name(s: &str) -> RecorderResult<RawEpisodeMeta> {
|
||||||
let raw_title = s.trim();
|
let raw_title = s.trim();
|
||||||
let raw_title_without_ch_brackets = replace_ch_bracket_to_en(raw_title);
|
let raw_title_without_ch_brackets = replace_ch_bracket_to_en(raw_title);
|
||||||
let fansub = extract_fansub(&raw_title_without_ch_brackets);
|
let fansub = extract_fansub(&raw_title_without_ch_brackets);
|
||||||
@ -321,11 +321,11 @@ pub fn extract_episode_meta_from_raw_name(s: &str) -> RecorderResult<RawEpisodeM
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::{RawEpisodeMeta, extract_episode_meta_from_raw_name};
|
use super::{RawEpisodeMeta, extract_episode_meta_from_origin_name};
|
||||||
|
|
||||||
fn test_raw_ep_parser_case(raw_name: &str, expected: &str) {
|
fn test_raw_ep_parser_case(raw_name: &str, expected: &str) {
|
||||||
let expected: Option<RawEpisodeMeta> = serde_json::from_str(expected).unwrap_or_default();
|
let expected: Option<RawEpisodeMeta> = serde_json::from_str(expected).unwrap_or_default();
|
||||||
let found = extract_episode_meta_from_raw_name(raw_name).ok();
|
let found = extract_episode_meta_from_origin_name(raw_name).ok();
|
||||||
|
|
||||||
if expected != found {
|
if expected != found {
|
||||||
println!(
|
println!(
|
@ -1,5 +0,0 @@
|
|||||||
pub mod parser;
|
|
||||||
|
|
||||||
pub use parser::{
|
|
||||||
RawEpisodeMeta, extract_episode_meta_from_raw_name, extract_season_from_title_body,
|
|
||||||
};
|
|
@ -43,7 +43,7 @@ pub enum Bangumi {
|
|||||||
MikanBangumiId,
|
MikanBangumiId,
|
||||||
DisplayName,
|
DisplayName,
|
||||||
SubscriberId,
|
SubscriberId,
|
||||||
RawName,
|
OriginName,
|
||||||
Season,
|
Season,
|
||||||
SeasonRaw,
|
SeasonRaw,
|
||||||
Fansub,
|
Fansub,
|
||||||
@ -51,6 +51,7 @@ pub enum Bangumi {
|
|||||||
Filter,
|
Filter,
|
||||||
RssLink,
|
RssLink,
|
||||||
PosterLink,
|
PosterLink,
|
||||||
|
OriginPosterLink,
|
||||||
SavePath,
|
SavePath,
|
||||||
Homepage,
|
Homepage,
|
||||||
}
|
}
|
||||||
@ -69,7 +70,7 @@ pub enum Episodes {
|
|||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
MikanEpisodeId,
|
MikanEpisodeId,
|
||||||
RawName,
|
OriginName,
|
||||||
DisplayName,
|
DisplayName,
|
||||||
BangumiId,
|
BangumiId,
|
||||||
SubscriberId,
|
SubscriberId,
|
||||||
@ -80,6 +81,7 @@ pub enum Episodes {
|
|||||||
SeasonRaw,
|
SeasonRaw,
|
||||||
Fansub,
|
Fansub,
|
||||||
PosterLink,
|
PosterLink,
|
||||||
|
OriginPosterLink,
|
||||||
EpisodeIndex,
|
EpisodeIndex,
|
||||||
Homepage,
|
Homepage,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
@ -100,7 +102,7 @@ pub enum SubscriptionEpisode {
|
|||||||
pub enum Downloads {
|
pub enum Downloads {
|
||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
RawName,
|
OriginName,
|
||||||
DisplayName,
|
DisplayName,
|
||||||
SubscriberId,
|
SubscriberId,
|
||||||
DownloaderId,
|
DownloaderId,
|
||||||
|
@ -96,7 +96,7 @@ impl MigrationTrait for Migration {
|
|||||||
.col(text_null(Bangumi::MikanBangumiId))
|
.col(text_null(Bangumi::MikanBangumiId))
|
||||||
.col(integer(Bangumi::SubscriberId))
|
.col(integer(Bangumi::SubscriberId))
|
||||||
.col(text(Bangumi::DisplayName))
|
.col(text(Bangumi::DisplayName))
|
||||||
.col(text(Bangumi::RawName))
|
.col(text(Bangumi::OriginName))
|
||||||
.col(integer(Bangumi::Season))
|
.col(integer(Bangumi::Season))
|
||||||
.col(text_null(Bangumi::SeasonRaw))
|
.col(text_null(Bangumi::SeasonRaw))
|
||||||
.col(text_null(Bangumi::Fansub))
|
.col(text_null(Bangumi::Fansub))
|
||||||
@ -104,6 +104,7 @@ impl MigrationTrait for Migration {
|
|||||||
.col(json_binary_null(Bangumi::Filter))
|
.col(json_binary_null(Bangumi::Filter))
|
||||||
.col(text_null(Bangumi::RssLink))
|
.col(text_null(Bangumi::RssLink))
|
||||||
.col(text_null(Bangumi::PosterLink))
|
.col(text_null(Bangumi::PosterLink))
|
||||||
|
.col(text_null(Bangumi::OriginPosterLink))
|
||||||
.col(text_null(Bangumi::SavePath))
|
.col(text_null(Bangumi::SavePath))
|
||||||
.col(text_null(Bangumi::Homepage))
|
.col(text_null(Bangumi::Homepage))
|
||||||
.foreign_key(
|
.foreign_key(
|
||||||
@ -220,7 +221,7 @@ impl MigrationTrait for Migration {
|
|||||||
table_auto_z(Episodes::Table)
|
table_auto_z(Episodes::Table)
|
||||||
.col(pk_auto(Episodes::Id))
|
.col(pk_auto(Episodes::Id))
|
||||||
.col(text_null(Episodes::MikanEpisodeId))
|
.col(text_null(Episodes::MikanEpisodeId))
|
||||||
.col(text(Episodes::RawName))
|
.col(text(Episodes::OriginName))
|
||||||
.col(text(Episodes::DisplayName))
|
.col(text(Episodes::DisplayName))
|
||||||
.col(integer(Episodes::BangumiId))
|
.col(integer(Episodes::BangumiId))
|
||||||
.col(integer(Episodes::SubscriberId))
|
.col(integer(Episodes::SubscriberId))
|
||||||
@ -230,6 +231,7 @@ impl MigrationTrait for Migration {
|
|||||||
.col(text_null(Episodes::SeasonRaw))
|
.col(text_null(Episodes::SeasonRaw))
|
||||||
.col(text_null(Episodes::Fansub))
|
.col(text_null(Episodes::Fansub))
|
||||||
.col(text_null(Episodes::PosterLink))
|
.col(text_null(Episodes::PosterLink))
|
||||||
|
.col(text_null(Episodes::OriginPosterLink))
|
||||||
.col(integer(Episodes::EpisodeIndex))
|
.col(integer(Episodes::EpisodeIndex))
|
||||||
.col(text_null(Episodes::Homepage))
|
.col(text_null(Episodes::Homepage))
|
||||||
.col(text_null(Episodes::Subtitle))
|
.col(text_null(Episodes::Subtitle))
|
||||||
|
@ -80,7 +80,7 @@ impl MigrationTrait for Migration {
|
|||||||
.create_table(
|
.create_table(
|
||||||
table_auto_z(Downloads::Table)
|
table_auto_z(Downloads::Table)
|
||||||
.col(pk_auto(Downloads::Id))
|
.col(pk_auto(Downloads::Id))
|
||||||
.col(string(Downloads::RawName))
|
.col(string(Downloads::OriginName))
|
||||||
.col(string(Downloads::DisplayName))
|
.col(string(Downloads::DisplayName))
|
||||||
.col(integer(Downloads::SubscriberId))
|
.col(integer(Downloads::SubscriberId))
|
||||||
.col(integer(Downloads::DownloaderId))
|
.col(integer(Downloads::DownloaderId))
|
||||||
|
@ -17,7 +17,7 @@ use crate::{
|
|||||||
MikanBangumiHash, MikanBangumiMeta, build_mikan_bangumi_subscription_rss_url,
|
MikanBangumiHash, MikanBangumiMeta, build_mikan_bangumi_subscription_rss_url,
|
||||||
scrape_mikan_poster_meta_from_image_url,
|
scrape_mikan_poster_meta_from_image_url,
|
||||||
},
|
},
|
||||||
rawname::extract_season_from_title_body,
|
origin::extract_season_from_title_body,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ pub struct Model {
|
|||||||
pub mikan_bangumi_id: Option<String>,
|
pub mikan_bangumi_id: Option<String>,
|
||||||
pub subscriber_id: i32,
|
pub subscriber_id: i32,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub raw_name: String,
|
pub origin_name: String,
|
||||||
pub season: i32,
|
pub season: i32,
|
||||||
pub season_raw: Option<String>,
|
pub season_raw: Option<String>,
|
||||||
pub fansub: Option<String>,
|
pub fansub: Option<String>,
|
||||||
@ -49,6 +49,7 @@ pub struct Model {
|
|||||||
pub filter: Option<BangumiFilter>,
|
pub filter: Option<BangumiFilter>,
|
||||||
pub rss_link: Option<String>,
|
pub rss_link: Option<String>,
|
||||||
pub poster_link: Option<String>,
|
pub poster_link: Option<String>,
|
||||||
|
pub origin_poster_link: Option<String>,
|
||||||
pub save_path: Option<String>,
|
pub save_path: Option<String>,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
}
|
}
|
||||||
@ -130,12 +131,11 @@ impl ActiveModel {
|
|||||||
Some(&meta.mikan_fansub_id),
|
Some(&meta.mikan_fansub_id),
|
||||||
);
|
);
|
||||||
|
|
||||||
let poster_link = if let Some(origin_poster_src) = meta.origin_poster_src {
|
let poster_link = if let Some(origin_poster_src) = meta.origin_poster_src.clone() {
|
||||||
let poster_meta = scrape_mikan_poster_meta_from_image_url(
|
let poster_meta = scrape_mikan_poster_meta_from_image_url(
|
||||||
mikan_client,
|
mikan_client,
|
||||||
storage_service,
|
storage_service,
|
||||||
origin_poster_src,
|
origin_poster_src,
|
||||||
subscriber_id,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
poster_meta.poster_src
|
poster_meta.poster_src
|
||||||
@ -148,11 +148,12 @@ impl ActiveModel {
|
|||||||
mikan_fansub_id: ActiveValue::Set(Some(meta.mikan_fansub_id)),
|
mikan_fansub_id: ActiveValue::Set(Some(meta.mikan_fansub_id)),
|
||||||
subscriber_id: ActiveValue::Set(subscriber_id),
|
subscriber_id: ActiveValue::Set(subscriber_id),
|
||||||
display_name: ActiveValue::Set(meta.bangumi_title.clone()),
|
display_name: ActiveValue::Set(meta.bangumi_title.clone()),
|
||||||
raw_name: ActiveValue::Set(meta.bangumi_title),
|
origin_name: ActiveValue::Set(meta.bangumi_title),
|
||||||
season: ActiveValue::Set(season_index),
|
season: ActiveValue::Set(season_index),
|
||||||
season_raw: ActiveValue::Set(season_raw),
|
season_raw: ActiveValue::Set(season_raw),
|
||||||
fansub: ActiveValue::Set(Some(meta.fansub)),
|
fansub: ActiveValue::Set(Some(meta.fansub)),
|
||||||
poster_link: ActiveValue::Set(poster_link),
|
poster_link: ActiveValue::Set(poster_link),
|
||||||
|
origin_poster_link: ActiveValue::Set(meta.origin_poster_src.map(|src| src.to_string())),
|
||||||
homepage: ActiveValue::Set(Some(meta.homepage.to_string())),
|
homepage: ActiveValue::Set(Some(meta.homepage.to_string())),
|
||||||
rss_link: ActiveValue::Set(Some(rss_url.to_string())),
|
rss_link: ActiveValue::Set(Some(rss_url.to_string())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@ -228,7 +229,7 @@ impl Model {
|
|||||||
Column::SubscriberId,
|
Column::SubscriberId,
|
||||||
])
|
])
|
||||||
.update_columns([
|
.update_columns([
|
||||||
Column::RawName,
|
Column::OriginName,
|
||||||
Column::Fansub,
|
Column::Fansub,
|
||||||
Column::PosterLink,
|
Column::PosterLink,
|
||||||
Column::Season,
|
Column::Season,
|
||||||
|
@ -44,7 +44,7 @@ pub struct Model {
|
|||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub raw_name: String,
|
pub origin_name: String,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub downloader_id: i32,
|
pub downloader_id: i32,
|
||||||
pub episode_id: i32,
|
pub episode_id: i32,
|
||||||
|
@ -10,7 +10,7 @@ use crate::{
|
|||||||
errors::RecorderResult,
|
errors::RecorderResult,
|
||||||
extract::{
|
extract::{
|
||||||
mikan::{MikanEpisodeHash, MikanEpisodeMeta, build_mikan_episode_homepage_url},
|
mikan::{MikanEpisodeHash, MikanEpisodeMeta, build_mikan_episode_homepage_url},
|
||||||
rawname::extract_episode_meta_from_raw_name,
|
origin::extract_episode_meta_from_origin_name,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ pub struct Model {
|
|||||||
pub id: i32,
|
pub id: i32,
|
||||||
#[sea_orm(indexed)]
|
#[sea_orm(indexed)]
|
||||||
pub mikan_episode_id: Option<String>,
|
pub mikan_episode_id: Option<String>,
|
||||||
pub raw_name: String,
|
pub origin_name: String,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub bangumi_id: i32,
|
pub bangumi_id: i32,
|
||||||
pub subscriber_id: i32,
|
pub subscriber_id: i32,
|
||||||
@ -35,6 +35,7 @@ pub struct Model {
|
|||||||
pub season_raw: Option<String>,
|
pub season_raw: Option<String>,
|
||||||
pub fansub: Option<String>,
|
pub fansub: Option<String>,
|
||||||
pub poster_link: Option<String>,
|
pub poster_link: Option<String>,
|
||||||
|
pub origin_poster_link: Option<String>,
|
||||||
pub episode_index: i32,
|
pub episode_index: i32,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
pub subtitle: Option<String>,
|
pub subtitle: Option<String>,
|
||||||
@ -123,7 +124,7 @@ impl ActiveModel {
|
|||||||
episode: MikanEpisodeMeta,
|
episode: MikanEpisodeMeta,
|
||||||
) -> RecorderResult<Self> {
|
) -> RecorderResult<Self> {
|
||||||
let mikan_base_url = ctx.mikan().base_url().clone();
|
let mikan_base_url = ctx.mikan().base_url().clone();
|
||||||
let episode_extention_meta = extract_episode_meta_from_raw_name(&episode.episode_title)
|
let episode_extention_meta = extract_episode_meta_from_origin_name(&episode.episode_title)
|
||||||
.inspect_err(|err| {
|
.inspect_err(|err| {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
err = ?err,
|
err = ?err,
|
||||||
@ -136,7 +137,7 @@ impl ActiveModel {
|
|||||||
|
|
||||||
let mut episode_active_model = Self {
|
let mut episode_active_model = Self {
|
||||||
mikan_episode_id: ActiveValue::Set(Some(episode.mikan_episode_id)),
|
mikan_episode_id: ActiveValue::Set(Some(episode.mikan_episode_id)),
|
||||||
raw_name: ActiveValue::Set(episode.episode_title.clone()),
|
origin_name: ActiveValue::Set(episode.episode_title.clone()),
|
||||||
display_name: ActiveValue::Set(episode.episode_title.clone()),
|
display_name: ActiveValue::Set(episode.episode_title.clone()),
|
||||||
bangumi_id: ActiveValue::Set(bangumi.id),
|
bangumi_id: ActiveValue::Set(bangumi.id),
|
||||||
subscriber_id: ActiveValue::Set(bangumi.subscriber_id),
|
subscriber_id: ActiveValue::Set(bangumi.subscriber_id),
|
||||||
@ -145,6 +146,7 @@ impl ActiveModel {
|
|||||||
season: ActiveValue::Set(bangumi.season),
|
season: ActiveValue::Set(bangumi.season),
|
||||||
fansub: ActiveValue::Set(bangumi.fansub.clone()),
|
fansub: ActiveValue::Set(bangumi.fansub.clone()),
|
||||||
poster_link: ActiveValue::Set(bangumi.poster_link.clone()),
|
poster_link: ActiveValue::Set(bangumi.poster_link.clone()),
|
||||||
|
origin_poster_link: ActiveValue::Set(bangumi.origin_poster_link.clone()),
|
||||||
episode_index: ActiveValue::Set(0),
|
episode_index: ActiveValue::Set(0),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@ -231,7 +233,7 @@ impl Model {
|
|||||||
let new_episode_ids = Entity::insert_many(new_episode_active_modes)
|
let new_episode_ids = Entity::insert_many(new_episode_active_modes)
|
||||||
.on_conflict(
|
.on_conflict(
|
||||||
OnConflict::columns([Column::MikanEpisodeId, Column::SubscriberId])
|
OnConflict::columns([Column::MikanEpisodeId, Column::SubscriberId])
|
||||||
.update_columns([Column::RawName, Column::PosterLink, Column::Homepage])
|
.update_columns([Column::OriginName, Column::PosterLink, Column::Homepage])
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.exec_with_returning_columns(db, [Column::Id])
|
.exec_with_returning_columns(db, [Column::Id])
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use async_stream::try_stream;
|
||||||
|
use axum::{body::Body, response::Response};
|
||||||
|
use axum_extra::{TypedHeader, headers::Range};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
use futures::{Stream, StreamExt};
|
||||||
|
use http::{HeaderValue, StatusCode, header};
|
||||||
use opendal::{Buffer, Metadata, Operator, Reader, Writer, layers::LoggingLayer};
|
use opendal::{Buffer, Metadata, Operator, Reader, Writer, layers::LoggingLayer};
|
||||||
use quirks_path::PathBuf;
|
use quirks_path::{Path, PathBuf};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::instrument;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::StorageConfig;
|
use super::StorageConfig;
|
||||||
use crate::errors::app_error::RecorderResult;
|
use crate::{
|
||||||
|
errors::{RecorderError, RecorderResult},
|
||||||
|
utils::http::{bound_range_to_content_range, build_no_satisfiable_content_range},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@ -69,23 +79,44 @@ impl StorageService {
|
|||||||
Ok(op)
|
Ok(op)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_subscriber_path(&self, subscriber_id: i32, path: &str) -> PathBuf {
|
pub fn build_subscriber_path(&self, subscriber_id: i32, path: impl AsRef<Path>) -> PathBuf {
|
||||||
let mut p = PathBuf::from("/subscribers");
|
let mut p = PathBuf::from("/subscribers");
|
||||||
p.push(subscriber_id.to_string());
|
p.push(subscriber_id.to_string());
|
||||||
p.push(path);
|
p.push(path);
|
||||||
p
|
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(
|
pub fn build_subscriber_object_path(
|
||||||
&self,
|
&self,
|
||||||
content_category: StorageContentCategory,
|
|
||||||
subscriber_id: i32,
|
subscriber_id: i32,
|
||||||
|
content_category: StorageContentCategory,
|
||||||
bucket: &str,
|
bucket: &str,
|
||||||
object_name: &str,
|
object_name: &str,
|
||||||
) -> PathBuf {
|
) -> PathBuf {
|
||||||
self.build_subscriber_path(
|
self.build_subscriber_path(
|
||||||
subscriber_id,
|
subscriber_id,
|
||||||
&format!("{}/{}/{}", content_category.as_ref(), bucket, object_name),
|
[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>(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,4 +187,101 @@ impl StorageService {
|
|||||||
|
|
||||||
Ok(metadata)
|
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,18 +1,23 @@
|
|||||||
use std::ops::Bound;
|
use std::ops::Bound;
|
||||||
|
|
||||||
pub fn bound_range_to_content_range(
|
use http::HeaderValue;
|
||||||
r: &(Bound<u64>, Bound<u64>),
|
|
||||||
l: u64,
|
pub fn build_no_satisfiable_content_range(len: u64) -> HeaderValue {
|
||||||
) -> Result<String, String> {
|
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 {
|
match r {
|
||||||
(Bound::Included(start), Bound::Included(end)) => Ok(format!("bytes {start}-{end}/{l}")),
|
(Bound::Included(start), Bound::Included(end)) => Some(format!("bytes {start}-{end}/{l}")),
|
||||||
(Bound::Included(start), Bound::Excluded(end)) => {
|
(Bound::Included(start), Bound::Excluded(end)) => {
|
||||||
Ok(format!("bytes {start}-{}/{l}", end - 1))
|
Some(format!("bytes {start}-{}/{l}", end - 1))
|
||||||
}
|
}
|
||||||
(Bound::Included(start), Bound::Unbounded) => Ok(format!(
|
(Bound::Included(start), Bound::Unbounded) => Some(format!(
|
||||||
"bytes {start}-{}/{l}",
|
"bytes {start}-{}/{l}",
|
||||||
if l > 0 { l - 1 } else { 0 }
|
if l > 0 { l - 1 } else { 0 }
|
||||||
)),
|
)),
|
||||||
_ => Err(format!("bytes */{l}")),
|
_ => None,
|
||||||
}
|
}
|
||||||
|
.and_then(|s| HeaderValue::from_str(&s).ok())
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,24 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_stream::try_stream;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension, Router,
|
Extension, Router,
|
||||||
body::Body,
|
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
middleware::from_fn_with_state,
|
middleware::from_fn_with_state,
|
||||||
response::Response,
|
response::Response,
|
||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
use axum_extra::{TypedHeader, headers::Range};
|
use axum_extra::{TypedHeader, headers::Range};
|
||||||
use bytes::Bytes;
|
|
||||||
use futures::{Stream, StreamExt};
|
|
||||||
use http::{HeaderMap, HeaderValue, StatusCode, header};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::AppContextTrait,
|
app::AppContextTrait,
|
||||||
auth::{AuthError, AuthUserInfo, auth_middleware},
|
auth::{AuthError, AuthUserInfo, auth_middleware},
|
||||||
errors::{RecorderError, RecorderResult},
|
errors::RecorderResult,
|
||||||
utils::http::bound_range_to_content_range,
|
|
||||||
web::controller::Controller,
|
web::controller::Controller,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const CONTROLLER_PREFIX: &str = "/api/static";
|
pub const CONTROLLER_PREFIX: &str = "/api/static";
|
||||||
|
|
||||||
async fn serve_file_with_cache(
|
async fn serve_subscriber_static(
|
||||||
State(ctx): State<Arc<dyn AppContextTrait>>,
|
State(ctx): State<Arc<dyn AppContextTrait>>,
|
||||||
Path((subscriber_id, path)): Path<(i32, String)>,
|
Path((subscriber_id, path)): Path<(i32, String)>,
|
||||||
Extension(auth_user_info): Extension<AuthUserInfo>,
|
Extension(auth_user_info): Extension<AuthUserInfo>,
|
||||||
@ -40,106 +32,28 @@ async fn serve_file_with_cache(
|
|||||||
|
|
||||||
let storage_path = storage.build_subscriber_path(subscriber_id, &path);
|
let storage_path = storage.build_subscriber_path(subscriber_id, &path);
|
||||||
|
|
||||||
let metadata = storage
|
storage.serve_file(storage_path, range).await
|
||||||
.stat(&storage_path)
|
}
|
||||||
.await
|
|
||||||
.map_err(|_| RecorderError::from_status(StatusCode::NOT_FOUND))?;
|
|
||||||
|
|
||||||
if !metadata.is_file() {
|
async fn serve_public_static(
|
||||||
return Err(RecorderError::from_status(StatusCode::NOT_FOUND));
|
State(ctx): State<Arc<dyn AppContextTrait>>,
|
||||||
}
|
Path(path): Path<String>,
|
||||||
|
range: Option<TypedHeader<Range>>,
|
||||||
|
) -> RecorderResult<Response> {
|
||||||
|
let storage = ctx.storage();
|
||||||
|
|
||||||
let mime_type = mime_guess::from_path(&path).first_or_octet_stream();
|
let storage_path = storage.build_public_path(&path);
|
||||||
|
|
||||||
let response = if let Some(TypedHeader(range)) = range {
|
storage.serve_file(storage_path, range).await
|
||||||
let ranges = range
|
|
||||||
.satisfiable_ranges(metadata.content_length())
|
|
||||||
.collect_vec();
|
|
||||||
|
|
||||||
if ranges.is_empty() {
|
|
||||||
Response::builder()
|
|
||||||
.status(StatusCode::PARTIAL_CONTENT)
|
|
||||||
.header(header::CONTENT_TYPE, mime_type.as_ref())
|
|
||||||
.body(Body::empty())?
|
|
||||||
} else if ranges.len() == 1 {
|
|
||||||
let r = ranges[0];
|
|
||||||
let reader = storage.reader(&storage_path).await?;
|
|
||||||
let content_range = bound_range_to_content_range(&r, metadata.content_length())
|
|
||||||
.map_err(|s| {
|
|
||||||
RecorderError::from_status_and_headers(
|
|
||||||
StatusCode::RANGE_NOT_SATISFIABLE,
|
|
||||||
HeaderMap::from_iter(
|
|
||||||
[(header::CONTENT_RANGE, HeaderValue::from_str(&s).unwrap())]
|
|
||||||
.into_iter(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let stream = reader.into_bytes_stream(r).await?;
|
|
||||||
|
|
||||||
Response::builder()
|
|
||||||
.status(StatusCode::PARTIAL_CONTENT)
|
|
||||||
.header(header::CONTENT_TYPE, mime_type.as_ref())
|
|
||||||
.header(header::CONTENT_RANGE, content_range)
|
|
||||||
.body(Body::from_stream(stream))?
|
|
||||||
} else {
|
|
||||||
let boundary = Uuid::new_v4().to_string();
|
|
||||||
let reader = storage.reader(&storage_path).await?;
|
|
||||||
let stream: impl Stream<Item = Result<Bytes, RecorderError>> = {
|
|
||||||
let boundary = boundary.clone();
|
|
||||||
try_stream! {
|
|
||||||
for r in ranges {
|
|
||||||
let content_range = bound_range_to_content_range(&r, metadata.content_length())
|
|
||||||
.map_err(|s| {
|
|
||||||
RecorderError::from_status_and_headers(
|
|
||||||
StatusCode::RANGE_NOT_SATISFIABLE,
|
|
||||||
HeaderMap::from_iter([(header::CONTENT_RANGE, HeaderValue::from_str(&s).unwrap())].into_iter()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let part_header = format!("--{boundary}\r\nContent-Type: {}\r\nContent-Range: {}\r\n\r\n",
|
|
||||||
mime_type.as_ref(),
|
|
||||||
content_range,
|
|
||||||
);
|
|
||||||
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 {
|
|
||||||
let reader = storage.reader(&storage_path).await?;
|
|
||||||
let stream = reader.into_bytes_stream(..).await?;
|
|
||||||
|
|
||||||
Response::builder()
|
|
||||||
.status(StatusCode::OK)
|
|
||||||
.header(header::CONTENT_TYPE, mime_type.as_ref())
|
|
||||||
.body(Body::from_stream(stream))?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create(ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller> {
|
pub async fn create(ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller> {
|
||||||
let router = Router::<Arc<dyn AppContextTrait>>::new().route(
|
let router = Router::<Arc<dyn AppContextTrait>>::new()
|
||||||
"/subscribers/{subscriber_id}/*path",
|
.route(
|
||||||
get(serve_file_with_cache).layer(from_fn_with_state(ctx, auth_middleware)),
|
"/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))
|
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router))
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
|
|
||||||
import { Image } from '@/components/ui/image';
|
import { Img } from '@/components/ui/img';
|
||||||
|
|
||||||
export function AppIcon() {
|
export function AppIcon() {
|
||||||
return (
|
return (
|
||||||
@ -16,7 +16,7 @@ export function AppIcon() {
|
|||||||
>
|
>
|
||||||
<div className="flex size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
<div className="flex size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||||
<div className="relative size-8">
|
<div className="relative size-8">
|
||||||
<Image
|
<Img
|
||||||
src="/assets/favicon.png"
|
src="/assets/favicon.png"
|
||||||
alt="App Logo"
|
alt="App Logo"
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import type { ComponentProps } from 'react';
|
|
||||||
|
|
||||||
export type ImageProps = Omit<ComponentProps<'img'>, 'alt'> &
|
|
||||||
Required<Pick<ComponentProps<'img'>, 'alt'>>;
|
|
||||||
|
|
||||||
export const Image = (props: ImageProps) => {
|
|
||||||
// biome-ignore lint/nursery/noImgElement: <explanation>
|
|
||||||
return <img {...props} alt={props.alt} />;
|
|
||||||
};
|
|
9
apps/webui/src/components/ui/img.tsx
Normal file
9
apps/webui/src/components/ui/img.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
|
export type ImgProps = Omit<ComponentProps<"img">, "alt"> &
|
||||||
|
Required<Pick<ComponentProps<"img">, "alt">>;
|
||||||
|
|
||||||
|
export const Img = (props: ImgProps) => {
|
||||||
|
// biome-ignore lint/nursery/noImgElement: <explanation>
|
||||||
|
return <img {...props} alt={props.alt} />;
|
||||||
|
};
|
@ -106,7 +106,6 @@ query GetSubscriptionDetail ($id: Int!) {
|
|||||||
id
|
id
|
||||||
mikanBangumiId
|
mikanBangumiId
|
||||||
displayName
|
displayName
|
||||||
rawName
|
|
||||||
season
|
season
|
||||||
seasonRaw
|
seasonRaw
|
||||||
fansub
|
fansub
|
||||||
|
@ -24,7 +24,7 @@ type Documents = {
|
|||||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument,
|
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument,
|
||||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
|
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
|
||||||
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": typeof types.DeleteSubscriptionsDocument,
|
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": typeof types.DeleteSubscriptionsDocument,
|
||||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
||||||
"\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsIncrementalDocument,
|
"\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsIncrementalDocument,
|
||||||
"\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsFullDocument,
|
"\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsFullDocument,
|
||||||
"\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionSourcesDocument,
|
"\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionSourcesDocument,
|
||||||
@ -43,7 +43,7 @@ const documents: Documents = {
|
|||||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument,
|
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument,
|
||||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
|
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
|
||||||
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": types.DeleteSubscriptionsDocument,
|
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": types.DeleteSubscriptionsDocument,
|
||||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
||||||
"\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsIncrementalDocument,
|
"\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsIncrementalDocument,
|
||||||
"\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsFullDocument,
|
"\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsFullDocument,
|
||||||
"\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionSourcesDocument,
|
"\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionSourcesDocument,
|
||||||
@ -109,7 +109,7 @@ export function gql(source: "\n mutation DeleteSubscriptions($filters: Subscr
|
|||||||
/**
|
/**
|
||||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"];
|
export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"];
|
||||||
/**
|
/**
|
||||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
@ -30,8 +30,9 @@ export type Bangumi = {
|
|||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
||||||
mikanFansubId?: Maybe<Scalars['String']['output']>;
|
mikanFansubId?: Maybe<Scalars['String']['output']>;
|
||||||
|
originName: Scalars['String']['output'];
|
||||||
|
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||||
posterLink?: Maybe<Scalars['String']['output']>;
|
posterLink?: Maybe<Scalars['String']['output']>;
|
||||||
rawName: Scalars['String']['output'];
|
|
||||||
rssLink?: Maybe<Scalars['String']['output']>;
|
rssLink?: Maybe<Scalars['String']['output']>;
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
season: Scalars['Int']['output'];
|
season: Scalars['Int']['output'];
|
||||||
@ -74,8 +75,9 @@ export type BangumiBasic = {
|
|||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
||||||
mikanFansubId?: Maybe<Scalars['String']['output']>;
|
mikanFansubId?: Maybe<Scalars['String']['output']>;
|
||||||
|
originName: Scalars['String']['output'];
|
||||||
|
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||||
posterLink?: Maybe<Scalars['String']['output']>;
|
posterLink?: Maybe<Scalars['String']['output']>;
|
||||||
rawName: Scalars['String']['output'];
|
|
||||||
rssLink?: Maybe<Scalars['String']['output']>;
|
rssLink?: Maybe<Scalars['String']['output']>;
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
season: Scalars['Int']['output'];
|
season: Scalars['Int']['output'];
|
||||||
@ -108,8 +110,9 @@ export type BangumiFilterInput = {
|
|||||||
mikanBangumiId?: InputMaybe<StringFilterInput>;
|
mikanBangumiId?: InputMaybe<StringFilterInput>;
|
||||||
mikanFansubId?: InputMaybe<StringFilterInput>;
|
mikanFansubId?: InputMaybe<StringFilterInput>;
|
||||||
or?: InputMaybe<Array<BangumiFilterInput>>;
|
or?: InputMaybe<Array<BangumiFilterInput>>;
|
||||||
|
originName?: InputMaybe<StringFilterInput>;
|
||||||
|
originPosterLink?: InputMaybe<StringFilterInput>;
|
||||||
posterLink?: InputMaybe<StringFilterInput>;
|
posterLink?: InputMaybe<StringFilterInput>;
|
||||||
rawName?: InputMaybe<StringFilterInput>;
|
|
||||||
rssLink?: InputMaybe<StringFilterInput>;
|
rssLink?: InputMaybe<StringFilterInput>;
|
||||||
savePath?: InputMaybe<StringFilterInput>;
|
savePath?: InputMaybe<StringFilterInput>;
|
||||||
season?: InputMaybe<IntegerFilterInput>;
|
season?: InputMaybe<IntegerFilterInput>;
|
||||||
@ -127,8 +130,9 @@ export type BangumiInsertInput = {
|
|||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
||||||
mikanFansubId?: InputMaybe<Scalars['String']['input']>;
|
mikanFansubId?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originName: Scalars['String']['input'];
|
||||||
|
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
rawName: Scalars['String']['input'];
|
|
||||||
rssLink?: InputMaybe<Scalars['String']['input']>;
|
rssLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
season: Scalars['Int']['input'];
|
season: Scalars['Int']['input'];
|
||||||
@ -146,8 +150,9 @@ export type BangumiOrderInput = {
|
|||||||
id?: InputMaybe<OrderByEnum>;
|
id?: InputMaybe<OrderByEnum>;
|
||||||
mikanBangumiId?: InputMaybe<OrderByEnum>;
|
mikanBangumiId?: InputMaybe<OrderByEnum>;
|
||||||
mikanFansubId?: InputMaybe<OrderByEnum>;
|
mikanFansubId?: InputMaybe<OrderByEnum>;
|
||||||
|
originName?: InputMaybe<OrderByEnum>;
|
||||||
|
originPosterLink?: InputMaybe<OrderByEnum>;
|
||||||
posterLink?: InputMaybe<OrderByEnum>;
|
posterLink?: InputMaybe<OrderByEnum>;
|
||||||
rawName?: InputMaybe<OrderByEnum>;
|
|
||||||
rssLink?: InputMaybe<OrderByEnum>;
|
rssLink?: InputMaybe<OrderByEnum>;
|
||||||
savePath?: InputMaybe<OrderByEnum>;
|
savePath?: InputMaybe<OrderByEnum>;
|
||||||
season?: InputMaybe<OrderByEnum>;
|
season?: InputMaybe<OrderByEnum>;
|
||||||
@ -165,8 +170,9 @@ export type BangumiUpdateInput = {
|
|||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
||||||
mikanFansubId?: InputMaybe<Scalars['String']['input']>;
|
mikanFansubId?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originName?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
rawName?: InputMaybe<Scalars['String']['input']>;
|
|
||||||
rssLink?: InputMaybe<Scalars['String']['input']>;
|
rssLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
season?: InputMaybe<Scalars['Int']['input']>;
|
season?: InputMaybe<Scalars['Int']['input']>;
|
||||||
@ -492,7 +498,7 @@ export type Downloads = {
|
|||||||
homepage?: Maybe<Scalars['String']['output']>;
|
homepage?: Maybe<Scalars['String']['output']>;
|
||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mime: DownloadMimeEnum;
|
mime: DownloadMimeEnum;
|
||||||
rawName: Scalars['String']['output'];
|
originName: Scalars['String']['output'];
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
status: DownloadStatusEnum;
|
status: DownloadStatusEnum;
|
||||||
subscriber?: Maybe<Subscribers>;
|
subscriber?: Maybe<Subscribers>;
|
||||||
@ -512,7 +518,7 @@ export type DownloadsBasic = {
|
|||||||
homepage?: Maybe<Scalars['String']['output']>;
|
homepage?: Maybe<Scalars['String']['output']>;
|
||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mime: DownloadMimeEnum;
|
mime: DownloadMimeEnum;
|
||||||
rawName: Scalars['String']['output'];
|
originName: Scalars['String']['output'];
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
status: DownloadStatusEnum;
|
status: DownloadStatusEnum;
|
||||||
subscriberId: Scalars['Int']['output'];
|
subscriberId: Scalars['Int']['output'];
|
||||||
@ -546,7 +552,7 @@ export type DownloadsFilterInput = {
|
|||||||
id?: InputMaybe<IntegerFilterInput>;
|
id?: InputMaybe<IntegerFilterInput>;
|
||||||
mime?: InputMaybe<DownloadMimeEnumFilterInput>;
|
mime?: InputMaybe<DownloadMimeEnumFilterInput>;
|
||||||
or?: InputMaybe<Array<DownloadsFilterInput>>;
|
or?: InputMaybe<Array<DownloadsFilterInput>>;
|
||||||
rawName?: InputMaybe<StringFilterInput>;
|
originName?: InputMaybe<StringFilterInput>;
|
||||||
savePath?: InputMaybe<StringFilterInput>;
|
savePath?: InputMaybe<StringFilterInput>;
|
||||||
status?: InputMaybe<DownloadStatusEnumFilterInput>;
|
status?: InputMaybe<DownloadStatusEnumFilterInput>;
|
||||||
subscriberId?: InputMaybe<SubscriberIdFilterInput>;
|
subscriberId?: InputMaybe<SubscriberIdFilterInput>;
|
||||||
@ -564,7 +570,7 @@ export type DownloadsInsertInput = {
|
|||||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mime: DownloadMimeEnum;
|
mime: DownloadMimeEnum;
|
||||||
rawName: Scalars['String']['input'];
|
originName: Scalars['String']['input'];
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
status: DownloadStatusEnum;
|
status: DownloadStatusEnum;
|
||||||
subscriberId?: InputMaybe<Scalars['Int']['input']>;
|
subscriberId?: InputMaybe<Scalars['Int']['input']>;
|
||||||
@ -582,7 +588,7 @@ export type DownloadsOrderInput = {
|
|||||||
homepage?: InputMaybe<OrderByEnum>;
|
homepage?: InputMaybe<OrderByEnum>;
|
||||||
id?: InputMaybe<OrderByEnum>;
|
id?: InputMaybe<OrderByEnum>;
|
||||||
mime?: InputMaybe<OrderByEnum>;
|
mime?: InputMaybe<OrderByEnum>;
|
||||||
rawName?: InputMaybe<OrderByEnum>;
|
originName?: InputMaybe<OrderByEnum>;
|
||||||
savePath?: InputMaybe<OrderByEnum>;
|
savePath?: InputMaybe<OrderByEnum>;
|
||||||
status?: InputMaybe<OrderByEnum>;
|
status?: InputMaybe<OrderByEnum>;
|
||||||
subscriberId?: InputMaybe<OrderByEnum>;
|
subscriberId?: InputMaybe<OrderByEnum>;
|
||||||
@ -600,7 +606,7 @@ export type DownloadsUpdateInput = {
|
|||||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mime?: InputMaybe<DownloadMimeEnum>;
|
mime?: InputMaybe<DownloadMimeEnum>;
|
||||||
rawName?: InputMaybe<Scalars['String']['input']>;
|
originName?: InputMaybe<Scalars['String']['input']>;
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
status?: InputMaybe<DownloadStatusEnum>;
|
status?: InputMaybe<DownloadStatusEnum>;
|
||||||
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
||||||
@ -619,8 +625,9 @@ export type Episodes = {
|
|||||||
homepage?: Maybe<Scalars['String']['output']>;
|
homepage?: Maybe<Scalars['String']['output']>;
|
||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mikanEpisodeId?: Maybe<Scalars['String']['output']>;
|
mikanEpisodeId?: Maybe<Scalars['String']['output']>;
|
||||||
|
originName: Scalars['String']['output'];
|
||||||
|
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||||
posterLink?: Maybe<Scalars['String']['output']>;
|
posterLink?: Maybe<Scalars['String']['output']>;
|
||||||
rawName: Scalars['String']['output'];
|
|
||||||
resolution?: Maybe<Scalars['String']['output']>;
|
resolution?: Maybe<Scalars['String']['output']>;
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
season: Scalars['Int']['output'];
|
season: Scalars['Int']['output'];
|
||||||
@ -665,8 +672,9 @@ export type EpisodesBasic = {
|
|||||||
homepage?: Maybe<Scalars['String']['output']>;
|
homepage?: Maybe<Scalars['String']['output']>;
|
||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
mikanEpisodeId?: Maybe<Scalars['String']['output']>;
|
mikanEpisodeId?: Maybe<Scalars['String']['output']>;
|
||||||
|
originName: Scalars['String']['output'];
|
||||||
|
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||||
posterLink?: Maybe<Scalars['String']['output']>;
|
posterLink?: Maybe<Scalars['String']['output']>;
|
||||||
rawName: Scalars['String']['output'];
|
|
||||||
resolution?: Maybe<Scalars['String']['output']>;
|
resolution?: Maybe<Scalars['String']['output']>;
|
||||||
savePath?: Maybe<Scalars['String']['output']>;
|
savePath?: Maybe<Scalars['String']['output']>;
|
||||||
season: Scalars['Int']['output'];
|
season: Scalars['Int']['output'];
|
||||||
@ -702,8 +710,9 @@ export type EpisodesFilterInput = {
|
|||||||
id?: InputMaybe<IntegerFilterInput>;
|
id?: InputMaybe<IntegerFilterInput>;
|
||||||
mikanEpisodeId?: InputMaybe<StringFilterInput>;
|
mikanEpisodeId?: InputMaybe<StringFilterInput>;
|
||||||
or?: InputMaybe<Array<EpisodesFilterInput>>;
|
or?: InputMaybe<Array<EpisodesFilterInput>>;
|
||||||
|
originName?: InputMaybe<StringFilterInput>;
|
||||||
|
originPosterLink?: InputMaybe<StringFilterInput>;
|
||||||
posterLink?: InputMaybe<StringFilterInput>;
|
posterLink?: InputMaybe<StringFilterInput>;
|
||||||
rawName?: InputMaybe<StringFilterInput>;
|
|
||||||
resolution?: InputMaybe<StringFilterInput>;
|
resolution?: InputMaybe<StringFilterInput>;
|
||||||
savePath?: InputMaybe<StringFilterInput>;
|
savePath?: InputMaybe<StringFilterInput>;
|
||||||
season?: InputMaybe<IntegerFilterInput>;
|
season?: InputMaybe<IntegerFilterInput>;
|
||||||
@ -723,8 +732,9 @@ export type EpisodesInsertInput = {
|
|||||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mikanEpisodeId?: InputMaybe<Scalars['String']['input']>;
|
mikanEpisodeId?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originName: Scalars['String']['input'];
|
||||||
|
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
rawName: Scalars['String']['input'];
|
|
||||||
resolution?: InputMaybe<Scalars['String']['input']>;
|
resolution?: InputMaybe<Scalars['String']['input']>;
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
season: Scalars['Int']['input'];
|
season: Scalars['Int']['input'];
|
||||||
@ -744,8 +754,9 @@ export type EpisodesOrderInput = {
|
|||||||
homepage?: InputMaybe<OrderByEnum>;
|
homepage?: InputMaybe<OrderByEnum>;
|
||||||
id?: InputMaybe<OrderByEnum>;
|
id?: InputMaybe<OrderByEnum>;
|
||||||
mikanEpisodeId?: InputMaybe<OrderByEnum>;
|
mikanEpisodeId?: InputMaybe<OrderByEnum>;
|
||||||
|
originName?: InputMaybe<OrderByEnum>;
|
||||||
|
originPosterLink?: InputMaybe<OrderByEnum>;
|
||||||
posterLink?: InputMaybe<OrderByEnum>;
|
posterLink?: InputMaybe<OrderByEnum>;
|
||||||
rawName?: InputMaybe<OrderByEnum>;
|
|
||||||
resolution?: InputMaybe<OrderByEnum>;
|
resolution?: InputMaybe<OrderByEnum>;
|
||||||
savePath?: InputMaybe<OrderByEnum>;
|
savePath?: InputMaybe<OrderByEnum>;
|
||||||
season?: InputMaybe<OrderByEnum>;
|
season?: InputMaybe<OrderByEnum>;
|
||||||
@ -765,8 +776,9 @@ export type EpisodesUpdateInput = {
|
|||||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
mikanEpisodeId?: InputMaybe<Scalars['String']['input']>;
|
mikanEpisodeId?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originName?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||||
rawName?: InputMaybe<Scalars['String']['input']>;
|
|
||||||
resolution?: InputMaybe<Scalars['String']['input']>;
|
resolution?: InputMaybe<Scalars['String']['input']>;
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
season?: InputMaybe<Scalars['Int']['input']>;
|
season?: InputMaybe<Scalars['Int']['input']>;
|
||||||
@ -1708,7 +1720,7 @@ export type GetSubscriptionDetailQueryVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, rawName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, savePath?: string | null, homepage?: string | null }> } }> } };
|
export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, savePath?: string | null, homepage?: string | null }> } }> } };
|
||||||
|
|
||||||
export type SyncSubscriptionFeedsIncrementalMutationVariables = Exact<{
|
export type SyncSubscriptionFeedsIncrementalMutationVariables = Exact<{
|
||||||
filter: SubscriptionsFilterInput;
|
filter: SubscriptionsFilterInput;
|
||||||
@ -1765,7 +1777,7 @@ export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind
|
|||||||
export const InsertSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credentialId"}}]}}]}}]} as unknown as DocumentNode<InsertSubscriptionMutation, InsertSubscriptionMutationVariables>;
|
export const InsertSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credentialId"}}]}}]}}]} as unknown as DocumentNode<InsertSubscriptionMutation, InsertSubscriptionMutationVariables>;
|
||||||
export const UpdateSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}}]}}]} as unknown as DocumentNode<UpdateSubscriptionsMutation, UpdateSubscriptionsMutationVariables>;
|
export const UpdateSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}}]}}]} as unknown as DocumentNode<UpdateSubscriptionsMutation, UpdateSubscriptionsMutationVariables>;
|
||||||
export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode<DeleteSubscriptionsMutation, DeleteSubscriptionsMutationVariables>;
|
export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode<DeleteSubscriptionsMutation, DeleteSubscriptionsMutationVariables>;
|
||||||
export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"rawName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"savePath"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetSubscriptionDetailQuery, GetSubscriptionDetailQueryVariables>;
|
export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"savePath"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetSubscriptionDetailQuery, GetSubscriptionDetailQueryVariables>;
|
||||||
export const SyncSubscriptionFeedsIncrementalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsIncremental"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsIncremental"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionFeedsIncrementalMutation, SyncSubscriptionFeedsIncrementalMutationVariables>;
|
export const SyncSubscriptionFeedsIncrementalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsIncremental"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsIncremental"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionFeedsIncrementalMutation, SyncSubscriptionFeedsIncrementalMutationVariables>;
|
||||||
export const SyncSubscriptionFeedsFullDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsFull"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsFull"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionFeedsFullMutation, SyncSubscriptionFeedsFullMutationVariables>;
|
export const SyncSubscriptionFeedsFullDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsFull"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsFull"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionFeedsFullMutation, SyncSubscriptionFeedsFullMutationVariables>;
|
||||||
export const SyncSubscriptionSourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionSources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneSources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionSourcesMutation, SyncSubscriptionSourcesMutationVariables>;
|
export const SyncSubscriptionSourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionSources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneSources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionSourcesMutation, SyncSubscriptionSourcesMutationVariables>;
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import { Img } from '@/components/ui/img';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
@ -324,7 +325,18 @@ function SubscriptionDetailRouteComponent() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{subscription.bangumi.nodes.map((bangumi) => (
|
{subscription.bangumi.nodes.map((bangumi) => (
|
||||||
<Card key={bangumi.id} className="p-4">
|
<Card key={bangumi.id} className="p-4">
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
|
||||||
|
<div className="col-span-1 row-span-2 space-y-2">
|
||||||
|
<div className="flex h-full items-center justify-center overflow-hidden rounded-md bg-muted">
|
||||||
|
{bangumi.posterLink && (
|
||||||
|
<Img
|
||||||
|
src={`/api/static${bangumi.posterLink}`}
|
||||||
|
alt="Poster"
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium text-muted-foreground text-xs">
|
<Label className="font-medium text-muted-foreground text-xs">
|
||||||
Display Name
|
Display Name
|
||||||
@ -333,14 +345,6 @@ function SubscriptionDetailRouteComponent() {
|
|||||||
{bangumi.displayName}
|
{bangumi.displayName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="font-medium text-muted-foreground text-xs">
|
|
||||||
Season
|
|
||||||
</Label>
|
|
||||||
<div className="text-sm">
|
|
||||||
{bangumi.season || '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium text-muted-foreground text-xs">
|
<Label className="font-medium text-muted-foreground text-xs">
|
||||||
Fansub
|
Fansub
|
||||||
@ -351,10 +355,21 @@ function SubscriptionDetailRouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium text-muted-foreground text-xs">
|
<Label className="font-medium text-muted-foreground text-xs">
|
||||||
Save Path
|
Season
|
||||||
|
</Label>
|
||||||
|
<div className="text-sm">
|
||||||
|
{bangumi.season || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-muted-foreground text-xs">
|
||||||
|
Updated At
|
||||||
</Label>
|
</Label>
|
||||||
<div className="font-mono text-sm">
|
<div className="font-mono text-sm">
|
||||||
{bangumi.savePath || '-'}
|
{format(
|
||||||
|
new Date(bangumi.updatedAt),
|
||||||
|
'yyyy-MM-dd'
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,6 +21,10 @@ impl OptDynErr {
|
|||||||
pub fn none() -> Self {
|
pub fn none() -> Self {
|
||||||
Self(None)
|
Self(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for OptDynErr {
|
impl Display for OptDynErr {
|
||||||
|
Loading…
Reference in New Issue
Block a user