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 crate::{
|
||||
app::Environment,
|
||||
errors::{RecorderError, RecorderResult},
|
||||
web::{
|
||||
controller::{self, core::ControllerTrait},
|
||||
@ -52,13 +51,14 @@ impl App {
|
||||
|
||||
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::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);
|
||||
}
|
||||
|
||||
@ -86,17 +86,13 @@ impl App {
|
||||
async {
|
||||
{
|
||||
let monitor = task.setup_monitor().await?;
|
||||
if matches!(context.environment(), Environment::Development) {
|
||||
monitor.run().await?;
|
||||
} else {
|
||||
monitor
|
||||
.run_with_signal(async move {
|
||||
Self::shutdown_signal().await;
|
||||
tracing::info!("apalis shutting down...");
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
monitor
|
||||
.run_with_signal(async move {
|
||||
Self::shutdown_signal().await;
|
||||
tracing::info!("apalis shutting down...");
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok::<(), RecorderError>(())
|
||||
|
@ -40,12 +40,10 @@ pub enum RecorderError {
|
||||
NetAddrParseError { source: std::net::AddrParseError },
|
||||
#[snafu(transparent)]
|
||||
RegexError { source: regex::Error },
|
||||
#[snafu(transparent)]
|
||||
InvalidMethodError { source: http::method::InvalidMethod },
|
||||
#[snafu(transparent)]
|
||||
InvalidHeaderNameError {
|
||||
source: http::header::InvalidHeaderName,
|
||||
},
|
||||
#[snafu(display("Invalid method"))]
|
||||
InvalidMethodError,
|
||||
#[snafu(display("Invalid header name"))]
|
||||
InvalidHeaderNameError,
|
||||
#[snafu(transparent)]
|
||||
TracingAppenderInitError {
|
||||
source: tracing_appender::rolling::InitError,
|
||||
@ -84,10 +82,8 @@ pub enum RecorderError {
|
||||
#[snafu(source(from(opendal::Error, Box::new)))]
|
||||
source: Box<opendal::Error>,
|
||||
},
|
||||
#[snafu(transparent)]
|
||||
InvalidHeaderValueError {
|
||||
source: http::header::InvalidHeaderValue,
|
||||
},
|
||||
#[snafu(display("Invalid header value"))]
|
||||
InvalidHeaderValueError,
|
||||
#[snafu(transparent)]
|
||||
HttpClientError { source: HttpClientError },
|
||||
#[cfg(feature = "testcontainers")]
|
||||
@ -234,7 +230,8 @@ impl IntoResponse for RecorderError {
|
||||
headers,
|
||||
source,
|
||||
} => {
|
||||
let message = Option::<_>::from(source)
|
||||
let message = source
|
||||
.into_inner()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
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>;
|
||||
|
@ -268,8 +268,8 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn test_torrent_ep_parser(raw_name: &str, expected: &str) {
|
||||
let extname = Path::new(raw_name)
|
||||
pub fn test_torrent_ep_parser(origin_name: &str, expected: &str) {
|
||||
let extname = Path::new(origin_name)
|
||||
.extension()
|
||||
.map(|e| format!(".{e}"))
|
||||
.unwrap_or_default()
|
||||
@ -278,7 +278,7 @@ mod tests {
|
||||
if extname == ".srt" || extname == ".ass" {
|
||||
let expected: Option<TorrentEpisodeSubtitleMeta> = serde_json::from_str(expected).ok();
|
||||
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();
|
||||
|
||||
if expected != found {
|
||||
@ -299,7 +299,8 @@ mod tests {
|
||||
assert_eq!(expected, found);
|
||||
} else {
|
||||
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();
|
||||
|
||||
if expected != found {
|
||||
|
@ -741,13 +741,11 @@ pub async fn scrape_mikan_poster_meta_from_image_url(
|
||||
mikan_client: &MikanClient,
|
||||
storage_service: &StorageService,
|
||||
origin_poster_src_url: Url,
|
||||
subscriber_id: i32,
|
||||
) -> RecorderResult<MikanBangumiPosterMeta> {
|
||||
if let Some(poster_src) = storage_service
|
||||
.exists(
|
||||
storage_service.build_subscriber_object_path(
|
||||
storage_service.build_public_object_path(
|
||||
StorageContentCategory::Image,
|
||||
subscriber_id,
|
||||
MIKAN_POSTER_BUCKET_KEY,
|
||||
&origin_poster_src_url
|
||||
.path()
|
||||
@ -768,9 +766,8 @@ pub async fn scrape_mikan_poster_meta_from_image_url(
|
||||
|
||||
let poster_str = storage_service
|
||||
.write(
|
||||
storage_service.build_subscriber_object_path(
|
||||
storage_service.build_public_object_path(
|
||||
StorageContentCategory::Image,
|
||||
subscriber_id,
|
||||
MIKAN_POSTER_BUCKET_KEY,
|
||||
&origin_poster_src_url
|
||||
.path()
|
||||
@ -1084,15 +1081,13 @@ mod test {
|
||||
&mikan_client,
|
||||
&storage_service,
|
||||
bangumi_poster_url,
|
||||
1,
|
||||
)
|
||||
.await?;
|
||||
|
||||
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,
|
||||
1,
|
||||
MIKAN_POSTER_BUCKET_KEY,
|
||||
"202309/5ce9fed1.jpg",
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
pub mod bittorrent;
|
||||
pub mod defs;
|
||||
pub mod html;
|
||||
pub mod http;
|
||||
pub mod media;
|
||||
pub mod mikan;
|
||||
pub mod rawname;
|
||||
pub mod bittorrent;
|
||||
pub mod origin;
|
||||
|
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)
|
||||
}
|
||||
|
||||
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_without_ch_brackets = replace_ch_bracket_to_en(raw_title);
|
||||
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)]
|
||||
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) {
|
||||
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 {
|
||||
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,
|
||||
DisplayName,
|
||||
SubscriberId,
|
||||
RawName,
|
||||
OriginName,
|
||||
Season,
|
||||
SeasonRaw,
|
||||
Fansub,
|
||||
@ -51,6 +51,7 @@ pub enum Bangumi {
|
||||
Filter,
|
||||
RssLink,
|
||||
PosterLink,
|
||||
OriginPosterLink,
|
||||
SavePath,
|
||||
Homepage,
|
||||
}
|
||||
@ -69,7 +70,7 @@ pub enum Episodes {
|
||||
Table,
|
||||
Id,
|
||||
MikanEpisodeId,
|
||||
RawName,
|
||||
OriginName,
|
||||
DisplayName,
|
||||
BangumiId,
|
||||
SubscriberId,
|
||||
@ -80,6 +81,7 @@ pub enum Episodes {
|
||||
SeasonRaw,
|
||||
Fansub,
|
||||
PosterLink,
|
||||
OriginPosterLink,
|
||||
EpisodeIndex,
|
||||
Homepage,
|
||||
Subtitle,
|
||||
@ -100,7 +102,7 @@ pub enum SubscriptionEpisode {
|
||||
pub enum Downloads {
|
||||
Table,
|
||||
Id,
|
||||
RawName,
|
||||
OriginName,
|
||||
DisplayName,
|
||||
SubscriberId,
|
||||
DownloaderId,
|
||||
|
@ -96,7 +96,7 @@ impl MigrationTrait for Migration {
|
||||
.col(text_null(Bangumi::MikanBangumiId))
|
||||
.col(integer(Bangumi::SubscriberId))
|
||||
.col(text(Bangumi::DisplayName))
|
||||
.col(text(Bangumi::RawName))
|
||||
.col(text(Bangumi::OriginName))
|
||||
.col(integer(Bangumi::Season))
|
||||
.col(text_null(Bangumi::SeasonRaw))
|
||||
.col(text_null(Bangumi::Fansub))
|
||||
@ -104,6 +104,7 @@ impl MigrationTrait for Migration {
|
||||
.col(json_binary_null(Bangumi::Filter))
|
||||
.col(text_null(Bangumi::RssLink))
|
||||
.col(text_null(Bangumi::PosterLink))
|
||||
.col(text_null(Bangumi::OriginPosterLink))
|
||||
.col(text_null(Bangumi::SavePath))
|
||||
.col(text_null(Bangumi::Homepage))
|
||||
.foreign_key(
|
||||
@ -220,7 +221,7 @@ impl MigrationTrait for Migration {
|
||||
table_auto_z(Episodes::Table)
|
||||
.col(pk_auto(Episodes::Id))
|
||||
.col(text_null(Episodes::MikanEpisodeId))
|
||||
.col(text(Episodes::RawName))
|
||||
.col(text(Episodes::OriginName))
|
||||
.col(text(Episodes::DisplayName))
|
||||
.col(integer(Episodes::BangumiId))
|
||||
.col(integer(Episodes::SubscriberId))
|
||||
@ -230,6 +231,7 @@ impl MigrationTrait for Migration {
|
||||
.col(text_null(Episodes::SeasonRaw))
|
||||
.col(text_null(Episodes::Fansub))
|
||||
.col(text_null(Episodes::PosterLink))
|
||||
.col(text_null(Episodes::OriginPosterLink))
|
||||
.col(integer(Episodes::EpisodeIndex))
|
||||
.col(text_null(Episodes::Homepage))
|
||||
.col(text_null(Episodes::Subtitle))
|
||||
|
@ -80,7 +80,7 @@ impl MigrationTrait for Migration {
|
||||
.create_table(
|
||||
table_auto_z(Downloads::Table)
|
||||
.col(pk_auto(Downloads::Id))
|
||||
.col(string(Downloads::RawName))
|
||||
.col(string(Downloads::OriginName))
|
||||
.col(string(Downloads::DisplayName))
|
||||
.col(integer(Downloads::SubscriberId))
|
||||
.col(integer(Downloads::DownloaderId))
|
||||
|
@ -17,7 +17,7 @@ use crate::{
|
||||
MikanBangumiHash, MikanBangumiMeta, build_mikan_bangumi_subscription_rss_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 subscriber_id: i32,
|
||||
pub display_name: String,
|
||||
pub raw_name: String,
|
||||
pub origin_name: String,
|
||||
pub season: i32,
|
||||
pub season_raw: Option<String>,
|
||||
pub fansub: Option<String>,
|
||||
@ -49,6 +49,7 @@ pub struct Model {
|
||||
pub filter: Option<BangumiFilter>,
|
||||
pub rss_link: Option<String>,
|
||||
pub poster_link: Option<String>,
|
||||
pub origin_poster_link: Option<String>,
|
||||
pub save_path: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
}
|
||||
@ -130,12 +131,11 @@ impl ActiveModel {
|
||||
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(
|
||||
mikan_client,
|
||||
storage_service,
|
||||
origin_poster_src,
|
||||
subscriber_id,
|
||||
)
|
||||
.await?;
|
||||
poster_meta.poster_src
|
||||
@ -148,11 +148,12 @@ impl ActiveModel {
|
||||
mikan_fansub_id: ActiveValue::Set(Some(meta.mikan_fansub_id)),
|
||||
subscriber_id: ActiveValue::Set(subscriber_id),
|
||||
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_raw: ActiveValue::Set(season_raw),
|
||||
fansub: ActiveValue::Set(Some(meta.fansub)),
|
||||
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())),
|
||||
rss_link: ActiveValue::Set(Some(rss_url.to_string())),
|
||||
..Default::default()
|
||||
@ -228,7 +229,7 @@ impl Model {
|
||||
Column::SubscriberId,
|
||||
])
|
||||
.update_columns([
|
||||
Column::RawName,
|
||||
Column::OriginName,
|
||||
Column::Fansub,
|
||||
Column::PosterLink,
|
||||
Column::Season,
|
||||
|
@ -44,7 +44,7 @@ pub struct Model {
|
||||
pub updated_at: DateTimeUtc,
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub raw_name: String,
|
||||
pub origin_name: String,
|
||||
pub display_name: String,
|
||||
pub downloader_id: i32,
|
||||
pub episode_id: i32,
|
||||
|
@ -10,7 +10,7 @@ use crate::{
|
||||
errors::RecorderResult,
|
||||
extract::{
|
||||
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,
|
||||
#[sea_orm(indexed)]
|
||||
pub mikan_episode_id: Option<String>,
|
||||
pub raw_name: String,
|
||||
pub origin_name: String,
|
||||
pub display_name: String,
|
||||
pub bangumi_id: i32,
|
||||
pub subscriber_id: i32,
|
||||
@ -35,6 +35,7 @@ pub struct Model {
|
||||
pub season_raw: Option<String>,
|
||||
pub fansub: Option<String>,
|
||||
pub poster_link: Option<String>,
|
||||
pub origin_poster_link: Option<String>,
|
||||
pub episode_index: i32,
|
||||
pub homepage: Option<String>,
|
||||
pub subtitle: Option<String>,
|
||||
@ -123,7 +124,7 @@ impl ActiveModel {
|
||||
episode: MikanEpisodeMeta,
|
||||
) -> RecorderResult<Self> {
|
||||
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| {
|
||||
tracing::error!(
|
||||
err = ?err,
|
||||
@ -136,7 +137,7 @@ impl ActiveModel {
|
||||
|
||||
let mut episode_active_model = Self {
|
||||
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()),
|
||||
bangumi_id: ActiveValue::Set(bangumi.id),
|
||||
subscriber_id: ActiveValue::Set(bangumi.subscriber_id),
|
||||
@ -145,6 +146,7 @@ impl ActiveModel {
|
||||
season: ActiveValue::Set(bangumi.season),
|
||||
fansub: ActiveValue::Set(bangumi.fansub.clone()),
|
||||
poster_link: ActiveValue::Set(bangumi.poster_link.clone()),
|
||||
origin_poster_link: ActiveValue::Set(bangumi.origin_poster_link.clone()),
|
||||
episode_index: ActiveValue::Set(0),
|
||||
..Default::default()
|
||||
};
|
||||
@ -231,7 +233,7 @@ impl Model {
|
||||
let new_episode_ids = Entity::insert_many(new_episode_active_modes)
|
||||
.on_conflict(
|
||||
OnConflict::columns([Column::MikanEpisodeId, Column::SubscriberId])
|
||||
.update_columns([Column::RawName, Column::PosterLink, Column::Homepage])
|
||||
.update_columns([Column::OriginName, Column::PosterLink, Column::Homepage])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec_with_returning_columns(db, [Column::Id])
|
||||
|
@ -1,13 +1,23 @@
|
||||
use std::fmt;
|
||||
|
||||
use async_stream::try_stream;
|
||||
use axum::{body::Body, response::Response};
|
||||
use axum_extra::{TypedHeader, headers::Range};
|
||||
use bytes::Bytes;
|
||||
use futures::{Stream, StreamExt};
|
||||
use http::{HeaderValue, StatusCode, header};
|
||||
use opendal::{Buffer, Metadata, Operator, Reader, Writer, layers::LoggingLayer};
|
||||
use quirks_path::PathBuf;
|
||||
use quirks_path::{Path, PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
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)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@ -69,23 +79,44 @@ impl StorageService {
|
||||
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");
|
||||
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,
|
||||
content_category: StorageContentCategory,
|
||||
subscriber_id: i32,
|
||||
content_category: StorageContentCategory,
|
||||
bucket: &str,
|
||||
object_name: &str,
|
||||
) -> PathBuf {
|
||||
self.build_subscriber_path(
|
||||
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)
|
||||
}
|
||||
|
||||
#[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;
|
||||
|
||||
pub fn bound_range_to_content_range(
|
||||
r: &(Bound<u64>, Bound<u64>),
|
||||
l: u64,
|
||||
) -> Result<String, String> {
|
||||
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)) => Ok(format!("bytes {start}-{end}/{l}")),
|
||||
(Bound::Included(start), Bound::Included(end)) => Some(format!("bytes {start}-{end}/{l}")),
|
||||
(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}",
|
||||
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 async_stream::try_stream;
|
||||
use axum::{
|
||||
Extension, Router,
|
||||
body::Body,
|
||||
extract::{Path, State},
|
||||
middleware::from_fn_with_state,
|
||||
response::Response,
|
||||
routing::get,
|
||||
};
|
||||
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::{
|
||||
app::AppContextTrait,
|
||||
auth::{AuthError, AuthUserInfo, auth_middleware},
|
||||
errors::{RecorderError, RecorderResult},
|
||||
utils::http::bound_range_to_content_range,
|
||||
errors::RecorderResult,
|
||||
web::controller::Controller,
|
||||
};
|
||||
|
||||
pub const CONTROLLER_PREFIX: &str = "/api/static";
|
||||
|
||||
async fn serve_file_with_cache(
|
||||
async fn serve_subscriber_static(
|
||||
State(ctx): State<Arc<dyn AppContextTrait>>,
|
||||
Path((subscriber_id, path)): Path<(i32, String)>,
|
||||
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 metadata = storage
|
||||
.stat(&storage_path)
|
||||
.await
|
||||
.map_err(|_| RecorderError::from_status(StatusCode::NOT_FOUND))?;
|
||||
storage.serve_file(storage_path, range).await
|
||||
}
|
||||
|
||||
if !metadata.is_file() {
|
||||
return Err(RecorderError::from_status(StatusCode::NOT_FOUND));
|
||||
}
|
||||
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 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 {
|
||||
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)
|
||||
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_file_with_cache).layer(from_fn_with_state(ctx, auth_middleware)),
|
||||
);
|
||||
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,
|
||||
} from '@/components/ui/sidebar';
|
||||
|
||||
import { Image } from '@/components/ui/image';
|
||||
import { Img } from '@/components/ui/img';
|
||||
|
||||
export function AppIcon() {
|
||||
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="relative size-8">
|
||||
<Image
|
||||
<Img
|
||||
src="/assets/favicon.png"
|
||||
alt="App Logo"
|
||||
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
|
||||
mikanBangumiId
|
||||
displayName
|
||||
rawName
|
||||
season
|
||||
seasonRaw
|
||||
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 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,
|
||||
"\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 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,
|
||||
@ -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 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,
|
||||
"\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 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,
|
||||
@ -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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -30,8 +30,9 @@ export type Bangumi = {
|
||||
id: Scalars['Int']['output'];
|
||||
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
||||
mikanFansubId?: Maybe<Scalars['String']['output']>;
|
||||
originName: Scalars['String']['output'];
|
||||
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||
posterLink?: Maybe<Scalars['String']['output']>;
|
||||
rawName: Scalars['String']['output'];
|
||||
rssLink?: Maybe<Scalars['String']['output']>;
|
||||
savePath?: Maybe<Scalars['String']['output']>;
|
||||
season: Scalars['Int']['output'];
|
||||
@ -74,8 +75,9 @@ export type BangumiBasic = {
|
||||
id: Scalars['Int']['output'];
|
||||
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
||||
mikanFansubId?: Maybe<Scalars['String']['output']>;
|
||||
originName: Scalars['String']['output'];
|
||||
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||
posterLink?: Maybe<Scalars['String']['output']>;
|
||||
rawName: Scalars['String']['output'];
|
||||
rssLink?: Maybe<Scalars['String']['output']>;
|
||||
savePath?: Maybe<Scalars['String']['output']>;
|
||||
season: Scalars['Int']['output'];
|
||||
@ -108,8 +110,9 @@ export type BangumiFilterInput = {
|
||||
mikanBangumiId?: InputMaybe<StringFilterInput>;
|
||||
mikanFansubId?: InputMaybe<StringFilterInput>;
|
||||
or?: InputMaybe<Array<BangumiFilterInput>>;
|
||||
originName?: InputMaybe<StringFilterInput>;
|
||||
originPosterLink?: InputMaybe<StringFilterInput>;
|
||||
posterLink?: InputMaybe<StringFilterInput>;
|
||||
rawName?: InputMaybe<StringFilterInput>;
|
||||
rssLink?: InputMaybe<StringFilterInput>;
|
||||
savePath?: InputMaybe<StringFilterInput>;
|
||||
season?: InputMaybe<IntegerFilterInput>;
|
||||
@ -127,8 +130,9 @@ export type BangumiInsertInput = {
|
||||
id?: InputMaybe<Scalars['Int']['input']>;
|
||||
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
||||
mikanFansubId?: InputMaybe<Scalars['String']['input']>;
|
||||
originName: Scalars['String']['input'];
|
||||
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||
rawName: Scalars['String']['input'];
|
||||
rssLink?: InputMaybe<Scalars['String']['input']>;
|
||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||
season: Scalars['Int']['input'];
|
||||
@ -146,8 +150,9 @@ export type BangumiOrderInput = {
|
||||
id?: InputMaybe<OrderByEnum>;
|
||||
mikanBangumiId?: InputMaybe<OrderByEnum>;
|
||||
mikanFansubId?: InputMaybe<OrderByEnum>;
|
||||
originName?: InputMaybe<OrderByEnum>;
|
||||
originPosterLink?: InputMaybe<OrderByEnum>;
|
||||
posterLink?: InputMaybe<OrderByEnum>;
|
||||
rawName?: InputMaybe<OrderByEnum>;
|
||||
rssLink?: InputMaybe<OrderByEnum>;
|
||||
savePath?: InputMaybe<OrderByEnum>;
|
||||
season?: InputMaybe<OrderByEnum>;
|
||||
@ -165,8 +170,9 @@ export type BangumiUpdateInput = {
|
||||
id?: InputMaybe<Scalars['Int']['input']>;
|
||||
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
||||
mikanFansubId?: InputMaybe<Scalars['String']['input']>;
|
||||
originName?: InputMaybe<Scalars['String']['input']>;
|
||||
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||
rawName?: InputMaybe<Scalars['String']['input']>;
|
||||
rssLink?: InputMaybe<Scalars['String']['input']>;
|
||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||
season?: InputMaybe<Scalars['Int']['input']>;
|
||||
@ -492,7 +498,7 @@ export type Downloads = {
|
||||
homepage?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['Int']['output'];
|
||||
mime: DownloadMimeEnum;
|
||||
rawName: Scalars['String']['output'];
|
||||
originName: Scalars['String']['output'];
|
||||
savePath?: Maybe<Scalars['String']['output']>;
|
||||
status: DownloadStatusEnum;
|
||||
subscriber?: Maybe<Subscribers>;
|
||||
@ -512,7 +518,7 @@ export type DownloadsBasic = {
|
||||
homepage?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['Int']['output'];
|
||||
mime: DownloadMimeEnum;
|
||||
rawName: Scalars['String']['output'];
|
||||
originName: Scalars['String']['output'];
|
||||
savePath?: Maybe<Scalars['String']['output']>;
|
||||
status: DownloadStatusEnum;
|
||||
subscriberId: Scalars['Int']['output'];
|
||||
@ -546,7 +552,7 @@ export type DownloadsFilterInput = {
|
||||
id?: InputMaybe<IntegerFilterInput>;
|
||||
mime?: InputMaybe<DownloadMimeEnumFilterInput>;
|
||||
or?: InputMaybe<Array<DownloadsFilterInput>>;
|
||||
rawName?: InputMaybe<StringFilterInput>;
|
||||
originName?: InputMaybe<StringFilterInput>;
|
||||
savePath?: InputMaybe<StringFilterInput>;
|
||||
status?: InputMaybe<DownloadStatusEnumFilterInput>;
|
||||
subscriberId?: InputMaybe<SubscriberIdFilterInput>;
|
||||
@ -564,7 +570,7 @@ export type DownloadsInsertInput = {
|
||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||
id?: InputMaybe<Scalars['Int']['input']>;
|
||||
mime: DownloadMimeEnum;
|
||||
rawName: Scalars['String']['input'];
|
||||
originName: Scalars['String']['input'];
|
||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||
status: DownloadStatusEnum;
|
||||
subscriberId?: InputMaybe<Scalars['Int']['input']>;
|
||||
@ -582,7 +588,7 @@ export type DownloadsOrderInput = {
|
||||
homepage?: InputMaybe<OrderByEnum>;
|
||||
id?: InputMaybe<OrderByEnum>;
|
||||
mime?: InputMaybe<OrderByEnum>;
|
||||
rawName?: InputMaybe<OrderByEnum>;
|
||||
originName?: InputMaybe<OrderByEnum>;
|
||||
savePath?: InputMaybe<OrderByEnum>;
|
||||
status?: InputMaybe<OrderByEnum>;
|
||||
subscriberId?: InputMaybe<OrderByEnum>;
|
||||
@ -600,7 +606,7 @@ export type DownloadsUpdateInput = {
|
||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||
id?: InputMaybe<Scalars['Int']['input']>;
|
||||
mime?: InputMaybe<DownloadMimeEnum>;
|
||||
rawName?: InputMaybe<Scalars['String']['input']>;
|
||||
originName?: InputMaybe<Scalars['String']['input']>;
|
||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||
status?: InputMaybe<DownloadStatusEnum>;
|
||||
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
||||
@ -619,8 +625,9 @@ export type Episodes = {
|
||||
homepage?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['Int']['output'];
|
||||
mikanEpisodeId?: Maybe<Scalars['String']['output']>;
|
||||
originName: Scalars['String']['output'];
|
||||
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||
posterLink?: Maybe<Scalars['String']['output']>;
|
||||
rawName: Scalars['String']['output'];
|
||||
resolution?: Maybe<Scalars['String']['output']>;
|
||||
savePath?: Maybe<Scalars['String']['output']>;
|
||||
season: Scalars['Int']['output'];
|
||||
@ -665,8 +672,9 @@ export type EpisodesBasic = {
|
||||
homepage?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['Int']['output'];
|
||||
mikanEpisodeId?: Maybe<Scalars['String']['output']>;
|
||||
originName: Scalars['String']['output'];
|
||||
originPosterLink?: Maybe<Scalars['String']['output']>;
|
||||
posterLink?: Maybe<Scalars['String']['output']>;
|
||||
rawName: Scalars['String']['output'];
|
||||
resolution?: Maybe<Scalars['String']['output']>;
|
||||
savePath?: Maybe<Scalars['String']['output']>;
|
||||
season: Scalars['Int']['output'];
|
||||
@ -702,8 +710,9 @@ export type EpisodesFilterInput = {
|
||||
id?: InputMaybe<IntegerFilterInput>;
|
||||
mikanEpisodeId?: InputMaybe<StringFilterInput>;
|
||||
or?: InputMaybe<Array<EpisodesFilterInput>>;
|
||||
originName?: InputMaybe<StringFilterInput>;
|
||||
originPosterLink?: InputMaybe<StringFilterInput>;
|
||||
posterLink?: InputMaybe<StringFilterInput>;
|
||||
rawName?: InputMaybe<StringFilterInput>;
|
||||
resolution?: InputMaybe<StringFilterInput>;
|
||||
savePath?: InputMaybe<StringFilterInput>;
|
||||
season?: InputMaybe<IntegerFilterInput>;
|
||||
@ -723,8 +732,9 @@ export type EpisodesInsertInput = {
|
||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||
id?: InputMaybe<Scalars['Int']['input']>;
|
||||
mikanEpisodeId?: InputMaybe<Scalars['String']['input']>;
|
||||
originName: Scalars['String']['input'];
|
||||
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||
rawName: Scalars['String']['input'];
|
||||
resolution?: InputMaybe<Scalars['String']['input']>;
|
||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||
season: Scalars['Int']['input'];
|
||||
@ -744,8 +754,9 @@ export type EpisodesOrderInput = {
|
||||
homepage?: InputMaybe<OrderByEnum>;
|
||||
id?: InputMaybe<OrderByEnum>;
|
||||
mikanEpisodeId?: InputMaybe<OrderByEnum>;
|
||||
originName?: InputMaybe<OrderByEnum>;
|
||||
originPosterLink?: InputMaybe<OrderByEnum>;
|
||||
posterLink?: InputMaybe<OrderByEnum>;
|
||||
rawName?: InputMaybe<OrderByEnum>;
|
||||
resolution?: InputMaybe<OrderByEnum>;
|
||||
savePath?: InputMaybe<OrderByEnum>;
|
||||
season?: InputMaybe<OrderByEnum>;
|
||||
@ -765,8 +776,9 @@ export type EpisodesUpdateInput = {
|
||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||
id?: InputMaybe<Scalars['Int']['input']>;
|
||||
mikanEpisodeId?: InputMaybe<Scalars['String']['input']>;
|
||||
originName?: InputMaybe<Scalars['String']['input']>;
|
||||
originPosterLink?: InputMaybe<Scalars['String']['input']>;
|
||||
posterLink?: InputMaybe<Scalars['String']['input']>;
|
||||
rawName?: InputMaybe<Scalars['String']['input']>;
|
||||
resolution?: InputMaybe<Scalars['String']['input']>;
|
||||
savePath?: InputMaybe<Scalars['String']['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<{
|
||||
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 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 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 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>;
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
} from '@/components/ui/card';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Img } from '@/components/ui/img';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
@ -324,7 +325,18 @@ function SubscriptionDetailRouteComponent() {
|
||||
<div className="space-y-3">
|
||||
{subscription.bangumi.nodes.map((bangumi) => (
|
||||
<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">
|
||||
<Label className="font-medium text-muted-foreground text-xs">
|
||||
Display Name
|
||||
@ -333,14 +345,6 @@ function SubscriptionDetailRouteComponent() {
|
||||
{bangumi.displayName}
|
||||
</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">
|
||||
<Label className="font-medium text-muted-foreground text-xs">
|
||||
Fansub
|
||||
@ -351,10 +355,21 @@ function SubscriptionDetailRouteComponent() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<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>
|
||||
<div className="font-mono text-sm">
|
||||
{bangumi.savePath || '-'}
|
||||
{format(
|
||||
new Date(bangumi.updatedAt),
|
||||
'yyyy-MM-dd'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -21,6 +21,10 @@ impl OptDynErr {
|
||||
pub fn none() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for OptDynErr {
|
||||
|
Loading…
Reference in New Issue
Block a user