feat: add rss feeds and episode enclosure
This commit is contained in:
		
							parent
							
								
									16429a44b4
								
							
						
					
					
						commit
						f055011b86
					
				@ -212,7 +212,7 @@ async fn main() -> Result<()> {
 | 
				
			|||||||
                };
 | 
					                };
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                let episode_torrent_url = rss_item.url;
 | 
					                let episode_torrent_url = rss_item.torrent_link;
 | 
				
			||||||
                let episode_torrent_doppel_path = MikanDoppelPath::new(episode_torrent_url.clone());
 | 
					                let episode_torrent_doppel_path = MikanDoppelPath::new(episode_torrent_url.clone());
 | 
				
			||||||
                tracing::info!(title = rss_item.title, "Scraping episode torrent...");
 | 
					                tracing::info!(title = rss_item.title, "Scraping episode torrent...");
 | 
				
			||||||
                if !episode_torrent_doppel_path.exists_any() {
 | 
					                if !episode_torrent_doppel_path.exists_any() {
 | 
				
			||||||
 | 
				
			|||||||
@ -72,7 +72,7 @@ async fn main() -> RecorderResult<()> {
 | 
				
			|||||||
        }?;
 | 
					        }?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            let episode_torrent_url = rss_item.url;
 | 
					            let episode_torrent_url = rss_item.torrent_link;
 | 
				
			||||||
            let episode_torrent_doppel_path = MikanDoppelPath::new(episode_torrent_url.clone());
 | 
					            let episode_torrent_doppel_path = MikanDoppelPath::new(episode_torrent_url.clone());
 | 
				
			||||||
            tracing::info!(title = rss_item.title, "Scraping episode torrent...");
 | 
					            tracing::info!(title = rss_item.title, "Scraping episode torrent...");
 | 
				
			||||||
            if !episode_torrent_doppel_path.exists_any() {
 | 
					            if !episode_torrent_doppel_path.exists_any() {
 | 
				
			||||||
@ -173,7 +173,7 @@ async fn main() -> RecorderResult<()> {
 | 
				
			|||||||
                };
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    let episode_torrent_url = rss_item.url;
 | 
					                    let episode_torrent_url = rss_item.torrent_link;
 | 
				
			||||||
                    let episode_torrent_doppel_path =
 | 
					                    let episode_torrent_doppel_path =
 | 
				
			||||||
                        MikanDoppelPath::new(episode_torrent_url.clone());
 | 
					                        MikanDoppelPath::new(episode_torrent_url.clone());
 | 
				
			||||||
                    tracing::info!(title = rss_item.title, "Scraping episode torrent...");
 | 
					                    tracing::info!(title = rss_item.title, "Scraping episode torrent...");
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,8 @@ use crate::{
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const PROJECT_NAME: &str = "konobangu";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct App {
 | 
					pub struct App {
 | 
				
			||||||
    pub context: Arc<dyn AppContextTrait>,
 | 
					    pub context: Arc<dyn AppContextTrait>,
 | 
				
			||||||
    pub builder: AppBuilder,
 | 
					    pub builder: AppBuilder,
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ pub mod context;
 | 
				
			|||||||
pub mod core;
 | 
					pub mod core;
 | 
				
			||||||
pub mod env;
 | 
					pub mod env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use core::App;
 | 
					pub use core::{App, PROJECT_NAME};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use builder::AppBuilder;
 | 
					pub use builder::AppBuilder;
 | 
				
			||||||
pub use config::AppConfig;
 | 
					pub use config::AppConfig;
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@ use super::{
 | 
				
			|||||||
    service::{AuthServiceTrait, AuthUserInfo},
 | 
					    service::{AuthServiceTrait, AuthUserInfo},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    app::AppContextTrait,
 | 
					    app::{AppContextTrait, PROJECT_NAME},
 | 
				
			||||||
    models::{auth::AuthType, subscribers::SEED_SUBSCRIBER},
 | 
					    models::{auth::AuthType, subscribers::SEED_SUBSCRIBER},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -86,7 +86,7 @@ impl AuthServiceTrait for BasicAuthService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn www_authenticate_header_value(&self) -> Option<HeaderValue> {
 | 
					    fn www_authenticate_header_value(&self) -> Option<HeaderValue> {
 | 
				
			||||||
        Some(HeaderValue::from_static(r#"Basic realm="konobangu""#))
 | 
					        Some(HeaderValue::from_str(format!("Basic realm=\"{PROJECT_NAME}\"").as_str()).unwrap())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn auth_type(&self) -> AuthType {
 | 
					    fn auth_type(&self) -> AuthType {
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,11 @@ use super::{
 | 
				
			|||||||
    errors::{AuthError, OidcProviderUrlSnafu, OidcRequestRedirectUriSnafu},
 | 
					    errors::{AuthError, OidcProviderUrlSnafu, OidcRequestRedirectUriSnafu},
 | 
				
			||||||
    service::{AuthServiceTrait, AuthUserInfo},
 | 
					    service::{AuthServiceTrait, AuthUserInfo},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use crate::{app::AppContextTrait, errors::RecorderError, models::auth::AuthType};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    app::{AppContextTrait, PROJECT_NAME},
 | 
				
			||||||
 | 
					    errors::RecorderError,
 | 
				
			||||||
 | 
					    models::auth::AuthType,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct OidcHttpClient(pub Arc<HttpClient>);
 | 
					pub struct OidcHttpClient(pub Arc<HttpClient>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -351,7 +355,7 @@ impl AuthServiceTrait for OidcAuthService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn www_authenticate_header_value(&self) -> Option<HeaderValue> {
 | 
					    fn www_authenticate_header_value(&self) -> Option<HeaderValue> {
 | 
				
			||||||
        Some(HeaderValue::from_static(r#"Bearer realm="konobangu""#))
 | 
					        Some(HeaderValue::from_str(format!("Bearer realm=\"{PROJECT_NAME}\"").as_str()).unwrap())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn auth_type(&self) -> AuthType {
 | 
					    fn auth_type(&self) -> AuthType {
 | 
				
			||||||
 | 
				
			|||||||
@ -47,8 +47,12 @@ pub enum RecorderError {
 | 
				
			|||||||
    RegexError { source: regex::Error },
 | 
					    RegexError { source: regex::Error },
 | 
				
			||||||
    #[snafu(display("Invalid method"))]
 | 
					    #[snafu(display("Invalid method"))]
 | 
				
			||||||
    InvalidMethodError,
 | 
					    InvalidMethodError,
 | 
				
			||||||
 | 
					    #[snafu(display("Invalid header value"))]
 | 
				
			||||||
 | 
					    InvalidHeaderValueError,
 | 
				
			||||||
    #[snafu(display("Invalid header name"))]
 | 
					    #[snafu(display("Invalid header name"))]
 | 
				
			||||||
    InvalidHeaderNameError,
 | 
					    InvalidHeaderNameError,
 | 
				
			||||||
 | 
					    #[snafu(display("Missing origin (protocol or host) in headers and forwarded info"))]
 | 
				
			||||||
 | 
					    MissingOriginError,
 | 
				
			||||||
    #[snafu(transparent)]
 | 
					    #[snafu(transparent)]
 | 
				
			||||||
    TracingAppenderInitError {
 | 
					    TracingAppenderInitError {
 | 
				
			||||||
        source: tracing_appender::rolling::InitError,
 | 
					        source: tracing_appender::rolling::InitError,
 | 
				
			||||||
@ -87,8 +91,6 @@ pub enum RecorderError {
 | 
				
			|||||||
        #[snafu(source(from(opendal::Error, Box::new)))]
 | 
					        #[snafu(source(from(opendal::Error, Box::new)))]
 | 
				
			||||||
        source: Box<opendal::Error>,
 | 
					        source: Box<opendal::Error>,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    #[snafu(display("Invalid header value"))]
 | 
					 | 
				
			||||||
    InvalidHeaderValueError,
 | 
					 | 
				
			||||||
    #[snafu(transparent)]
 | 
					    #[snafu(transparent)]
 | 
				
			||||||
    HttpClientError { source: HttpClientError },
 | 
					    HttpClientError { source: HttpClientError },
 | 
				
			||||||
    #[cfg(feature = "testcontainers")]
 | 
					    #[cfg(feature = "testcontainers")]
 | 
				
			||||||
@ -248,6 +250,11 @@ impl IntoResponse for RecorderError {
 | 
				
			|||||||
                )
 | 
					                )
 | 
				
			||||||
                    .into_response()
 | 
					                    .into_response()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            Self::ModelEntityNotFound { entity } => (
 | 
				
			||||||
 | 
					                StatusCode::NOT_FOUND,
 | 
				
			||||||
 | 
					                Json::<StandardErrorResponse>(StandardErrorResponse::from(entity.to_string())),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					                .into_response(),
 | 
				
			||||||
            err => (
 | 
					            err => (
 | 
				
			||||||
                StatusCode::INTERNAL_SERVER_ERROR,
 | 
					                StatusCode::INTERNAL_SERVER_ERROR,
 | 
				
			||||||
                Json::<StandardErrorResponse>(StandardErrorResponse::from(err.to_string())),
 | 
					                Json::<StandardErrorResponse>(StandardErrorResponse::from(err.to_string())),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					use chrono::{DateTime, Utc};
 | 
				
			||||||
use fancy_regex::Regex as FancyRegex;
 | 
					use fancy_regex::Regex as FancyRegex;
 | 
				
			||||||
use lazy_static::lazy_static;
 | 
					use lazy_static::lazy_static;
 | 
				
			||||||
use quirks_path::Path;
 | 
					use quirks_path::Path;
 | 
				
			||||||
@ -33,6 +34,14 @@ lazy_static! {
 | 
				
			|||||||
        Regex::new(r"([Ss]|Season )(\d{1,3})").unwrap();
 | 
					        Regex::new(r"([Ss]|Season )(\d{1,3})").unwrap();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Debug)]
 | 
				
			||||||
 | 
					pub struct EpisodeEnclosureMeta {
 | 
				
			||||||
 | 
					    pub magnet_link: Option<String>,
 | 
				
			||||||
 | 
					    pub torrent_link: Option<String>,
 | 
				
			||||||
 | 
					    pub pub_date: Option<DateTime<Utc>>,
 | 
				
			||||||
 | 
					    pub content_length: Option<u64>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
 | 
					#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
 | 
				
			||||||
pub struct TorrentEpisodeMediaMeta {
 | 
					pub struct TorrentEpisodeMediaMeta {
 | 
				
			||||||
    pub fansub: Option<String>,
 | 
					    pub fansub: Option<String>,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,12 @@
 | 
				
			|||||||
use axum::http::{HeaderName, HeaderValue, Uri, header, request::Parts};
 | 
					use axum::{
 | 
				
			||||||
 | 
					    extract::FromRequestParts,
 | 
				
			||||||
 | 
					    http::{HeaderName, HeaderValue, Uri, header, request::Parts},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
use itertools::Itertools;
 | 
					use itertools::Itertools;
 | 
				
			||||||
use url::Url;
 | 
					use url::Url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::errors::RecorderError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Fields from a "Forwarded" header per [RFC7239 sec 4](https://www.rfc-editor.org/rfc/rfc7239#section-4)
 | 
					/// Fields from a "Forwarded" header per [RFC7239 sec 4](https://www.rfc-editor.org/rfc/rfc7239#section-4)
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
pub struct ForwardedHeader {
 | 
					pub struct ForwardedHeader {
 | 
				
			||||||
@ -101,9 +106,13 @@ pub struct ForwardedRelatedInfo {
 | 
				
			|||||||
    pub origin: Option<String>,
 | 
					    pub origin: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl ForwardedRelatedInfo {
 | 
					impl<T> FromRequestParts<T> for ForwardedRelatedInfo {
 | 
				
			||||||
    pub fn from_request_parts(request_parts: &Parts) -> ForwardedRelatedInfo {
 | 
					    type Rejection = RecorderError;
 | 
				
			||||||
        let headers = &request_parts.headers;
 | 
					    fn from_request_parts(
 | 
				
			||||||
 | 
					        parts: &mut Parts,
 | 
				
			||||||
 | 
					        _state: &T,
 | 
				
			||||||
 | 
					    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
 | 
				
			||||||
 | 
					        let headers = &parts.headers;
 | 
				
			||||||
        let forwarded = headers
 | 
					        let forwarded = headers
 | 
				
			||||||
            .get(header::FORWARDED)
 | 
					            .get(header::FORWARDED)
 | 
				
			||||||
            .and_then(|s| ForwardedHeader::try_from(s.clone()).ok());
 | 
					            .and_then(|s| ForwardedHeader::try_from(s.clone()).ok());
 | 
				
			||||||
@ -132,17 +141,19 @@ impl ForwardedRelatedInfo {
 | 
				
			|||||||
            .get(header::ORIGIN)
 | 
					            .get(header::ORIGIN)
 | 
				
			||||||
            .and_then(|s| s.to_str().map(String::from).ok());
 | 
					            .and_then(|s| s.to_str().map(String::from).ok());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ForwardedRelatedInfo {
 | 
					        futures::future::ready(Ok(ForwardedRelatedInfo {
 | 
				
			||||||
            host,
 | 
					            host,
 | 
				
			||||||
            x_forwarded_for,
 | 
					            x_forwarded_for,
 | 
				
			||||||
            x_forwarded_host,
 | 
					            x_forwarded_host,
 | 
				
			||||||
            x_forwarded_proto,
 | 
					            x_forwarded_proto,
 | 
				
			||||||
            forwarded,
 | 
					            forwarded,
 | 
				
			||||||
            uri: request_parts.uri.clone(),
 | 
					            uri: parts.uri.clone(),
 | 
				
			||||||
            origin,
 | 
					            origin,
 | 
				
			||||||
        }
 | 
					        }))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ForwardedRelatedInfo {
 | 
				
			||||||
    pub fn resolved_protocol(&self) -> Option<&str> {
 | 
					    pub fn resolved_protocol(&self) -> Option<&str> {
 | 
				
			||||||
        self.forwarded
 | 
					        self.forwarded
 | 
				
			||||||
            .as_ref()
 | 
					            .as_ref()
 | 
				
			||||||
 | 
				
			|||||||
@ -20,12 +20,15 @@ use super::scrape_mikan_bangumi_meta_stream_from_season_flow_url;
 | 
				
			|||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    app::AppContextTrait,
 | 
					    app::AppContextTrait,
 | 
				
			||||||
    errors::{RecorderError, RecorderResult},
 | 
					    errors::{RecorderError, RecorderResult},
 | 
				
			||||||
    extract::mikan::{
 | 
					    extract::{
 | 
				
			||||||
        MikanBangumiHash, MikanBangumiMeta, MikanEpisodeHash, MikanEpisodeMeta,
 | 
					        bittorrent::EpisodeEnclosureMeta,
 | 
				
			||||||
        MikanRssEpisodeItem, MikanSeasonFlowUrlMeta, MikanSeasonStr,
 | 
					        mikan::{
 | 
				
			||||||
        MikanSubscriberSubscriptionRssUrlMeta, build_mikan_bangumi_subscription_rss_url,
 | 
					            MikanBangumiHash, MikanBangumiMeta, MikanEpisodeHash, MikanEpisodeMeta,
 | 
				
			||||||
        build_mikan_season_flow_url, build_mikan_subscriber_subscription_rss_url,
 | 
					            MikanRssEpisodeItem, MikanSeasonFlowUrlMeta, MikanSeasonStr,
 | 
				
			||||||
        scrape_mikan_episode_meta_from_episode_homepage_url,
 | 
					            MikanSubscriberSubscriptionRssUrlMeta, build_mikan_bangumi_subscription_rss_url,
 | 
				
			||||||
 | 
					            build_mikan_season_flow_url, build_mikan_subscriber_subscription_rss_url,
 | 
				
			||||||
 | 
					            scrape_mikan_episode_meta_from_episode_homepage_url,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    models::{
 | 
					    models::{
 | 
				
			||||||
        bangumi, episodes, subscription_bangumi, subscription_episode,
 | 
					        bangumi, episodes, subscription_bangumi, subscription_episode,
 | 
				
			||||||
@ -54,7 +57,7 @@ async fn sync_mikan_feeds_from_rss_item_list(
 | 
				
			|||||||
        .map(|(episode_id, hash, bangumi_id)| (hash.mikan_episode_id, (episode_id, bangumi_id)))
 | 
					        .map(|(episode_id, hash, bangumi_id)| (hash.mikan_episode_id, (episode_id, bangumi_id)))
 | 
				
			||||||
        .collect::<HashMap<_, _>>();
 | 
					        .collect::<HashMap<_, _>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut new_episode_meta_list: Vec<MikanEpisodeMeta> = vec![];
 | 
					        let mut new_episode_meta_list: Vec<(MikanEpisodeMeta, EpisodeEnclosureMeta)> = vec![];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mikan_client = ctx.mikan();
 | 
					        let mikan_client = ctx.mikan();
 | 
				
			||||||
        for to_insert_rss_item in rss_item_list.into_iter().filter(|rss_item| {
 | 
					        for to_insert_rss_item in rss_item_list.into_iter().filter(|rss_item| {
 | 
				
			||||||
@ -65,7 +68,8 @@ async fn sync_mikan_feeds_from_rss_item_list(
 | 
				
			|||||||
                to_insert_rss_item.build_homepage_url(mikan_base_url.clone()),
 | 
					                to_insert_rss_item.build_homepage_url(mikan_base_url.clone()),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .await?;
 | 
					            .await?;
 | 
				
			||||||
            new_episode_meta_list.push(episode_meta);
 | 
					            let episode_enclosure_meta = EpisodeEnclosureMeta::from(to_insert_rss_item);
 | 
				
			||||||
 | 
					            new_episode_meta_list.push((episode_meta, episode_enclosure_meta));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        (new_episode_meta_list, existed_episode_hash2id_map)
 | 
					        (new_episode_meta_list, existed_episode_hash2id_map)
 | 
				
			||||||
@ -92,22 +96,22 @@ async fn sync_mikan_feeds_from_rss_item_list(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    let new_episode_meta_list_group_by_bangumi_hash: HashMap<
 | 
					    let new_episode_meta_list_group_by_bangumi_hash: HashMap<
 | 
				
			||||||
        MikanBangumiHash,
 | 
					        MikanBangumiHash,
 | 
				
			||||||
        Vec<MikanEpisodeMeta>,
 | 
					        Vec<(MikanEpisodeMeta, EpisodeEnclosureMeta)>,
 | 
				
			||||||
    > = {
 | 
					    > = {
 | 
				
			||||||
        let mut m = hashmap! {};
 | 
					        let mut m = hashmap! {};
 | 
				
			||||||
        for episode_meta in new_episode_meta_list {
 | 
					        for (episode_meta, episode_enclosure_meta) in new_episode_meta_list {
 | 
				
			||||||
            let bangumi_hash = episode_meta.bangumi_hash();
 | 
					            let bangumi_hash = episode_meta.bangumi_hash();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            m.entry(bangumi_hash)
 | 
					            m.entry(bangumi_hash)
 | 
				
			||||||
                .or_insert_with(Vec::new)
 | 
					                .or_insert_with(Vec::new)
 | 
				
			||||||
                .push(episode_meta);
 | 
					                .push((episode_meta, episode_enclosure_meta));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        m
 | 
					        m
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (group_bangumi_hash, group_episode_meta_list) in new_episode_meta_list_group_by_bangumi_hash
 | 
					    for (group_bangumi_hash, group_episode_meta_list) in new_episode_meta_list_group_by_bangumi_hash
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        let first_episode_meta = group_episode_meta_list.first().unwrap();
 | 
					        let (first_episode_meta, _) = group_episode_meta_list.first().unwrap();
 | 
				
			||||||
        let group_bangumi_model = bangumi::Model::get_or_insert_from_mikan(
 | 
					        let group_bangumi_model = bangumi::Model::get_or_insert_from_mikan(
 | 
				
			||||||
            ctx,
 | 
					            ctx,
 | 
				
			||||||
            group_bangumi_hash,
 | 
					            group_bangumi_hash,
 | 
				
			||||||
@ -126,9 +130,12 @@ async fn sync_mikan_feeds_from_rss_item_list(
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .await?;
 | 
					        .await?;
 | 
				
			||||||
        let group_episode_creation_list = group_episode_meta_list
 | 
					        let group_episode_creation_list =
 | 
				
			||||||
            .into_iter()
 | 
					            group_episode_meta_list
 | 
				
			||||||
            .map(|episode_meta| (&group_bangumi_model, episode_meta));
 | 
					                .into_iter()
 | 
				
			||||||
 | 
					                .map(|(episode_meta, episode_enclosure_meta)| {
 | 
				
			||||||
 | 
					                    (&group_bangumi_model, episode_meta, episode_enclosure_meta)
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        episodes::Model::add_mikan_episodes_for_subscription(
 | 
					        episodes::Model::add_mikan_episodes_for_subscription(
 | 
				
			||||||
            ctx,
 | 
					            ctx,
 | 
				
			||||||
@ -273,7 +280,7 @@ impl MikanSubscriberSubscription {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, InputObject, SimpleObject)]
 | 
					#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 | 
				
			||||||
pub struct MikanSeasonSubscription {
 | 
					pub struct MikanSeasonSubscription {
 | 
				
			||||||
    pub subscription_id: i32,
 | 
					    pub subscription_id: i32,
 | 
				
			||||||
    pub year: i32,
 | 
					    pub year: i32,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ use std::{borrow::Cow, fmt, str::FromStr, sync::Arc};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use async_stream::try_stream;
 | 
					use async_stream::try_stream;
 | 
				
			||||||
use bytes::Bytes;
 | 
					use bytes::Bytes;
 | 
				
			||||||
use chrono::DateTime;
 | 
					use chrono::{DateTime, Utc};
 | 
				
			||||||
use downloader::bittorrent::defs::BITTORRENT_MIME_TYPE;
 | 
					use downloader::bittorrent::defs::BITTORRENT_MIME_TYPE;
 | 
				
			||||||
use fetch::{html::fetch_html, image::fetch_image};
 | 
					use fetch::{html::fetch_html, image::fetch_image};
 | 
				
			||||||
use futures::{Stream, TryStreamExt, pin_mut};
 | 
					use futures::{Stream, TryStreamExt, pin_mut};
 | 
				
			||||||
@ -17,6 +17,7 @@ use crate::{
 | 
				
			|||||||
    app::AppContextTrait,
 | 
					    app::AppContextTrait,
 | 
				
			||||||
    errors::app_error::{RecorderError, RecorderResult},
 | 
					    errors::app_error::{RecorderError, RecorderResult},
 | 
				
			||||||
    extract::{
 | 
					    extract::{
 | 
				
			||||||
 | 
					        bittorrent::EpisodeEnclosureMeta,
 | 
				
			||||||
        html::{extract_background_image_src_from_style_attr, extract_inner_text_from_element_ref},
 | 
					        html::{extract_background_image_src_from_style_attr, extract_inner_text_from_element_ref},
 | 
				
			||||||
        media::extract_image_src_from_str,
 | 
					        media::extract_image_src_from_str,
 | 
				
			||||||
        mikan::{
 | 
					        mikan::{
 | 
				
			||||||
@ -39,11 +40,12 @@ use crate::{
 | 
				
			|||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 | 
					#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 | 
				
			||||||
pub struct MikanRssEpisodeItem {
 | 
					pub struct MikanRssEpisodeItem {
 | 
				
			||||||
    pub title: String,
 | 
					    pub title: String,
 | 
				
			||||||
    pub url: Url,
 | 
					    pub torrent_link: Url,
 | 
				
			||||||
    pub content_length: Option<u64>,
 | 
					    pub content_length: Option<u64>,
 | 
				
			||||||
    pub mime: String,
 | 
					    pub mime: String,
 | 
				
			||||||
    pub pub_date: Option<i64>,
 | 
					    pub pub_date: Option<DateTime<Utc>>,
 | 
				
			||||||
    pub mikan_episode_id: String,
 | 
					    pub mikan_episode_id: String,
 | 
				
			||||||
 | 
					    pub magnet_link: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl MikanRssEpisodeItem {
 | 
					impl MikanRssEpisodeItem {
 | 
				
			||||||
@ -95,18 +97,30 @@ impl TryFrom<rss::Item> for MikanRssEpisodeItem {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Ok(MikanRssEpisodeItem {
 | 
					        Ok(MikanRssEpisodeItem {
 | 
				
			||||||
            title,
 | 
					            title,
 | 
				
			||||||
            url: enclosure_url,
 | 
					            torrent_link: enclosure_url,
 | 
				
			||||||
            content_length: enclosure.length.parse().ok(),
 | 
					            content_length: enclosure.length.parse().ok(),
 | 
				
			||||||
            mime: mime_type,
 | 
					            mime: mime_type,
 | 
				
			||||||
            pub_date: item
 | 
					            pub_date: item.pub_date.and_then(|s| {
 | 
				
			||||||
                .pub_date
 | 
					                DateTime::parse_from_rfc2822(&s)
 | 
				
			||||||
                .and_then(|s| DateTime::parse_from_rfc2822(&s).ok())
 | 
					                    .ok()
 | 
				
			||||||
                .map(|s| s.timestamp_millis()),
 | 
					                    .map(|s| s.with_timezone(&Utc))
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
            mikan_episode_id,
 | 
					            mikan_episode_id,
 | 
				
			||||||
 | 
					            magnet_link: None,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<MikanRssEpisodeItem> for EpisodeEnclosureMeta {
 | 
				
			||||||
 | 
					    fn from(item: MikanRssEpisodeItem) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            magnet_link: item.magnet_link,
 | 
				
			||||||
 | 
					            torrent_link: Some(item.torrent_link.to_string()),
 | 
				
			||||||
 | 
					            pub_date: item.pub_date,
 | 
				
			||||||
 | 
					            content_length: item.content_length,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 | 
					#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 | 
				
			||||||
pub struct MikanSubscriberSubscriptionRssUrlMeta {
 | 
					pub struct MikanSubscriberSubscriptionRssUrlMeta {
 | 
				
			||||||
    pub mikan_subscription_token: String,
 | 
					    pub mikan_subscription_token: String,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								apps/recorder/src/graphql/domains/bangumi.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								apps/recorder/src/graphql/domains/bangumi.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					use seaography::{Builder as SeaographyBuilder, BuilderContext};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{graphql::domains::subscribers::restrict_subscriber_for_entity, models::bangumi};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_bangumi_to_schema_context(context: &mut BuilderContext) {
 | 
				
			||||||
 | 
					    restrict_subscriber_for_entity::<bangumi::Entity>(context, &bangumi::Column::SubscriberId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_bangumi_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder {
 | 
				
			||||||
 | 
					    builder.register_enumeration::<bangumi::BangumiType>();
 | 
				
			||||||
 | 
					    seaography::register_entity!(builder, bangumi);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    builder
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3,12 +3,22 @@ use std::sync::Arc;
 | 
				
			|||||||
use async_graphql::dynamic::{
 | 
					use async_graphql::dynamic::{
 | 
				
			||||||
    Field, FieldFuture, FieldValue, InputObject, InputValue, Object, TypeRef,
 | 
					    Field, FieldFuture, FieldValue, InputObject, InputValue, Object, TypeRef,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use seaography::Builder as SeaographyBuilder;
 | 
					use seaography::{Builder as SeaographyBuilder, BuilderContext};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use util_derive::DynamicGraphql;
 | 
					use util_derive::DynamicGraphql;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    app::AppContextTrait, auth::AuthUserInfo, errors::RecorderError, models::credential_3rd,
 | 
					    app::AppContextTrait,
 | 
				
			||||||
 | 
					    auth::AuthUserInfo,
 | 
				
			||||||
 | 
					    errors::RecorderError,
 | 
				
			||||||
 | 
					    graphql::{
 | 
				
			||||||
 | 
					        domains::subscribers::restrict_subscriber_for_entity,
 | 
				
			||||||
 | 
					        infra::crypto::{
 | 
				
			||||||
 | 
					            register_crypto_column_input_conversion_to_schema_context,
 | 
				
			||||||
 | 
					            register_crypto_column_output_conversion_to_schema_context,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    models::credential_3rd,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(DynamicGraphql, Serialize, Deserialize, Clone, Debug)]
 | 
					#[derive(DynamicGraphql, Serialize, Deserialize, Clone, Debug)]
 | 
				
			||||||
@ -63,9 +73,52 @@ impl Credential3rdCheckAvailableInfo {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_credential3rd_to_schema_context(
 | 
				
			||||||
 | 
					    context: &mut BuilderContext,
 | 
				
			||||||
 | 
					    ctx: Arc<dyn AppContextTrait>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    restrict_subscriber_for_entity::<credential_3rd::Entity>(
 | 
				
			||||||
 | 
					        context,
 | 
				
			||||||
 | 
					        &credential_3rd::Column::SubscriberId,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    register_crypto_column_input_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
				
			||||||
 | 
					        context,
 | 
				
			||||||
 | 
					        ctx.clone(),
 | 
				
			||||||
 | 
					        &credential_3rd::Column::Cookies,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    register_crypto_column_input_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
				
			||||||
 | 
					        context,
 | 
				
			||||||
 | 
					        ctx.clone(),
 | 
				
			||||||
 | 
					        &credential_3rd::Column::Username,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    register_crypto_column_input_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
				
			||||||
 | 
					        context,
 | 
				
			||||||
 | 
					        ctx.clone(),
 | 
				
			||||||
 | 
					        &credential_3rd::Column::Password,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    register_crypto_column_output_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
				
			||||||
 | 
					        context,
 | 
				
			||||||
 | 
					        ctx.clone(),
 | 
				
			||||||
 | 
					        &credential_3rd::Column::Cookies,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    register_crypto_column_output_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
				
			||||||
 | 
					        context,
 | 
				
			||||||
 | 
					        ctx.clone(),
 | 
				
			||||||
 | 
					        &credential_3rd::Column::Username,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    register_crypto_column_output_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
				
			||||||
 | 
					        context,
 | 
				
			||||||
 | 
					        ctx,
 | 
				
			||||||
 | 
					        &credential_3rd::Column::Password,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn register_credential3rd_to_schema_builder(
 | 
					pub fn register_credential3rd_to_schema_builder(
 | 
				
			||||||
    mut builder: SeaographyBuilder,
 | 
					    mut builder: SeaographyBuilder,
 | 
				
			||||||
) -> SeaographyBuilder {
 | 
					) -> SeaographyBuilder {
 | 
				
			||||||
 | 
					    builder.register_enumeration::<credential_3rd::Credential3rdType>();
 | 
				
			||||||
 | 
					    seaography::register_entity!(builder, credential_3rd);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    builder.schema = builder
 | 
					    builder.schema = builder
 | 
				
			||||||
        .schema
 | 
					        .schema
 | 
				
			||||||
        .register(Credential3rdCheckAvailableInput::generate_input_object());
 | 
					        .register(Credential3rdCheckAvailableInput::generate_input_object());
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										17
									
								
								apps/recorder/src/graphql/domains/downloaders.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								apps/recorder/src/graphql/domains/downloaders.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					use seaography::{Builder as SeaographyBuilder, BuilderContext};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{graphql::domains::subscribers::restrict_subscriber_for_entity, models::downloaders};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_downloaders_to_schema_context(context: &mut BuilderContext) {
 | 
				
			||||||
 | 
					    restrict_subscriber_for_entity::<downloaders::Entity>(
 | 
				
			||||||
 | 
					        context,
 | 
				
			||||||
 | 
					        &downloaders::Column::SubscriberId,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_downloaders_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder {
 | 
				
			||||||
 | 
					    builder.register_enumeration::<downloaders::DownloaderCategory>();
 | 
				
			||||||
 | 
					    seaography::register_entity!(builder, downloaders);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    builder
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								apps/recorder/src/graphql/domains/downloads.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								apps/recorder/src/graphql/domains/downloads.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					use seaography::{Builder as SeaographyBuilder, BuilderContext};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{graphql::domains::subscribers::restrict_subscriber_for_entity, models::downloads};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_downloads_to_schema_context(context: &mut BuilderContext) {
 | 
				
			||||||
 | 
					    restrict_subscriber_for_entity::<downloads::Entity>(context, &downloads::Column::SubscriberId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_downloads_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder {
 | 
				
			||||||
 | 
					    builder.register_enumeration::<downloads::DownloadStatus>();
 | 
				
			||||||
 | 
					    builder.register_enumeration::<downloads::DownloadMime>();
 | 
				
			||||||
 | 
					    seaography::register_entity!(builder, downloads);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    builder
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								apps/recorder/src/graphql/domains/episodes.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								apps/recorder/src/graphql/domains/episodes.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					use seaography::{Builder as SeaographyBuilder, BuilderContext};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{graphql::domains::subscribers::restrict_subscriber_for_entity, models::episodes};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_episodes_to_schema_context(context: &mut BuilderContext) {
 | 
				
			||||||
 | 
					    restrict_subscriber_for_entity::<episodes::Entity>(context, &episodes::Column::SubscriberId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_episodes_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder {
 | 
				
			||||||
 | 
					    builder.register_enumeration::<episodes::EpisodeType>();
 | 
				
			||||||
 | 
					    seaography::register_entity!(builder, episodes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    builder
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								apps/recorder/src/graphql/domains/feeds.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								apps/recorder/src/graphql/domains/feeds.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					use seaography::{Builder as SeaographyBuilder, BuilderContext};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{graphql::domains::subscribers::restrict_subscriber_for_entity, models::feeds};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_feeds_to_schema_context(context: &mut BuilderContext) {
 | 
				
			||||||
 | 
					    restrict_subscriber_for_entity::<feeds::Entity>(context, &feeds::Column::SubscriberId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_feeds_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder {
 | 
				
			||||||
 | 
					    builder.register_enumeration::<feeds::FeedType>();
 | 
				
			||||||
 | 
					    builder.register_enumeration::<feeds::FeedSource>();
 | 
				
			||||||
 | 
					    seaography::register_entity!(builder, feeds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    builder
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,5 +1,12 @@
 | 
				
			|||||||
pub mod credential_3rd;
 | 
					pub mod credential_3rd;
 | 
				
			||||||
pub mod crypto;
 | 
					
 | 
				
			||||||
 | 
					pub mod bangumi;
 | 
				
			||||||
 | 
					pub mod downloaders;
 | 
				
			||||||
 | 
					pub mod downloads;
 | 
				
			||||||
 | 
					pub mod episodes;
 | 
				
			||||||
 | 
					pub mod feeds;
 | 
				
			||||||
pub mod subscriber_tasks;
 | 
					pub mod subscriber_tasks;
 | 
				
			||||||
pub mod subscribers;
 | 
					pub mod subscribers;
 | 
				
			||||||
 | 
					pub mod subscription_bangumi;
 | 
				
			||||||
 | 
					pub mod subscription_episode;
 | 
				
			||||||
pub mod subscriptions;
 | 
					pub mod subscriptions;
 | 
				
			||||||
 | 
				
			|||||||
@ -320,6 +320,7 @@ where
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn register_subscribers_to_schema_context(context: &mut BuilderContext) {
 | 
					pub fn register_subscribers_to_schema_context(context: &mut BuilderContext) {
 | 
				
			||||||
 | 
					    restrict_subscriber_for_entity::<subscribers::Entity>(context, &subscribers::Column::Id);
 | 
				
			||||||
    for column in subscribers::Column::iter() {
 | 
					    for column in subscribers::Column::iter() {
 | 
				
			||||||
        if !matches!(column, subscribers::Column::Id) {
 | 
					        if !matches!(column, subscribers::Column::Id) {
 | 
				
			||||||
            let key = get_entity_column_key::<subscribers::Entity>(context, &column);
 | 
					            let key = get_entity_column_key::<subscribers::Entity>(context, &column);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								apps/recorder/src/graphql/domains/subscription_bangumi.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/recorder/src/graphql/domains/subscription_bangumi.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					use seaography::{Builder as SeaographyBuilder, BuilderContext};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    graphql::domains::subscribers::restrict_subscriber_for_entity, models::subscription_bangumi,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_subscription_bangumi_to_schema_context(context: &mut BuilderContext) {
 | 
				
			||||||
 | 
					    restrict_subscriber_for_entity::<subscription_bangumi::Entity>(
 | 
				
			||||||
 | 
					        context,
 | 
				
			||||||
 | 
					        &subscription_bangumi::Column::SubscriberId,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_subscription_bangumi_to_schema_builder(
 | 
				
			||||||
 | 
					    mut builder: SeaographyBuilder,
 | 
				
			||||||
 | 
					) -> SeaographyBuilder {
 | 
				
			||||||
 | 
					    seaography::register_entity!(builder, subscription_bangumi);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    builder
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								apps/recorder/src/graphql/domains/subscription_episode.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/recorder/src/graphql/domains/subscription_episode.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					use seaography::{Builder as SeaographyBuilder, BuilderContext};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    graphql::domains::subscribers::restrict_subscriber_for_entity, models::subscription_episode,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_subscription_episode_to_schema_context(context: &mut BuilderContext) {
 | 
				
			||||||
 | 
					    restrict_subscriber_for_entity::<subscription_episode::Entity>(
 | 
				
			||||||
 | 
					        context,
 | 
				
			||||||
 | 
					        &subscription_episode::Column::SubscriberId,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_subscription_episode_to_schema_builder(
 | 
				
			||||||
 | 
					    mut builder: SeaographyBuilder,
 | 
				
			||||||
 | 
					) -> SeaographyBuilder {
 | 
				
			||||||
 | 
					    seaography::register_entity!(builder, subscription_episode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    builder
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3,13 +3,16 @@ use std::sync::Arc;
 | 
				
			|||||||
use async_graphql::dynamic::{FieldValue, TypeRef};
 | 
					use async_graphql::dynamic::{FieldValue, TypeRef};
 | 
				
			||||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
 | 
					use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
 | 
				
			||||||
use seaography::{
 | 
					use seaography::{
 | 
				
			||||||
    Builder as SeaographyBuilder, EntityObjectBuilder, EntityQueryFieldBuilder,
 | 
					    Builder as SeaographyBuilder, BuilderContext, EntityObjectBuilder, EntityQueryFieldBuilder,
 | 
				
			||||||
    get_filter_conditions,
 | 
					    get_filter_conditions,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    errors::RecorderError,
 | 
					    errors::RecorderError,
 | 
				
			||||||
    graphql::infra::custom::generate_entity_filter_mutation_field,
 | 
					    graphql::{
 | 
				
			||||||
 | 
					        domains::subscribers::restrict_subscriber_for_entity,
 | 
				
			||||||
 | 
					        infra::custom::generate_entity_filter_mutation_field,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    models::{
 | 
					    models::{
 | 
				
			||||||
        subscriber_tasks,
 | 
					        subscriber_tasks,
 | 
				
			||||||
        subscriptions::{self, SubscriptionTrait},
 | 
					        subscriptions::{self, SubscriptionTrait},
 | 
				
			||||||
@ -17,9 +20,19 @@ use crate::{
 | 
				
			|||||||
    task::SubscriberTask,
 | 
					    task::SubscriberTask,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn register_subscriptions_to_schema_context(context: &mut BuilderContext) {
 | 
				
			||||||
 | 
					    restrict_subscriber_for_entity::<subscriptions::Entity>(
 | 
				
			||||||
 | 
					        context,
 | 
				
			||||||
 | 
					        &subscriptions::Column::SubscriberId,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn register_subscriptions_to_schema_builder(
 | 
					pub fn register_subscriptions_to_schema_builder(
 | 
				
			||||||
    mut builder: SeaographyBuilder,
 | 
					    mut builder: SeaographyBuilder,
 | 
				
			||||||
) -> SeaographyBuilder {
 | 
					) -> SeaographyBuilder {
 | 
				
			||||||
 | 
					    builder.register_enumeration::<subscriptions::SubscriptionCategory>();
 | 
				
			||||||
 | 
					    seaography::register_entity!(builder, subscriptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let context = builder.context;
 | 
					    let context = builder.context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let entity_object_builder = EntityObjectBuilder { context };
 | 
					    let entity_object_builder = EntityObjectBuilder { context };
 | 
				
			||||||
 | 
				
			|||||||
@ -7,10 +7,9 @@ use seaography::{BuilderContext, SeaResult};
 | 
				
			|||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    app::AppContextTrait,
 | 
					    app::AppContextTrait,
 | 
				
			||||||
    graphql::infra::util::{get_column_key, get_entity_key},
 | 
					    graphql::infra::util::{get_column_key, get_entity_key},
 | 
				
			||||||
    models::credential_3rd,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn register_crypto_column_input_conversion_to_schema_context<T>(
 | 
					pub fn register_crypto_column_input_conversion_to_schema_context<T>(
 | 
				
			||||||
    context: &mut BuilderContext,
 | 
					    context: &mut BuilderContext,
 | 
				
			||||||
    ctx: Arc<dyn AppContextTrait>,
 | 
					    ctx: Arc<dyn AppContextTrait>,
 | 
				
			||||||
    column: &T::Column,
 | 
					    column: &T::Column,
 | 
				
			||||||
@ -37,7 +36,7 @@ fn register_crypto_column_input_conversion_to_schema_context<T>(
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn register_crypto_column_output_conversion_to_schema_context<T>(
 | 
					pub fn register_crypto_column_output_conversion_to_schema_context<T>(
 | 
				
			||||||
    context: &mut BuilderContext,
 | 
					    context: &mut BuilderContext,
 | 
				
			||||||
    ctx: Arc<dyn AppContextTrait>,
 | 
					    ctx: Arc<dyn AppContextTrait>,
 | 
				
			||||||
    column: &T::Column,
 | 
					    column: &T::Column,
 | 
				
			||||||
@ -68,39 +67,3 @@ fn register_crypto_column_output_conversion_to_schema_context<T>(
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn register_crypto_to_schema_context(
 | 
					 | 
				
			||||||
    context: &mut BuilderContext,
 | 
					 | 
				
			||||||
    ctx: Arc<dyn AppContextTrait>,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    register_crypto_column_input_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
					 | 
				
			||||||
        context,
 | 
					 | 
				
			||||||
        ctx.clone(),
 | 
					 | 
				
			||||||
        &credential_3rd::Column::Cookies,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    register_crypto_column_input_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
					 | 
				
			||||||
        context,
 | 
					 | 
				
			||||||
        ctx.clone(),
 | 
					 | 
				
			||||||
        &credential_3rd::Column::Username,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    register_crypto_column_input_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
					 | 
				
			||||||
        context,
 | 
					 | 
				
			||||||
        ctx.clone(),
 | 
					 | 
				
			||||||
        &credential_3rd::Column::Password,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    register_crypto_column_output_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
					 | 
				
			||||||
        context,
 | 
					 | 
				
			||||||
        ctx.clone(),
 | 
					 | 
				
			||||||
        &credential_3rd::Column::Cookies,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    register_crypto_column_output_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
					 | 
				
			||||||
        context,
 | 
					 | 
				
			||||||
        ctx.clone(),
 | 
					 | 
				
			||||||
        &credential_3rd::Column::Username,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    register_crypto_column_output_conversion_to_schema_context::<credential_3rd::Entity>(
 | 
					 | 
				
			||||||
        context,
 | 
					 | 
				
			||||||
        ctx,
 | 
					 | 
				
			||||||
        &credential_3rd::Column::Password,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					pub mod crypto;
 | 
				
			||||||
pub mod custom;
 | 
					pub mod custom;
 | 
				
			||||||
pub mod json;
 | 
					pub mod json;
 | 
				
			||||||
pub mod util;
 | 
					pub mod util;
 | 
				
			||||||
 | 
				
			|||||||
@ -8,17 +8,36 @@ use crate::{
 | 
				
			|||||||
    app::AppContextTrait,
 | 
					    app::AppContextTrait,
 | 
				
			||||||
    graphql::{
 | 
					    graphql::{
 | 
				
			||||||
        domains::{
 | 
					        domains::{
 | 
				
			||||||
            credential_3rd::register_credential3rd_to_schema_builder,
 | 
					            bangumi::{register_bangumi_to_schema_builder, register_bangumi_to_schema_context},
 | 
				
			||||||
            crypto::register_crypto_to_schema_context,
 | 
					            credential_3rd::{
 | 
				
			||||||
 | 
					                register_credential3rd_to_schema_builder, register_credential3rd_to_schema_context,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            downloaders::{
 | 
				
			||||||
 | 
					                register_downloaders_to_schema_builder, register_downloaders_to_schema_context,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            downloads::{
 | 
				
			||||||
 | 
					                register_downloads_to_schema_builder, register_downloads_to_schema_context,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            episodes::{register_episodes_to_schema_builder, register_episodes_to_schema_context},
 | 
				
			||||||
 | 
					            feeds::{register_feeds_to_schema_builder, register_feeds_to_schema_context},
 | 
				
			||||||
            subscriber_tasks::{
 | 
					            subscriber_tasks::{
 | 
				
			||||||
                register_subscriber_tasks_to_schema_builder,
 | 
					                register_subscriber_tasks_to_schema_builder,
 | 
				
			||||||
                register_subscriber_tasks_to_schema_context,
 | 
					                register_subscriber_tasks_to_schema_context,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            subscribers::{
 | 
					            subscribers::{
 | 
				
			||||||
                register_subscribers_to_schema_builder, register_subscribers_to_schema_context,
 | 
					                register_subscribers_to_schema_builder, register_subscribers_to_schema_context,
 | 
				
			||||||
                restrict_subscriber_for_entity,
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            subscriptions::register_subscriptions_to_schema_builder,
 | 
					            subscription_bangumi::{
 | 
				
			||||||
 | 
					                register_subscription_bangumi_to_schema_builder,
 | 
				
			||||||
 | 
					                register_subscription_bangumi_to_schema_context,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            subscription_episode::{
 | 
				
			||||||
 | 
					                register_subscription_episode_to_schema_builder,
 | 
				
			||||||
 | 
					                register_subscription_episode_to_schema_context,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            subscriptions::{
 | 
				
			||||||
 | 
					                register_subscriptions_to_schema_builder, register_subscriptions_to_schema_context,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        infra::json::register_jsonb_input_filter_to_schema_builder,
 | 
					        infra::json::register_jsonb_input_filter_to_schema_builder,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -31,7 +50,6 @@ pub fn build_schema(
 | 
				
			|||||||
    depth: Option<usize>,
 | 
					    depth: Option<usize>,
 | 
				
			||||||
    complexity: Option<usize>,
 | 
					    complexity: Option<usize>,
 | 
				
			||||||
) -> Result<Schema, SchemaError> {
 | 
					) -> Result<Schema, SchemaError> {
 | 
				
			||||||
    use crate::models::*;
 | 
					 | 
				
			||||||
    let database = app_ctx.db().as_ref().clone();
 | 
					    let database = app_ctx.db().as_ref().clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let context = CONTEXT.get_or_init(|| {
 | 
					    let context = CONTEXT.get_or_init(|| {
 | 
				
			||||||
@ -39,45 +57,17 @@ pub fn build_schema(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // domains
 | 
					            // domains
 | 
				
			||||||
 | 
					            register_feeds_to_schema_context(&mut context);
 | 
				
			||||||
            register_subscribers_to_schema_context(&mut context);
 | 
					            register_subscribers_to_schema_context(&mut context);
 | 
				
			||||||
 | 
					            register_subscriptions_to_schema_context(&mut context);
 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                restrict_subscriber_for_entity::<downloaders::Entity>(
 | 
					 | 
				
			||||||
                    &mut context,
 | 
					 | 
				
			||||||
                    &downloaders::Column::SubscriberId,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                restrict_subscriber_for_entity::<downloads::Entity>(
 | 
					 | 
				
			||||||
                    &mut context,
 | 
					 | 
				
			||||||
                    &downloads::Column::SubscriberId,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                restrict_subscriber_for_entity::<episodes::Entity>(
 | 
					 | 
				
			||||||
                    &mut context,
 | 
					 | 
				
			||||||
                    &episodes::Column::SubscriberId,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                restrict_subscriber_for_entity::<subscriptions::Entity>(
 | 
					 | 
				
			||||||
                    &mut context,
 | 
					 | 
				
			||||||
                    &subscriptions::Column::SubscriberId,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                restrict_subscriber_for_entity::<subscribers::Entity>(
 | 
					 | 
				
			||||||
                    &mut context,
 | 
					 | 
				
			||||||
                    &subscribers::Column::Id,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                restrict_subscriber_for_entity::<subscription_bangumi::Entity>(
 | 
					 | 
				
			||||||
                    &mut context,
 | 
					 | 
				
			||||||
                    &subscription_bangumi::Column::SubscriberId,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                restrict_subscriber_for_entity::<subscription_episode::Entity>(
 | 
					 | 
				
			||||||
                    &mut context,
 | 
					 | 
				
			||||||
                    &subscription_episode::Column::SubscriberId,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                restrict_subscriber_for_entity::<credential_3rd::Entity>(
 | 
					 | 
				
			||||||
                    &mut context,
 | 
					 | 
				
			||||||
                    &credential_3rd::Column::SubscriberId,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            register_crypto_to_schema_context(&mut context, app_ctx.clone());
 | 
					 | 
				
			||||||
            register_subscriber_tasks_to_schema_context(&mut context);
 | 
					            register_subscriber_tasks_to_schema_context(&mut context);
 | 
				
			||||||
 | 
					            register_credential3rd_to_schema_context(&mut context, app_ctx.clone());
 | 
				
			||||||
 | 
					            register_downloaders_to_schema_context(&mut context);
 | 
				
			||||||
 | 
					            register_downloads_to_schema_context(&mut context);
 | 
				
			||||||
 | 
					            register_episodes_to_schema_context(&mut context);
 | 
				
			||||||
 | 
					            register_subscription_bangumi_to_schema_context(&mut context);
 | 
				
			||||||
 | 
					            register_subscription_episode_to_schema_context(&mut context);
 | 
				
			||||||
 | 
					            register_bangumi_to_schema_context(&mut context);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        context
 | 
					        context
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -91,32 +81,16 @@ pub fn build_schema(
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        // domains
 | 
					        // domains
 | 
				
			||||||
        builder = register_subscribers_to_schema_builder(builder);
 | 
					        builder = register_subscribers_to_schema_builder(builder);
 | 
				
			||||||
 | 
					        builder = register_feeds_to_schema_builder(builder);
 | 
				
			||||||
        seaography::register_entities!(
 | 
					        builder = register_episodes_to_schema_builder(builder);
 | 
				
			||||||
            builder,
 | 
					        builder = register_subscription_bangumi_to_schema_builder(builder);
 | 
				
			||||||
            [
 | 
					        builder = register_subscription_episode_to_schema_builder(builder);
 | 
				
			||||||
                bangumi,
 | 
					        builder = register_downloaders_to_schema_builder(builder);
 | 
				
			||||||
                downloaders,
 | 
					        builder = register_downloads_to_schema_builder(builder);
 | 
				
			||||||
                downloads,
 | 
					 | 
				
			||||||
                episodes,
 | 
					 | 
				
			||||||
                subscription_bangumi,
 | 
					 | 
				
			||||||
                subscription_episode,
 | 
					 | 
				
			||||||
                subscriptions,
 | 
					 | 
				
			||||||
                credential_3rd
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            builder.register_enumeration::<downloads::DownloadStatus>();
 | 
					 | 
				
			||||||
            builder.register_enumeration::<subscriptions::SubscriptionCategory>();
 | 
					 | 
				
			||||||
            builder.register_enumeration::<downloaders::DownloaderCategory>();
 | 
					 | 
				
			||||||
            builder.register_enumeration::<downloads::DownloadMime>();
 | 
					 | 
				
			||||||
            builder.register_enumeration::<credential_3rd::Credential3rdType>();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        builder = register_subscriptions_to_schema_builder(builder);
 | 
					        builder = register_subscriptions_to_schema_builder(builder);
 | 
				
			||||||
        builder = register_credential3rd_to_schema_builder(builder);
 | 
					        builder = register_credential3rd_to_schema_builder(builder);
 | 
				
			||||||
        builder = register_subscriber_tasks_to_schema_builder(builder);
 | 
					        builder = register_subscriber_tasks_to_schema_builder(builder);
 | 
				
			||||||
 | 
					        builder = register_bangumi_to_schema_builder(builder);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let schema = builder.schema_builder();
 | 
					    let schema = builder.schema_builder();
 | 
				
			||||||
 | 
				
			|||||||
@ -52,8 +52,12 @@ pub enum Bangumi {
 | 
				
			|||||||
    RssLink,
 | 
					    RssLink,
 | 
				
			||||||
    PosterLink,
 | 
					    PosterLink,
 | 
				
			||||||
    OriginPosterLink,
 | 
					    OriginPosterLink,
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @deprecated
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    SavePath,
 | 
					    SavePath,
 | 
				
			||||||
    Homepage,
 | 
					    Homepage,
 | 
				
			||||||
 | 
					    BangumiType,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(DeriveIden)]
 | 
					#[derive(DeriveIden)]
 | 
				
			||||||
@ -86,7 +90,11 @@ pub enum Episodes {
 | 
				
			|||||||
    Homepage,
 | 
					    Homepage,
 | 
				
			||||||
    Subtitle,
 | 
					    Subtitle,
 | 
				
			||||||
    Source,
 | 
					    Source,
 | 
				
			||||||
    Extra,
 | 
					    EpisodeType,
 | 
				
			||||||
 | 
					    EnclosureTorrentLink,
 | 
				
			||||||
 | 
					    EnclosureMagnetLink,
 | 
				
			||||||
 | 
					    EnclosurePubDate,
 | 
				
			||||||
 | 
					    EnclosureContentLength,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(DeriveIden)]
 | 
					#[derive(DeriveIden)]
 | 
				
			||||||
@ -149,6 +157,17 @@ pub enum Credential3rd {
 | 
				
			|||||||
    UserAgent,
 | 
					    UserAgent,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(DeriveIden)]
 | 
				
			||||||
 | 
					pub enum Feeds {
 | 
				
			||||||
 | 
					    Table,
 | 
				
			||||||
 | 
					    Id,
 | 
				
			||||||
 | 
					    Token,
 | 
				
			||||||
 | 
					    FeedType,
 | 
				
			||||||
 | 
					    FeedSource,
 | 
				
			||||||
 | 
					    SubscriberId,
 | 
				
			||||||
 | 
					    SubscriptionId,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
macro_rules! create_postgres_enum_for_active_enum {
 | 
					macro_rules! create_postgres_enum_for_active_enum {
 | 
				
			||||||
    ($manager: expr, $active_enum: expr, $($enum_value:expr),+) => {
 | 
					    ($manager: expr, $active_enum: expr, $($enum_value:expr),+) => {
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										98
									
								
								apps/recorder/src/migrations/m20250622_015618_feeds.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								apps/recorder/src/migrations/m20250622_015618_feeds.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,98 @@
 | 
				
			|||||||
 | 
					use async_trait::async_trait;
 | 
				
			||||||
 | 
					use sea_orm_migration::{
 | 
				
			||||||
 | 
					    prelude::*,
 | 
				
			||||||
 | 
					    schema::{enumeration, integer_null, pk_auto, text},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    migrations::defs::{
 | 
				
			||||||
 | 
					        CustomSchemaManagerExt, Feeds, GeneralIds, Subscribers, Subscriptions, table_auto_z,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    models::feeds::{FeedSource, FeedSourceEnum, FeedType, FeedTypeEnum},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(DeriveMigrationName)]
 | 
				
			||||||
 | 
					pub struct Migration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl MigrationTrait for Migration {
 | 
				
			||||||
 | 
					    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
 | 
				
			||||||
 | 
					        create_postgres_enum_for_active_enum!(manager, FeedTypeEnum, FeedType::Rss).await?;
 | 
				
			||||||
 | 
					        create_postgres_enum_for_active_enum!(
 | 
				
			||||||
 | 
					            manager,
 | 
				
			||||||
 | 
					            FeedSourceEnum,
 | 
				
			||||||
 | 
					            FeedSource::SubscriptionEpisode
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        manager
 | 
				
			||||||
 | 
					            .create_table(
 | 
				
			||||||
 | 
					                table_auto_z(Feeds::Table)
 | 
				
			||||||
 | 
					                    .col(pk_auto(Feeds::Id))
 | 
				
			||||||
 | 
					                    .col(text(Feeds::Token))
 | 
				
			||||||
 | 
					                    .col(enumeration(
 | 
				
			||||||
 | 
					                        Feeds::FeedType,
 | 
				
			||||||
 | 
					                        FeedTypeEnum,
 | 
				
			||||||
 | 
					                        FeedType::iden_values(),
 | 
				
			||||||
 | 
					                    ))
 | 
				
			||||||
 | 
					                    .col(
 | 
				
			||||||
 | 
					                        enumeration(Feeds::FeedSource, FeedSourceEnum, FeedSource::iden_values())
 | 
				
			||||||
 | 
					                            .not_null(),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .col(integer_null(Feeds::SubscriberId))
 | 
				
			||||||
 | 
					                    .col(integer_null(Feeds::SubscriptionId))
 | 
				
			||||||
 | 
					                    .index(
 | 
				
			||||||
 | 
					                        Index::create()
 | 
				
			||||||
 | 
					                            .if_not_exists()
 | 
				
			||||||
 | 
					                            .name("idx_feeds_token")
 | 
				
			||||||
 | 
					                            .table(Feeds::Table)
 | 
				
			||||||
 | 
					                            .col(Feeds::Token)
 | 
				
			||||||
 | 
					                            .unique(),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .foreign_key(
 | 
				
			||||||
 | 
					                        ForeignKey::create()
 | 
				
			||||||
 | 
					                            .name("fk_feeds_subscriber_id")
 | 
				
			||||||
 | 
					                            .from(Feeds::Table, Feeds::SubscriberId)
 | 
				
			||||||
 | 
					                            .to(Subscribers::Table, Subscribers::Id)
 | 
				
			||||||
 | 
					                            .on_update(ForeignKeyAction::Cascade)
 | 
				
			||||||
 | 
					                            .on_delete(ForeignKeyAction::Cascade),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .foreign_key(
 | 
				
			||||||
 | 
					                        ForeignKey::create()
 | 
				
			||||||
 | 
					                            .name("fk_feeds_subscription_id")
 | 
				
			||||||
 | 
					                            .from(Feeds::Table, Feeds::SubscriptionId)
 | 
				
			||||||
 | 
					                            .to(Subscriptions::Table, Subscriptions::Id)
 | 
				
			||||||
 | 
					                            .on_update(ForeignKeyAction::Cascade)
 | 
				
			||||||
 | 
					                            .on_delete(ForeignKeyAction::Cascade),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .to_owned(),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        manager
 | 
				
			||||||
 | 
					            .create_postgres_auto_update_ts_trigger_for_col(Feeds::Table, GeneralIds::UpdatedAt)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
 | 
				
			||||||
 | 
					        manager
 | 
				
			||||||
 | 
					            .drop_postgres_auto_update_ts_trigger_for_col(Feeds::Table, GeneralIds::UpdatedAt)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        manager
 | 
				
			||||||
 | 
					            .drop_table(Table::drop().if_exists().table(Feeds::Table).to_owned())
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        manager
 | 
				
			||||||
 | 
					            .drop_postgres_enum_for_active_enum(FeedTypeEnum)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        manager
 | 
				
			||||||
 | 
					            .drop_postgres_enum_for_active_enum(FeedSourceEnum)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,135 @@
 | 
				
			|||||||
 | 
					use async_trait::async_trait;
 | 
				
			||||||
 | 
					use sea_orm_migration::{
 | 
				
			||||||
 | 
					    prelude::*,
 | 
				
			||||||
 | 
					    schema::{
 | 
				
			||||||
 | 
					        enumeration, enumeration_null, integer_null, text_null, timestamp_with_time_zone_null,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    migrations::defs::{Bangumi, CustomSchemaManagerExt, Episodes},
 | 
				
			||||||
 | 
					    models::{
 | 
				
			||||||
 | 
					        bangumi::{BangumiType, BangumiTypeEnum},
 | 
				
			||||||
 | 
					        episodes::{EpisodeType, EpisodeTypeEnum},
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(DeriveMigrationName)]
 | 
				
			||||||
 | 
					pub struct Migration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl MigrationTrait for Migration {
 | 
				
			||||||
 | 
					    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
 | 
				
			||||||
 | 
					        create_postgres_enum_for_active_enum!(manager, EpisodeTypeEnum, EpisodeType::Mikan).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            create_postgres_enum_for_active_enum!(manager, BangumiTypeEnum, BangumiType::Mikan)
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					            manager
 | 
				
			||||||
 | 
					                .alter_table(
 | 
				
			||||||
 | 
					                    Table::alter()
 | 
				
			||||||
 | 
					                        .table(Bangumi::Table)
 | 
				
			||||||
 | 
					                        .add_column_if_not_exists(enumeration_null(
 | 
				
			||||||
 | 
					                            Bangumi::BangumiType,
 | 
				
			||||||
 | 
					                            BangumiTypeEnum,
 | 
				
			||||||
 | 
					                            BangumiType::iden_values(),
 | 
				
			||||||
 | 
					                        ))
 | 
				
			||||||
 | 
					                        .drop_column(Bangumi::SavePath)
 | 
				
			||||||
 | 
					                        .to_owned(),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            manager
 | 
				
			||||||
 | 
					                .exec_stmt(
 | 
				
			||||||
 | 
					                    UpdateStatement::new()
 | 
				
			||||||
 | 
					                        .table(Bangumi::Table)
 | 
				
			||||||
 | 
					                        .value(
 | 
				
			||||||
 | 
					                            Bangumi::BangumiType,
 | 
				
			||||||
 | 
					                            BangumiType::Mikan.as_enum(BangumiTypeEnum),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .and_where(Expr::col(Bangumi::BangumiType).is_null())
 | 
				
			||||||
 | 
					                        .and_where(Expr::col(Bangumi::MikanBangumiId).is_not_null())
 | 
				
			||||||
 | 
					                        .to_owned(),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            manager
 | 
				
			||||||
 | 
					                .alter_table(
 | 
				
			||||||
 | 
					                    Table::alter()
 | 
				
			||||||
 | 
					                        .table(Bangumi::Table)
 | 
				
			||||||
 | 
					                        .modify_column(enumeration(
 | 
				
			||||||
 | 
					                            Bangumi::BangumiType,
 | 
				
			||||||
 | 
					                            BangumiTypeEnum,
 | 
				
			||||||
 | 
					                            BangumiType::iden_values(),
 | 
				
			||||||
 | 
					                        ))
 | 
				
			||||||
 | 
					                        .to_owned(),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            create_postgres_enum_for_active_enum!(manager, EpisodeTypeEnum, EpisodeType::Mikan)
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            manager
 | 
				
			||||||
 | 
					                .alter_table(
 | 
				
			||||||
 | 
					                    Table::alter()
 | 
				
			||||||
 | 
					                        .table(Episodes::Table)
 | 
				
			||||||
 | 
					                        .add_column_if_not_exists(enumeration_null(
 | 
				
			||||||
 | 
					                            Episodes::EpisodeType,
 | 
				
			||||||
 | 
					                            EpisodeTypeEnum,
 | 
				
			||||||
 | 
					                            EpisodeType::enum_type_name(),
 | 
				
			||||||
 | 
					                        ))
 | 
				
			||||||
 | 
					                        .add_column_if_not_exists(text_null(Episodes::EnclosureMagnetLink))
 | 
				
			||||||
 | 
					                        .add_column_if_not_exists(text_null(Episodes::EnclosureTorrentLink))
 | 
				
			||||||
 | 
					                        .add_column_if_not_exists(timestamp_with_time_zone_null(
 | 
				
			||||||
 | 
					                            Episodes::EnclosurePubDate,
 | 
				
			||||||
 | 
					                        ))
 | 
				
			||||||
 | 
					                        .add_column_if_not_exists(integer_null(Episodes::EnclosureContentLength))
 | 
				
			||||||
 | 
					                        .to_owned(),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            manager
 | 
				
			||||||
 | 
					                .exec_stmt(
 | 
				
			||||||
 | 
					                    UpdateStatement::new()
 | 
				
			||||||
 | 
					                        .table(Episodes::Table)
 | 
				
			||||||
 | 
					                        .value(
 | 
				
			||||||
 | 
					                            Episodes::EpisodeType,
 | 
				
			||||||
 | 
					                            EpisodeType::Mikan.as_enum(EpisodeTypeEnum),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .and_where(Expr::col(Episodes::EpisodeType).is_null())
 | 
				
			||||||
 | 
					                        .and_where(Expr::col(Episodes::MikanEpisodeId).is_not_null())
 | 
				
			||||||
 | 
					                        .to_owned(),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            manager
 | 
				
			||||||
 | 
					                .alter_table(
 | 
				
			||||||
 | 
					                    Table::alter()
 | 
				
			||||||
 | 
					                        .table(Episodes::Table)
 | 
				
			||||||
 | 
					                        .modify_column(enumeration(
 | 
				
			||||||
 | 
					                            Episodes::EpisodeType,
 | 
				
			||||||
 | 
					                            EpisodeTypeEnum,
 | 
				
			||||||
 | 
					                            EpisodeType::enum_type_name(),
 | 
				
			||||||
 | 
					                        ))
 | 
				
			||||||
 | 
					                        .to_owned(),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
 | 
				
			||||||
 | 
					        manager
 | 
				
			||||||
 | 
					            .drop_postgres_enum_for_active_enum(BangumiTypeEnum)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        manager
 | 
				
			||||||
 | 
					            .drop_postgres_enum_for_active_enum(EpisodeTypeEnum)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -8,6 +8,8 @@ pub mod m20240224_082543_add_downloads;
 | 
				
			|||||||
pub mod m20241231_000001_auth;
 | 
					pub mod m20241231_000001_auth;
 | 
				
			||||||
pub mod m20250501_021523_credential_3rd;
 | 
					pub mod m20250501_021523_credential_3rd;
 | 
				
			||||||
pub mod m20250520_021135_subscriber_tasks;
 | 
					pub mod m20250520_021135_subscriber_tasks;
 | 
				
			||||||
 | 
					pub mod m20250622_015618_feeds;
 | 
				
			||||||
 | 
					pub mod m20250622_020819_bangumi_and_episode_type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct Migrator;
 | 
					pub struct Migrator;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -20,6 +22,8 @@ impl MigratorTrait for Migrator {
 | 
				
			|||||||
            Box::new(m20241231_000001_auth::Migration),
 | 
					            Box::new(m20241231_000001_auth::Migration),
 | 
				
			||||||
            Box::new(m20250501_021523_credential_3rd::Migration),
 | 
					            Box::new(m20250501_021523_credential_3rd::Migration),
 | 
				
			||||||
            Box::new(m20250520_021135_subscriber_tasks::Migration),
 | 
					            Box::new(m20250520_021135_subscriber_tasks::Migration),
 | 
				
			||||||
 | 
					            Box::new(m20250622_015618_feeds::Migration),
 | 
				
			||||||
 | 
					            Box::new(m20250622_020819_bangumi_and_episode_type::Migration),
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -29,7 +29,14 @@ pub struct BangumiFilter {
 | 
				
			|||||||
    pub group: Option<Vec<String>>,
 | 
					    pub group: Option<Vec<String>>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, SimpleObject)]
 | 
					#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
 | 
				
			||||||
 | 
					#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "bangumi_type")]
 | 
				
			||||||
 | 
					pub enum BangumiType {
 | 
				
			||||||
 | 
					    #[sea_orm(string_value = "mikan")]
 | 
				
			||||||
 | 
					    Mikan,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
 | 
				
			||||||
#[sea_orm(table_name = "bangumi")]
 | 
					#[sea_orm(table_name = "bangumi")]
 | 
				
			||||||
pub struct Model {
 | 
					pub struct Model {
 | 
				
			||||||
    #[sea_orm(default_expr = "Expr::current_timestamp()")]
 | 
					    #[sea_orm(default_expr = "Expr::current_timestamp()")]
 | 
				
			||||||
@ -39,6 +46,7 @@ pub struct Model {
 | 
				
			|||||||
    #[sea_orm(primary_key)]
 | 
					    #[sea_orm(primary_key)]
 | 
				
			||||||
    pub id: i32,
 | 
					    pub id: i32,
 | 
				
			||||||
    pub mikan_bangumi_id: Option<String>,
 | 
					    pub mikan_bangumi_id: Option<String>,
 | 
				
			||||||
 | 
					    pub bangumi_type: BangumiType,
 | 
				
			||||||
    pub subscriber_id: i32,
 | 
					    pub subscriber_id: i32,
 | 
				
			||||||
    pub display_name: String,
 | 
					    pub display_name: String,
 | 
				
			||||||
    pub origin_name: String,
 | 
					    pub origin_name: String,
 | 
				
			||||||
@ -50,7 +58,6 @@ pub struct Model {
 | 
				
			|||||||
    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 origin_poster_link: Option<String>,
 | 
				
			||||||
    pub save_path: Option<String>,
 | 
					 | 
				
			||||||
    pub homepage: Option<String>,
 | 
					    pub homepage: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -152,10 +159,7 @@ impl ActiveModel {
 | 
				
			|||||||
            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(
 | 
					            origin_poster_link: ActiveValue::Set(meta.origin_poster_src.map(|src| src.to_string())),
 | 
				
			||||||
                meta.origin_poster_src
 | 
					 | 
				
			||||||
                    .map(|src| src[url::Position::BeforePath..].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()
 | 
				
			||||||
@ -234,6 +238,7 @@ impl Model {
 | 
				
			|||||||
                        Column::OriginName,
 | 
					                        Column::OriginName,
 | 
				
			||||||
                        Column::Fansub,
 | 
					                        Column::Fansub,
 | 
				
			||||||
                        Column::PosterLink,
 | 
					                        Column::PosterLink,
 | 
				
			||||||
 | 
					                        Column::OriginPosterLink,
 | 
				
			||||||
                        Column::Season,
 | 
					                        Column::Season,
 | 
				
			||||||
                        Column::SeasonRaw,
 | 
					                        Column::SeasonRaw,
 | 
				
			||||||
                        Column::RssLink,
 | 
					                        Column::RssLink,
 | 
				
			||||||
 | 
				
			|||||||
@ -9,11 +9,19 @@ use crate::{
 | 
				
			|||||||
    app::AppContextTrait,
 | 
					    app::AppContextTrait,
 | 
				
			||||||
    errors::RecorderResult,
 | 
					    errors::RecorderResult,
 | 
				
			||||||
    extract::{
 | 
					    extract::{
 | 
				
			||||||
 | 
					        bittorrent::EpisodeEnclosureMeta,
 | 
				
			||||||
        mikan::{MikanEpisodeHash, MikanEpisodeMeta, build_mikan_episode_homepage_url},
 | 
					        mikan::{MikanEpisodeHash, MikanEpisodeMeta, build_mikan_episode_homepage_url},
 | 
				
			||||||
        origin::{OriginCompTrait, OriginNameRoot},
 | 
					        origin::{OriginCompTrait, OriginNameRoot},
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
 | 
				
			||||||
 | 
					#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "episode_type")]
 | 
				
			||||||
 | 
					pub enum EpisodeType {
 | 
				
			||||||
 | 
					    #[sea_orm(string_value = "mikan")]
 | 
				
			||||||
 | 
					    Mikan,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
 | 
					#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
 | 
				
			||||||
#[sea_orm(table_name = "episodes")]
 | 
					#[sea_orm(table_name = "episodes")]
 | 
				
			||||||
pub struct Model {
 | 
					pub struct Model {
 | 
				
			||||||
@ -25,11 +33,15 @@ 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 enclosure_torrent_link: Option<String>,
 | 
				
			||||||
 | 
					    pub enclosure_magnet_link: Option<String>,
 | 
				
			||||||
 | 
					    pub enclosure_pub_date: Option<DateTimeUtc>,
 | 
				
			||||||
 | 
					    pub enclosure_content_length: Option<u64>,
 | 
				
			||||||
 | 
					    pub episode_type: EpisodeType,
 | 
				
			||||||
    pub origin_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,
 | 
				
			||||||
    pub save_path: Option<String>,
 | 
					 | 
				
			||||||
    pub resolution: Option<String>,
 | 
					    pub resolution: Option<String>,
 | 
				
			||||||
    pub season: i32,
 | 
					    pub season: i32,
 | 
				
			||||||
    pub season_raw: Option<String>,
 | 
					    pub season_raw: Option<String>,
 | 
				
			||||||
@ -122,6 +134,7 @@ impl ActiveModel {
 | 
				
			|||||||
        ctx: &dyn AppContextTrait,
 | 
					        ctx: &dyn AppContextTrait,
 | 
				
			||||||
        bangumi: &bangumi::Model,
 | 
					        bangumi: &bangumi::Model,
 | 
				
			||||||
        episode: MikanEpisodeMeta,
 | 
					        episode: MikanEpisodeMeta,
 | 
				
			||||||
 | 
					        enclosure_meta: EpisodeEnclosureMeta,
 | 
				
			||||||
    ) -> 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 = OriginNameRoot::parse_comp(&episode.episode_title)
 | 
					        let episode_extention_meta = OriginNameRoot::parse_comp(&episode.episode_title)
 | 
				
			||||||
@ -149,6 +162,10 @@ impl ActiveModel {
 | 
				
			|||||||
            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()),
 | 
					            origin_poster_link: ActiveValue::Set(bangumi.origin_poster_link.clone()),
 | 
				
			||||||
            episode_index: ActiveValue::Set(0),
 | 
					            episode_index: ActiveValue::Set(0),
 | 
				
			||||||
 | 
					            enclosure_torrent_link: ActiveValue::Set(enclosure_meta.torrent_link),
 | 
				
			||||||
 | 
					            enclosure_magnet_link: ActiveValue::Set(enclosure_meta.magnet_link),
 | 
				
			||||||
 | 
					            enclosure_pub_date: ActiveValue::Set(enclosure_meta.pub_date),
 | 
				
			||||||
 | 
					            enclosure_content_length: ActiveValue::Set(enclosure_meta.content_length),
 | 
				
			||||||
            ..Default::default()
 | 
					            ..Default::default()
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -216,14 +233,19 @@ impl Model {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    pub async fn add_mikan_episodes_for_subscription(
 | 
					    pub async fn add_mikan_episodes_for_subscription(
 | 
				
			||||||
        ctx: &dyn AppContextTrait,
 | 
					        ctx: &dyn AppContextTrait,
 | 
				
			||||||
        creations: impl Iterator<Item = (&bangumi::Model, MikanEpisodeMeta)>,
 | 
					        creations: impl Iterator<Item = (&bangumi::Model, MikanEpisodeMeta, EpisodeEnclosureMeta)>,
 | 
				
			||||||
        subscriber_id: i32,
 | 
					        subscriber_id: i32,
 | 
				
			||||||
        subscription_id: i32,
 | 
					        subscription_id: i32,
 | 
				
			||||||
    ) -> RecorderResult<()> {
 | 
					    ) -> RecorderResult<()> {
 | 
				
			||||||
        let db = ctx.db();
 | 
					        let db = ctx.db();
 | 
				
			||||||
        let new_episode_active_modes: Vec<ActiveModel> = creations
 | 
					        let new_episode_active_modes: Vec<ActiveModel> = creations
 | 
				
			||||||
            .map(|(bangumi, episode_meta)| {
 | 
					            .map(|(bangumi, episode_meta, enclosure_meta)| {
 | 
				
			||||||
                ActiveModel::from_mikan_bangumi_and_episode_meta(ctx, bangumi, episode_meta)
 | 
					                ActiveModel::from_mikan_bangumi_and_episode_meta(
 | 
				
			||||||
 | 
					                    ctx,
 | 
				
			||||||
 | 
					                    bangumi,
 | 
				
			||||||
 | 
					                    episode_meta,
 | 
				
			||||||
 | 
					                    enclosure_meta,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .collect::<Result<_, _>>()?;
 | 
					            .collect::<Result<_, _>>()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -234,7 +256,23 @@ impl Model {
 | 
				
			|||||||
        let new_episode_ids = Entity::insert_many(new_episode_active_modes)
 | 
					        let new_episode_ids = Entity::insert_many(new_episode_active_modes)
 | 
				
			||||||
            .on_conflict(
 | 
					            .on_conflict(
 | 
				
			||||||
                OnConflict::columns([Column::MikanEpisodeId, Column::SubscriberId])
 | 
					                OnConflict::columns([Column::MikanEpisodeId, Column::SubscriberId])
 | 
				
			||||||
                    .update_columns([Column::OriginName, Column::PosterLink, Column::Homepage])
 | 
					                    .update_columns([
 | 
				
			||||||
 | 
					                        Column::OriginName,
 | 
				
			||||||
 | 
					                        Column::PosterLink,
 | 
				
			||||||
 | 
					                        Column::OriginPosterLink,
 | 
				
			||||||
 | 
					                        Column::Homepage,
 | 
				
			||||||
 | 
					                        Column::EnclosureContentLength,
 | 
				
			||||||
 | 
					                        Column::EnclosurePubDate,
 | 
				
			||||||
 | 
					                        Column::EnclosureTorrentLink,
 | 
				
			||||||
 | 
					                        Column::EnclosureMagnetLink,
 | 
				
			||||||
 | 
					                        Column::EpisodeIndex,
 | 
				
			||||||
 | 
					                        Column::Subtitle,
 | 
				
			||||||
 | 
					                        Column::Source,
 | 
				
			||||||
 | 
					                        Column::Resolution,
 | 
				
			||||||
 | 
					                        Column::Season,
 | 
				
			||||||
 | 
					                        Column::SeasonRaw,
 | 
				
			||||||
 | 
					                        Column::Fansub,
 | 
				
			||||||
 | 
					                    ])
 | 
				
			||||||
                    .to_owned(),
 | 
					                    .to_owned(),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .exec_with_returning_columns(db, [Column::Id])
 | 
					            .exec_with_returning_columns(db, [Column::Id])
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										131
									
								
								apps/recorder/src/models/feeds/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								apps/recorder/src/models/feeds/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,131 @@
 | 
				
			|||||||
 | 
					mod registry;
 | 
				
			||||||
 | 
					mod rss;
 | 
				
			||||||
 | 
					mod subscription_episodes_feed;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use ::rss::Channel;
 | 
				
			||||||
 | 
					use async_trait::async_trait;
 | 
				
			||||||
 | 
					pub use registry::Feed;
 | 
				
			||||||
 | 
					pub use rss::{RssFeedItemTrait, RssFeedTrait};
 | 
				
			||||||
 | 
					use sea_orm::{ActiveValue, DeriveEntityModel, entity::prelude::*};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					pub use subscription_episodes_feed::SubscriptionEpisodesFeed;
 | 
				
			||||||
 | 
					use url::Url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    app::AppContextTrait,
 | 
				
			||||||
 | 
					    errors::{RecorderError, RecorderResult},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(
 | 
				
			||||||
 | 
					    Debug, Serialize, Deserialize, Clone, PartialEq, Eq, DeriveActiveEnum, EnumIter, DeriveDisplay,
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "feed_type")]
 | 
				
			||||||
 | 
					#[serde(rename_all = "snake_case")]
 | 
				
			||||||
 | 
					pub enum FeedType {
 | 
				
			||||||
 | 
					    #[sea_orm(string_value = "rss")]
 | 
				
			||||||
 | 
					    Rss,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(
 | 
				
			||||||
 | 
					    Debug, Serialize, Deserialize, Clone, PartialEq, Eq, DeriveActiveEnum, EnumIter, DeriveDisplay,
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "feed_source")]
 | 
				
			||||||
 | 
					#[serde(rename_all = "snake_case")]
 | 
				
			||||||
 | 
					pub enum FeedSource {
 | 
				
			||||||
 | 
					    #[sea_orm(string_value = "subscription_episode")]
 | 
				
			||||||
 | 
					    SubscriptionEpisode,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, DeriveEntityModel)]
 | 
				
			||||||
 | 
					#[sea_orm(table_name = "feeds")]
 | 
				
			||||||
 | 
					pub struct Model {
 | 
				
			||||||
 | 
					    pub created_at: DateTimeUtc,
 | 
				
			||||||
 | 
					    pub updated_at: DateTimeUtc,
 | 
				
			||||||
 | 
					    #[sea_orm(primary_key)]
 | 
				
			||||||
 | 
					    pub id: i32,
 | 
				
			||||||
 | 
					    #[sea_orm(indexed)]
 | 
				
			||||||
 | 
					    pub token: String,
 | 
				
			||||||
 | 
					    #[sea_orm(indexed)]
 | 
				
			||||||
 | 
					    pub feed_type: FeedType,
 | 
				
			||||||
 | 
					    #[sea_orm(indexed)]
 | 
				
			||||||
 | 
					    pub feed_source: FeedSource,
 | 
				
			||||||
 | 
					    pub subscriber_id: Option<i32>,
 | 
				
			||||||
 | 
					    pub subscription_id: Option<i32>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
				
			||||||
 | 
					pub enum Relation {
 | 
				
			||||||
 | 
					    #[sea_orm(
 | 
				
			||||||
 | 
					        belongs_to = "super::subscriptions::Entity",
 | 
				
			||||||
 | 
					        from = "Column::SubscriptionId",
 | 
				
			||||||
 | 
					        to = "super::subscriptions::Column::Id",
 | 
				
			||||||
 | 
					        on_update = "Cascade",
 | 
				
			||||||
 | 
					        on_delete = "Cascade"
 | 
				
			||||||
 | 
					    )]
 | 
				
			||||||
 | 
					    Subscription,
 | 
				
			||||||
 | 
					    #[sea_orm(
 | 
				
			||||||
 | 
					        belongs_to = "super::subscribers::Entity",
 | 
				
			||||||
 | 
					        from = "Column::SubscriberId",
 | 
				
			||||||
 | 
					        to = "super::subscribers::Column::Id",
 | 
				
			||||||
 | 
					        on_update = "Cascade",
 | 
				
			||||||
 | 
					        on_delete = "Cascade"
 | 
				
			||||||
 | 
					    )]
 | 
				
			||||||
 | 
					    Subscriber,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Related<super::subscriptions::Entity> for Entity {
 | 
				
			||||||
 | 
					    fn to() -> RelationDef {
 | 
				
			||||||
 | 
					        Relation::Subscription.def()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Related<super::subscribers::Entity> for Entity {
 | 
				
			||||||
 | 
					    fn to() -> RelationDef {
 | 
				
			||||||
 | 
					        Relation::Subscriber.def()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
 | 
				
			||||||
 | 
					pub enum RelatedEntity {
 | 
				
			||||||
 | 
					    #[sea_orm(entity = "super::subscribers::Entity")]
 | 
				
			||||||
 | 
					    Subscriber,
 | 
				
			||||||
 | 
					    #[sea_orm(entity = "super::subscriptions::Entity")]
 | 
				
			||||||
 | 
					    Subscription,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl ActiveModelBehavior for ActiveModel {
 | 
				
			||||||
 | 
					    async fn before_save<C>(mut self, _db: &C, insert: bool) -> Result<Self, DbErr>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        C: ConnectionTrait,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if insert && let ActiveValue::NotSet = self.token {
 | 
				
			||||||
 | 
					            let token = nanoid::nanoid!(10);
 | 
				
			||||||
 | 
					            self.token = ActiveValue::Set(token);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Ok(self)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Model {
 | 
				
			||||||
 | 
					    pub async fn find_rss_feed_by_token(
 | 
				
			||||||
 | 
					        ctx: &dyn AppContextTrait,
 | 
				
			||||||
 | 
					        token: &str,
 | 
				
			||||||
 | 
					        api_base: &Url,
 | 
				
			||||||
 | 
					    ) -> RecorderResult<Channel> {
 | 
				
			||||||
 | 
					        let db = ctx.db();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let feed_model = Entity::find()
 | 
				
			||||||
 | 
					            .filter(Column::Token.eq(token))
 | 
				
			||||||
 | 
					            .filter(Column::FeedType.eq(FeedType::Rss))
 | 
				
			||||||
 | 
					            .one(db)
 | 
				
			||||||
 | 
					            .await?
 | 
				
			||||||
 | 
					            .ok_or(RecorderError::ModelEntityNotFound {
 | 
				
			||||||
 | 
					                entity: "Feed".into(),
 | 
				
			||||||
 | 
					            })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let feed = Feed::from_model(ctx, feed_model).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        feed.into_rss_channel(ctx, api_base)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										54
									
								
								apps/recorder/src/models/feeds/registry.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								apps/recorder/src/models/feeds/registry.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					use rss::Channel;
 | 
				
			||||||
 | 
					use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
 | 
				
			||||||
 | 
					use url::Url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    app::AppContextTrait,
 | 
				
			||||||
 | 
					    errors::{RecorderError, RecorderResult},
 | 
				
			||||||
 | 
					    models::{
 | 
				
			||||||
 | 
					        episodes,
 | 
				
			||||||
 | 
					        feeds::{self, FeedSource, RssFeedTrait, SubscriptionEpisodesFeed},
 | 
				
			||||||
 | 
					        subscriptions,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub enum Feed {
 | 
				
			||||||
 | 
					    SubscritpionEpisodes(SubscriptionEpisodesFeed),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Feed {
 | 
				
			||||||
 | 
					    pub async fn from_model(ctx: &dyn AppContextTrait, m: feeds::Model) -> RecorderResult<Self> {
 | 
				
			||||||
 | 
					        match m.feed_source {
 | 
				
			||||||
 | 
					            FeedSource::SubscriptionEpisode => {
 | 
				
			||||||
 | 
					                let db = ctx.db();
 | 
				
			||||||
 | 
					                let (subscription, episodes) = if let Some(subscription_id) = m.subscription_id
 | 
				
			||||||
 | 
					                    && let Some((subscription, episodes)) = subscriptions::Entity::find()
 | 
				
			||||||
 | 
					                        .filter(subscriptions::Column::Id.eq(subscription_id))
 | 
				
			||||||
 | 
					                        .find_with_related(episodes::Entity)
 | 
				
			||||||
 | 
					                        .all(db)
 | 
				
			||||||
 | 
					                        .await?
 | 
				
			||||||
 | 
					                        .pop()
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    (subscription, episodes)
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    return Err(RecorderError::ModelEntityNotFound {
 | 
				
			||||||
 | 
					                        entity: "Subscription".into(),
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                Ok(Feed::SubscritpionEpisodes(
 | 
				
			||||||
 | 
					                    SubscriptionEpisodesFeed::from_model(m, subscription, episodes),
 | 
				
			||||||
 | 
					                ))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn into_rss_channel(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        ctx: &dyn AppContextTrait,
 | 
				
			||||||
 | 
					        api_base: &Url,
 | 
				
			||||||
 | 
					    ) -> RecorderResult<Channel> {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::SubscritpionEpisodes(feed) => feed.into_channel(ctx, api_base),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										142
									
								
								apps/recorder/src/models/feeds/rss.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								apps/recorder/src/models/feeds/rss.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,142 @@
 | 
				
			|||||||
 | 
					use std::borrow::Cow;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono::{DateTime, Utc};
 | 
				
			||||||
 | 
					use downloader::bittorrent::BITTORRENT_MIME_TYPE;
 | 
				
			||||||
 | 
					use maplit::btreemap;
 | 
				
			||||||
 | 
					use rss::{
 | 
				
			||||||
 | 
					    Channel, ChannelBuilder, EnclosureBuilder, GuidBuilder, Item, ItemBuilder,
 | 
				
			||||||
 | 
					    extension::{ExtensionBuilder, ExtensionMap},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use url::Url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    app::AppContextTrait,
 | 
				
			||||||
 | 
					    errors::{RecorderError, RecorderResult},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub trait RssFeedItemTrait: Sized {
 | 
				
			||||||
 | 
					    fn get_guid_value(&self) -> Cow<'_, str>;
 | 
				
			||||||
 | 
					    fn get_title(&self) -> Cow<'_, str>;
 | 
				
			||||||
 | 
					    fn get_description(&self) -> Cow<'_, str>;
 | 
				
			||||||
 | 
					    fn get_link(&self, ctx: &dyn AppContextTrait, api_base: &Url) -> Option<Cow<'_, str>>;
 | 
				
			||||||
 | 
					    fn get_enclosure_mime(&self) -> Option<Cow<'_, str>>;
 | 
				
			||||||
 | 
					    fn get_enclosure_link(&self, ctx: &dyn AppContextTrait, api_base: &Url)
 | 
				
			||||||
 | 
					    -> Option<Cow<'_, str>>;
 | 
				
			||||||
 | 
					    fn get_enclosure_pub_date(&self) -> Option<DateTime<Utc>>;
 | 
				
			||||||
 | 
					    fn get_enclosure_content_length(&self) -> Option<u64>;
 | 
				
			||||||
 | 
					    fn into_item(self, ctx: &dyn AppContextTrait, api_base: &Url) -> RecorderResult<Item> {
 | 
				
			||||||
 | 
					        let enclosure_mime_type =
 | 
				
			||||||
 | 
					            self.get_enclosure_mime()
 | 
				
			||||||
 | 
					                .ok_or_else(|| RecorderError::MikanRssInvalidFieldError {
 | 
				
			||||||
 | 
					                    field: "enclosure_mime_type".into(),
 | 
				
			||||||
 | 
					                    source: None.into(),
 | 
				
			||||||
 | 
					                })?;
 | 
				
			||||||
 | 
					        let enclosure_link = self.get_enclosure_link(ctx, api_base).ok_or_else(|| {
 | 
				
			||||||
 | 
					            RecorderError::MikanRssInvalidFieldError {
 | 
				
			||||||
 | 
					                field: "enclosure_link".into(),
 | 
				
			||||||
 | 
					                source: None.into(),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
 | 
					        let enclosure_content_length = self.get_enclosure_content_length().ok_or_else(|| {
 | 
				
			||||||
 | 
					            RecorderError::MikanRssInvalidFieldError {
 | 
				
			||||||
 | 
					                field: "enclosure_content_length".into(),
 | 
				
			||||||
 | 
					                source: None.into(),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
 | 
					        let enclosure_pub_date = self.get_enclosure_pub_date().ok_or_else(|| {
 | 
				
			||||||
 | 
					            RecorderError::MikanRssInvalidFieldError {
 | 
				
			||||||
 | 
					                field: "enclosure_pub_date".into(),
 | 
				
			||||||
 | 
					                source: None.into(),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
 | 
					        let link = self.get_link(ctx, api_base).ok_or_else(|| {
 | 
				
			||||||
 | 
					            RecorderError::MikanRssInvalidFieldError {
 | 
				
			||||||
 | 
					                field: "link".into(),
 | 
				
			||||||
 | 
					                source: None.into(),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut extensions = ExtensionMap::default();
 | 
				
			||||||
 | 
					        if enclosure_mime_type == BITTORRENT_MIME_TYPE {
 | 
				
			||||||
 | 
					            extensions.insert(
 | 
				
			||||||
 | 
					                "torrent".to_string(),
 | 
				
			||||||
 | 
					                btreemap! {
 | 
				
			||||||
 | 
					                    "link".to_string() => vec![
 | 
				
			||||||
 | 
					                        ExtensionBuilder::default().name(
 | 
				
			||||||
 | 
					                        "link"
 | 
				
			||||||
 | 
					                        ).value(enclosure_link.to_string()).build()
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    "contentLength".to_string() => vec![
 | 
				
			||||||
 | 
					                        ExtensionBuilder::default().name(
 | 
				
			||||||
 | 
					                            "contentLength"
 | 
				
			||||||
 | 
					                        ).value(enclosure_content_length.to_string()).build()
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    "pubDate".to_string() => vec![
 | 
				
			||||||
 | 
					                        ExtensionBuilder::default().name(
 | 
				
			||||||
 | 
					                            "pubDate"
 | 
				
			||||||
 | 
					                        ).value(enclosure_pub_date.to_rfc3339()).build()
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let enclosure = EnclosureBuilder::default()
 | 
				
			||||||
 | 
					            .mime_type(enclosure_mime_type)
 | 
				
			||||||
 | 
					            .url(enclosure_link.to_string())
 | 
				
			||||||
 | 
					            .length(enclosure_content_length.to_string())
 | 
				
			||||||
 | 
					            .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let guid = GuidBuilder::default()
 | 
				
			||||||
 | 
					            .value(self.get_guid_value())
 | 
				
			||||||
 | 
					            .permalink(false)
 | 
				
			||||||
 | 
					            .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let item = ItemBuilder::default()
 | 
				
			||||||
 | 
					            .guid(guid)
 | 
				
			||||||
 | 
					            .title(self.get_title().to_string())
 | 
				
			||||||
 | 
					            .description(self.get_description().to_string())
 | 
				
			||||||
 | 
					            .link(link.to_string())
 | 
				
			||||||
 | 
					            .enclosure(enclosure)
 | 
				
			||||||
 | 
					            .pub_date(enclosure_pub_date.to_rfc3339())
 | 
				
			||||||
 | 
					            .extensions(extensions)
 | 
				
			||||||
 | 
					            .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(item)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub trait RssFeedTrait: Sized {
 | 
				
			||||||
 | 
					    type Item: RssFeedItemTrait;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_description(&self) -> Cow<'_, str>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_title(&self) -> Cow<'_, str>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_link(&self, ctx: &dyn AppContextTrait, api_base: &Url) -> Option<Cow<'_, str>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn items(&self) -> impl Iterator<Item = &Self::Item>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn into_items(self) -> impl Iterator<Item = Self::Item>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn into_channel(self, ctx: &dyn AppContextTrait, api_base: &Url) -> RecorderResult<Channel> {
 | 
				
			||||||
 | 
					        let link = self.get_link(ctx, api_base).ok_or_else(|| {
 | 
				
			||||||
 | 
					            RecorderError::MikanRssInvalidFieldError {
 | 
				
			||||||
 | 
					                field: "link".into(),
 | 
				
			||||||
 | 
					                source: None.into(),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let channel = ChannelBuilder::default()
 | 
				
			||||||
 | 
					            .title(self.get_title())
 | 
				
			||||||
 | 
					            .link(link.to_string())
 | 
				
			||||||
 | 
					            .description(self.get_description())
 | 
				
			||||||
 | 
					            .items({
 | 
				
			||||||
 | 
					                self.into_items()
 | 
				
			||||||
 | 
					                    .map(|item| item.into_item(ctx, api_base))
 | 
				
			||||||
 | 
					                    .collect::<RecorderResult<Vec<_>>>()?
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(channel)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										114
									
								
								apps/recorder/src/models/feeds/subscription_episodes_feed.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								apps/recorder/src/models/feeds/subscription_episodes_feed.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,114 @@
 | 
				
			|||||||
 | 
					use std::borrow::Cow;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono::{DateTime, Utc};
 | 
				
			||||||
 | 
					use downloader::bittorrent::BITTORRENT_MIME_TYPE;
 | 
				
			||||||
 | 
					use url::Url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    app::{AppContextTrait, PROJECT_NAME},
 | 
				
			||||||
 | 
					    models::{
 | 
				
			||||||
 | 
					        episodes,
 | 
				
			||||||
 | 
					        feeds::{
 | 
				
			||||||
 | 
					            self,
 | 
				
			||||||
 | 
					            rss::{RssFeedItemTrait, RssFeedTrait},
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        subscriptions,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    web::controller,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct SubscriptionEpisodesFeed {
 | 
				
			||||||
 | 
					    pub feed: feeds::Model,
 | 
				
			||||||
 | 
					    pub subscription: subscriptions::Model,
 | 
				
			||||||
 | 
					    pub episodes: Vec<episodes::Model>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl SubscriptionEpisodesFeed {
 | 
				
			||||||
 | 
					    pub fn from_model(
 | 
				
			||||||
 | 
					        feed: feeds::Model,
 | 
				
			||||||
 | 
					        subscription: subscriptions::Model,
 | 
				
			||||||
 | 
					        episodes: Vec<episodes::Model>,
 | 
				
			||||||
 | 
					    ) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            feed,
 | 
				
			||||||
 | 
					            subscription,
 | 
				
			||||||
 | 
					            episodes,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl RssFeedItemTrait for episodes::Model {
 | 
				
			||||||
 | 
					    fn get_guid_value(&self) -> Cow<'_, str> {
 | 
				
			||||||
 | 
					        Cow::Owned(format!("{PROJECT_NAME}:episode:{}", self.id))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_title(&self) -> Cow<'_, str> {
 | 
				
			||||||
 | 
					        Cow::Borrowed(&self.display_name)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_description(&self) -> Cow<'_, str> {
 | 
				
			||||||
 | 
					        Cow::Borrowed(&self.display_name)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_link(&self, _ctx: &dyn AppContextTrait, _api_base: &Url) -> Option<Cow<'_, str>> {
 | 
				
			||||||
 | 
					        self.homepage.as_deref().map(Cow::Borrowed)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_enclosure_mime(&self) -> Option<Cow<'_, str>> {
 | 
				
			||||||
 | 
					        if self.enclosure_torrent_link.is_some() {
 | 
				
			||||||
 | 
					            Some(Cow::Borrowed(BITTORRENT_MIME_TYPE))
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_enclosure_link(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        _ctx: &dyn AppContextTrait,
 | 
				
			||||||
 | 
					        _api_base: &Url,
 | 
				
			||||||
 | 
					    ) -> Option<Cow<'_, str>> {
 | 
				
			||||||
 | 
					        self.enclosure_torrent_link.as_deref().map(Cow::Borrowed)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_enclosure_pub_date(&self) -> Option<DateTime<Utc>> {
 | 
				
			||||||
 | 
					        self.enclosure_pub_date
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_enclosure_content_length(&self) -> Option<u64> {
 | 
				
			||||||
 | 
					        self.enclosure_content_length
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl RssFeedTrait for SubscriptionEpisodesFeed {
 | 
				
			||||||
 | 
					    type Item = episodes::Model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_description(&self) -> Cow<'_, str> {
 | 
				
			||||||
 | 
					        Cow::Owned(format!(
 | 
				
			||||||
 | 
					            "{PROJECT_NAME} - episodes of subscription \"{}\"",
 | 
				
			||||||
 | 
					            self.subscription.display_name
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_title(&self) -> Cow<'_, str> {
 | 
				
			||||||
 | 
					        Cow::Owned(format!("{PROJECT_NAME} - subscription episodes"))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_link(&self, _ctx: &dyn AppContextTrait, api_base: &Url) -> Option<Cow<'_, str>> {
 | 
				
			||||||
 | 
					        let api_base = api_base
 | 
				
			||||||
 | 
					            .join(&format!(
 | 
				
			||||||
 | 
					                "{}/{}",
 | 
				
			||||||
 | 
					                controller::feeds::CONTROLLER_PREFIX,
 | 
				
			||||||
 | 
					                self.feed.token
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					            .ok()?;
 | 
				
			||||||
 | 
					        Some(Cow::Owned(api_base.to_string()))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn items(&self) -> impl Iterator<Item = &Self::Item> {
 | 
				
			||||||
 | 
					        self.episodes.iter()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn into_items(self) -> impl Iterator<Item = Self::Item> {
 | 
				
			||||||
 | 
					        self.episodes.into_iter()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -4,6 +4,7 @@ pub mod credential_3rd;
 | 
				
			|||||||
pub mod downloaders;
 | 
					pub mod downloaders;
 | 
				
			||||||
pub mod downloads;
 | 
					pub mod downloads;
 | 
				
			||||||
pub mod episodes;
 | 
					pub mod episodes;
 | 
				
			||||||
 | 
					pub mod feeds;
 | 
				
			||||||
pub mod query;
 | 
					pub mod query;
 | 
				
			||||||
pub mod subscriber_tasks;
 | 
					pub mod subscriber_tasks;
 | 
				
			||||||
pub mod subscribers;
 | 
					pub mod subscribers;
 | 
				
			||||||
 | 
				
			|||||||
@ -3,11 +3,11 @@ use sea_orm::{ActiveValue, FromJsonQueryResult, TransactionTrait, entity::prelud
 | 
				
			|||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    app::AppContextTrait,
 | 
					    app::{AppContextTrait, PROJECT_NAME},
 | 
				
			||||||
    errors::app_error::{RecorderError, RecorderResult},
 | 
					    errors::app_error::{RecorderError, RecorderResult},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub const SEED_SUBSCRIBER: &str = "konobangu";
 | 
					pub const SEED_SUBSCRIBER: &str = PROJECT_NAME;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
 | 
					#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
 | 
				
			||||||
pub struct SubscriberBangumiConfig {
 | 
					pub struct SubscriberBangumiConfig {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,311 +0,0 @@
 | 
				
			|||||||
use std::{fmt::Debug, sync::Arc};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use async_trait::async_trait;
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    app::AppContextTrait,
 | 
					 | 
				
			||||||
    errors::{RecorderError, RecorderResult},
 | 
					 | 
				
			||||||
    extract::mikan::{
 | 
					 | 
				
			||||||
        MikanBangumiSubscription, MikanSeasonSubscription, MikanSubscriberSubscription,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(
 | 
					 | 
				
			||||||
    Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, DeriveDisplay,
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
#[sea_orm(
 | 
					 | 
				
			||||||
    rs_type = "String",
 | 
					 | 
				
			||||||
    db_type = "Enum",
 | 
					 | 
				
			||||||
    enum_name = "subscription_category"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
#[serde(rename_all = "snake_case")]
 | 
					 | 
				
			||||||
pub enum SubscriptionCategory {
 | 
					 | 
				
			||||||
    #[sea_orm(string_value = "mikan_subscriber")]
 | 
					 | 
				
			||||||
    MikanSubscriber,
 | 
					 | 
				
			||||||
    #[sea_orm(string_value = "mikan_season")]
 | 
					 | 
				
			||||||
    MikanSeason,
 | 
					 | 
				
			||||||
    #[sea_orm(string_value = "mikan_bangumi")]
 | 
					 | 
				
			||||||
    MikanBangumi,
 | 
					 | 
				
			||||||
    #[sea_orm(string_value = "manual")]
 | 
					 | 
				
			||||||
    Manual,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "subscriptions")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(default_expr = "Expr::current_timestamp()")]
 | 
					 | 
				
			||||||
    pub created_at: DateTimeUtc,
 | 
					 | 
				
			||||||
    #[sea_orm(default_expr = "Expr::current_timestamp()")]
 | 
					 | 
				
			||||||
    pub updated_at: DateTimeUtc,
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key)]
 | 
					 | 
				
			||||||
    pub id: i32,
 | 
					 | 
				
			||||||
    pub display_name: String,
 | 
					 | 
				
			||||||
    pub subscriber_id: i32,
 | 
					 | 
				
			||||||
    pub category: SubscriptionCategory,
 | 
					 | 
				
			||||||
    pub source_url: String,
 | 
					 | 
				
			||||||
    pub enabled: bool,
 | 
					 | 
				
			||||||
    pub credential_id: Option<i32>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::subscribers::Entity",
 | 
					 | 
				
			||||||
        from = "Column::SubscriberId",
 | 
					 | 
				
			||||||
        to = "super::subscribers::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "Cascade",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Subscriber,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::bangumi::Entity")]
 | 
					 | 
				
			||||||
    Bangumi,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::episodes::Entity")]
 | 
					 | 
				
			||||||
    Episodes,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::subscription_episode::Entity")]
 | 
					 | 
				
			||||||
    SubscriptionEpisode,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::subscription_bangumi::Entity")]
 | 
					 | 
				
			||||||
    SubscriptionBangumi,
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::credential_3rd::Entity",
 | 
					 | 
				
			||||||
        from = "Column::CredentialId",
 | 
					 | 
				
			||||||
        to = "super::credential_3rd::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "Cascade",
 | 
					 | 
				
			||||||
        on_delete = "SetNull"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Credential3rd,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::subscribers::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Subscriber.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::subscription_bangumi::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::SubscriptionBangumi.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::subscription_episode::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::SubscriptionEpisode.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::bangumi::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        super::subscription_bangumi::Relation::Bangumi.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn via() -> Option<RelationDef> {
 | 
					 | 
				
			||||||
        Some(
 | 
					 | 
				
			||||||
            super::subscription_bangumi::Relation::Subscription
 | 
					 | 
				
			||||||
                .def()
 | 
					 | 
				
			||||||
                .rev(),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::episodes::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        super::subscription_episode::Relation::Episode.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn via() -> Option<RelationDef> {
 | 
					 | 
				
			||||||
        Some(
 | 
					 | 
				
			||||||
            super::subscription_episode::Relation::Subscription
 | 
					 | 
				
			||||||
                .def()
 | 
					 | 
				
			||||||
                .rev(),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::credential_3rd::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Credential3rd.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
 | 
					 | 
				
			||||||
pub enum RelatedEntity {
 | 
					 | 
				
			||||||
    #[sea_orm(entity = "super::subscribers::Entity")]
 | 
					 | 
				
			||||||
    Subscriber,
 | 
					 | 
				
			||||||
    #[sea_orm(entity = "super::bangumi::Entity")]
 | 
					 | 
				
			||||||
    Bangumi,
 | 
					 | 
				
			||||||
    #[sea_orm(entity = "super::episodes::Entity")]
 | 
					 | 
				
			||||||
    Episode,
 | 
					 | 
				
			||||||
    #[sea_orm(entity = "super::subscription_episode::Entity")]
 | 
					 | 
				
			||||||
    SubscriptionEpisode,
 | 
					 | 
				
			||||||
    #[sea_orm(entity = "super::subscription_bangumi::Entity")]
 | 
					 | 
				
			||||||
    SubscriptionBangumi,
 | 
					 | 
				
			||||||
    #[sea_orm(entity = "super::credential_3rd::Entity")]
 | 
					 | 
				
			||||||
    Credential3rd,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_trait]
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModel {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Model {
 | 
					 | 
				
			||||||
    pub async fn toggle_with_ids(
 | 
					 | 
				
			||||||
        ctx: &dyn AppContextTrait,
 | 
					 | 
				
			||||||
        ids: impl Iterator<Item = i32>,
 | 
					 | 
				
			||||||
        enabled: bool,
 | 
					 | 
				
			||||||
    ) -> RecorderResult<()> {
 | 
					 | 
				
			||||||
        let db = ctx.db();
 | 
					 | 
				
			||||||
        Entity::update_many()
 | 
					 | 
				
			||||||
            .col_expr(Column::Enabled, Expr::value(enabled))
 | 
					 | 
				
			||||||
            .filter(Column::Id.is_in(ids))
 | 
					 | 
				
			||||||
            .exec(db)
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn delete_with_ids(
 | 
					 | 
				
			||||||
        ctx: &dyn AppContextTrait,
 | 
					 | 
				
			||||||
        ids: impl Iterator<Item = i32>,
 | 
					 | 
				
			||||||
    ) -> RecorderResult<()> {
 | 
					 | 
				
			||||||
        let db = ctx.db();
 | 
					 | 
				
			||||||
        Entity::delete_many()
 | 
					 | 
				
			||||||
            .filter(Column::Id.is_in(ids))
 | 
					 | 
				
			||||||
            .exec(db)
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn find_by_id_and_subscriber_id(
 | 
					 | 
				
			||||||
        ctx: &dyn AppContextTrait,
 | 
					 | 
				
			||||||
        subscriber_id: i32,
 | 
					 | 
				
			||||||
        subscription_id: i32,
 | 
					 | 
				
			||||||
    ) -> RecorderResult<Self> {
 | 
					 | 
				
			||||||
        let db = ctx.db();
 | 
					 | 
				
			||||||
        let subscription_model = Entity::find_by_id(subscription_id)
 | 
					 | 
				
			||||||
            .one(db)
 | 
					 | 
				
			||||||
            .await?
 | 
					 | 
				
			||||||
            .ok_or_else(|| RecorderError::ModelEntityNotFound {
 | 
					 | 
				
			||||||
                entity: "Subscription".into(),
 | 
					 | 
				
			||||||
            })?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if subscription_model.subscriber_id != subscriber_id {
 | 
					 | 
				
			||||||
            Err(RecorderError::ModelEntityNotFound {
 | 
					 | 
				
			||||||
                entity: "Subscription".into(),
 | 
					 | 
				
			||||||
            })?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(subscription_model)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_trait]
 | 
					 | 
				
			||||||
pub trait SubscriptionTrait: Sized + Debug {
 | 
					 | 
				
			||||||
    fn get_subscriber_id(&self) -> i32;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn get_subscription_id(&self) -> i32;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn sync_feeds_incremental(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn sync_feeds_full(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn sync_sources(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn try_from_model(model: &Model) -> RecorderResult<Self>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
 | 
					 | 
				
			||||||
#[serde(tag = "category")]
 | 
					 | 
				
			||||||
pub enum Subscription {
 | 
					 | 
				
			||||||
    #[serde(rename = "mikan_subscriber")]
 | 
					 | 
				
			||||||
    MikanSubscriber(MikanSubscriberSubscription),
 | 
					 | 
				
			||||||
    #[serde(rename = "mikan_season")]
 | 
					 | 
				
			||||||
    MikanSeason(MikanSeasonSubscription),
 | 
					 | 
				
			||||||
    #[serde(rename = "mikan_bangumi")]
 | 
					 | 
				
			||||||
    MikanBangumi(MikanBangumiSubscription),
 | 
					 | 
				
			||||||
    #[serde(rename = "manual")]
 | 
					 | 
				
			||||||
    Manual,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Subscription {
 | 
					 | 
				
			||||||
    pub fn category(&self) -> SubscriptionCategory {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::MikanSubscriber(_) => SubscriptionCategory::MikanSubscriber,
 | 
					 | 
				
			||||||
            Self::MikanSeason(_) => SubscriptionCategory::MikanSeason,
 | 
					 | 
				
			||||||
            Self::MikanBangumi(_) => SubscriptionCategory::MikanBangumi,
 | 
					 | 
				
			||||||
            Self::Manual => SubscriptionCategory::Manual,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_trait]
 | 
					 | 
				
			||||||
impl SubscriptionTrait for Subscription {
 | 
					 | 
				
			||||||
    fn get_subscriber_id(&self) -> i32 {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::MikanSubscriber(subscription) => subscription.get_subscriber_id(),
 | 
					 | 
				
			||||||
            Self::MikanSeason(subscription) => subscription.get_subscriber_id(),
 | 
					 | 
				
			||||||
            Self::MikanBangumi(subscription) => subscription.get_subscriber_id(),
 | 
					 | 
				
			||||||
            Self::Manual => unreachable!(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn get_subscription_id(&self) -> i32 {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::MikanSubscriber(subscription) => subscription.get_subscription_id(),
 | 
					 | 
				
			||||||
            Self::MikanSeason(subscription) => subscription.get_subscription_id(),
 | 
					 | 
				
			||||||
            Self::MikanBangumi(subscription) => subscription.get_subscription_id(),
 | 
					 | 
				
			||||||
            Self::Manual => unreachable!(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn sync_feeds_incremental(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::MikanSubscriber(subscription) => subscription.sync_feeds_incremental(ctx).await,
 | 
					 | 
				
			||||||
            Self::MikanSeason(subscription) => subscription.sync_feeds_incremental(ctx).await,
 | 
					 | 
				
			||||||
            Self::MikanBangumi(subscription) => subscription.sync_feeds_incremental(ctx).await,
 | 
					 | 
				
			||||||
            Self::Manual => Ok(()),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn sync_feeds_full(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::MikanSubscriber(subscription) => subscription.sync_feeds_full(ctx).await,
 | 
					 | 
				
			||||||
            Self::MikanSeason(subscription) => subscription.sync_feeds_full(ctx).await,
 | 
					 | 
				
			||||||
            Self::MikanBangumi(subscription) => subscription.sync_feeds_full(ctx).await,
 | 
					 | 
				
			||||||
            Self::Manual => Ok(()),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn sync_sources(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::MikanSubscriber(subscription) => subscription.sync_sources(ctx).await,
 | 
					 | 
				
			||||||
            Self::MikanSeason(subscription) => subscription.sync_sources(ctx).await,
 | 
					 | 
				
			||||||
            Self::MikanBangumi(subscription) => subscription.sync_sources(ctx).await,
 | 
					 | 
				
			||||||
            Self::Manual => Ok(()),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn try_from_model(model: &Model) -> RecorderResult<Self> {
 | 
					 | 
				
			||||||
        match model.category {
 | 
					 | 
				
			||||||
            SubscriptionCategory::MikanSubscriber => {
 | 
					 | 
				
			||||||
                MikanSubscriberSubscription::try_from_model(model).map(Self::MikanSubscriber)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            SubscriptionCategory::MikanSeason => {
 | 
					 | 
				
			||||||
                MikanSeasonSubscription::try_from_model(model).map(Self::MikanSeason)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            SubscriptionCategory::MikanBangumi => {
 | 
					 | 
				
			||||||
                MikanBangumiSubscription::try_from_model(model).map(Self::MikanBangumi)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            SubscriptionCategory::Manual => Ok(Self::Manual),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl TryFrom<&Model> for Subscription {
 | 
					 | 
				
			||||||
    type Error = RecorderError;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn try_from(model: &Model) -> Result<Self, Self::Error> {
 | 
					 | 
				
			||||||
        Self::try_from_model(model)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										20
									
								
								apps/recorder/src/models/subscriptions/core.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/recorder/src/models/subscriptions/core.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					use std::{fmt::Debug, sync::Arc};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use async_trait::async_trait;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{app::AppContextTrait, errors::RecorderResult, models::subscriptions};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					pub trait SubscriptionTrait: Sized + Debug {
 | 
				
			||||||
 | 
					    fn get_subscriber_id(&self) -> i32;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_subscription_id(&self) -> i32;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn sync_feeds_incremental(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn sync_feeds_full(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn sync_sources(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn try_from_model(model: &subscriptions::Model) -> RecorderResult<Self>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										185
									
								
								apps/recorder/src/models/subscriptions/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								apps/recorder/src/models/subscriptions/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,185 @@
 | 
				
			|||||||
 | 
					mod core;
 | 
				
			||||||
 | 
					mod registry;
 | 
				
			||||||
 | 
					pub use core::SubscriptionTrait;
 | 
				
			||||||
 | 
					use std::fmt::Debug;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use async_trait::async_trait;
 | 
				
			||||||
 | 
					pub use registry::{
 | 
				
			||||||
 | 
					    Subscription, SubscriptionCategory, SubscriptionCategoryEnum, SubscriptionCategoryIter,
 | 
				
			||||||
 | 
					    SubscriptionCategoryVariant, SubscriptionCategoryVariantIter,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sea_orm::entity::prelude::*;
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    app::AppContextTrait,
 | 
				
			||||||
 | 
					    errors::{RecorderError, RecorderResult},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
 | 
				
			||||||
 | 
					#[sea_orm(table_name = "subscriptions")]
 | 
				
			||||||
 | 
					pub struct Model {
 | 
				
			||||||
 | 
					    #[sea_orm(default_expr = "Expr::current_timestamp()")]
 | 
				
			||||||
 | 
					    pub created_at: DateTimeUtc,
 | 
				
			||||||
 | 
					    #[sea_orm(default_expr = "Expr::current_timestamp()")]
 | 
				
			||||||
 | 
					    pub updated_at: DateTimeUtc,
 | 
				
			||||||
 | 
					    #[sea_orm(primary_key)]
 | 
				
			||||||
 | 
					    pub id: i32,
 | 
				
			||||||
 | 
					    pub display_name: String,
 | 
				
			||||||
 | 
					    pub subscriber_id: i32,
 | 
				
			||||||
 | 
					    pub category: SubscriptionCategory,
 | 
				
			||||||
 | 
					    pub source_url: String,
 | 
				
			||||||
 | 
					    pub enabled: bool,
 | 
				
			||||||
 | 
					    pub credential_id: Option<i32>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
				
			||||||
 | 
					pub enum Relation {
 | 
				
			||||||
 | 
					    #[sea_orm(
 | 
				
			||||||
 | 
					        belongs_to = "super::subscribers::Entity",
 | 
				
			||||||
 | 
					        from = "Column::SubscriberId",
 | 
				
			||||||
 | 
					        to = "super::subscribers::Column::Id",
 | 
				
			||||||
 | 
					        on_update = "Cascade",
 | 
				
			||||||
 | 
					        on_delete = "Cascade"
 | 
				
			||||||
 | 
					    )]
 | 
				
			||||||
 | 
					    Subscriber,
 | 
				
			||||||
 | 
					    #[sea_orm(has_many = "super::bangumi::Entity")]
 | 
				
			||||||
 | 
					    Bangumi,
 | 
				
			||||||
 | 
					    #[sea_orm(has_many = "super::episodes::Entity")]
 | 
				
			||||||
 | 
					    Episodes,
 | 
				
			||||||
 | 
					    #[sea_orm(has_many = "super::subscription_episode::Entity")]
 | 
				
			||||||
 | 
					    SubscriptionEpisode,
 | 
				
			||||||
 | 
					    #[sea_orm(has_many = "super::subscription_bangumi::Entity")]
 | 
				
			||||||
 | 
					    SubscriptionBangumi,
 | 
				
			||||||
 | 
					    #[sea_orm(
 | 
				
			||||||
 | 
					        belongs_to = "super::credential_3rd::Entity",
 | 
				
			||||||
 | 
					        from = "Column::CredentialId",
 | 
				
			||||||
 | 
					        to = "super::credential_3rd::Column::Id",
 | 
				
			||||||
 | 
					        on_update = "Cascade",
 | 
				
			||||||
 | 
					        on_delete = "SetNull"
 | 
				
			||||||
 | 
					    )]
 | 
				
			||||||
 | 
					    Credential3rd,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Related<super::subscribers::Entity> for Entity {
 | 
				
			||||||
 | 
					    fn to() -> RelationDef {
 | 
				
			||||||
 | 
					        Relation::Subscriber.def()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Related<super::subscription_bangumi::Entity> for Entity {
 | 
				
			||||||
 | 
					    fn to() -> RelationDef {
 | 
				
			||||||
 | 
					        Relation::SubscriptionBangumi.def()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Related<super::subscription_episode::Entity> for Entity {
 | 
				
			||||||
 | 
					    fn to() -> RelationDef {
 | 
				
			||||||
 | 
					        Relation::SubscriptionEpisode.def()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Related<super::bangumi::Entity> for Entity {
 | 
				
			||||||
 | 
					    fn to() -> RelationDef {
 | 
				
			||||||
 | 
					        super::subscription_bangumi::Relation::Bangumi.def()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn via() -> Option<RelationDef> {
 | 
				
			||||||
 | 
					        Some(
 | 
				
			||||||
 | 
					            super::subscription_bangumi::Relation::Subscription
 | 
				
			||||||
 | 
					                .def()
 | 
				
			||||||
 | 
					                .rev(),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Related<super::episodes::Entity> for Entity {
 | 
				
			||||||
 | 
					    fn to() -> RelationDef {
 | 
				
			||||||
 | 
					        super::subscription_episode::Relation::Episode.def()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn via() -> Option<RelationDef> {
 | 
				
			||||||
 | 
					        Some(
 | 
				
			||||||
 | 
					            super::subscription_episode::Relation::Subscription
 | 
				
			||||||
 | 
					                .def()
 | 
				
			||||||
 | 
					                .rev(),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Related<super::credential_3rd::Entity> for Entity {
 | 
				
			||||||
 | 
					    fn to() -> RelationDef {
 | 
				
			||||||
 | 
					        Relation::Credential3rd.def()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
 | 
				
			||||||
 | 
					pub enum RelatedEntity {
 | 
				
			||||||
 | 
					    #[sea_orm(entity = "super::subscribers::Entity")]
 | 
				
			||||||
 | 
					    Subscriber,
 | 
				
			||||||
 | 
					    #[sea_orm(entity = "super::bangumi::Entity")]
 | 
				
			||||||
 | 
					    Bangumi,
 | 
				
			||||||
 | 
					    #[sea_orm(entity = "super::episodes::Entity")]
 | 
				
			||||||
 | 
					    Episode,
 | 
				
			||||||
 | 
					    #[sea_orm(entity = "super::subscription_episode::Entity")]
 | 
				
			||||||
 | 
					    SubscriptionEpisode,
 | 
				
			||||||
 | 
					    #[sea_orm(entity = "super::subscription_bangumi::Entity")]
 | 
				
			||||||
 | 
					    SubscriptionBangumi,
 | 
				
			||||||
 | 
					    #[sea_orm(entity = "super::credential_3rd::Entity")]
 | 
				
			||||||
 | 
					    Credential3rd,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl ActiveModelBehavior for ActiveModel {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ActiveModel {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Model {
 | 
				
			||||||
 | 
					    pub async fn toggle_with_ids(
 | 
				
			||||||
 | 
					        ctx: &dyn AppContextTrait,
 | 
				
			||||||
 | 
					        ids: impl Iterator<Item = i32>,
 | 
				
			||||||
 | 
					        enabled: bool,
 | 
				
			||||||
 | 
					    ) -> RecorderResult<()> {
 | 
				
			||||||
 | 
					        let db = ctx.db();
 | 
				
			||||||
 | 
					        Entity::update_many()
 | 
				
			||||||
 | 
					            .col_expr(Column::Enabled, Expr::value(enabled))
 | 
				
			||||||
 | 
					            .filter(Column::Id.is_in(ids))
 | 
				
			||||||
 | 
					            .exec(db)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn delete_with_ids(
 | 
				
			||||||
 | 
					        ctx: &dyn AppContextTrait,
 | 
				
			||||||
 | 
					        ids: impl Iterator<Item = i32>,
 | 
				
			||||||
 | 
					    ) -> RecorderResult<()> {
 | 
				
			||||||
 | 
					        let db = ctx.db();
 | 
				
			||||||
 | 
					        Entity::delete_many()
 | 
				
			||||||
 | 
					            .filter(Column::Id.is_in(ids))
 | 
				
			||||||
 | 
					            .exec(db)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn find_by_id_and_subscriber_id(
 | 
				
			||||||
 | 
					        ctx: &dyn AppContextTrait,
 | 
				
			||||||
 | 
					        subscriber_id: i32,
 | 
				
			||||||
 | 
					        subscription_id: i32,
 | 
				
			||||||
 | 
					    ) -> RecorderResult<Self> {
 | 
				
			||||||
 | 
					        let db = ctx.db();
 | 
				
			||||||
 | 
					        let subscription_model = Entity::find_by_id(subscription_id)
 | 
				
			||||||
 | 
					            .one(db)
 | 
				
			||||||
 | 
					            .await?
 | 
				
			||||||
 | 
					            .ok_or_else(|| RecorderError::ModelEntityNotFound {
 | 
				
			||||||
 | 
					                entity: "Subscription".into(),
 | 
				
			||||||
 | 
					            })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if subscription_model.subscriber_id != subscriber_id {
 | 
				
			||||||
 | 
					            Err(RecorderError::ModelEntityNotFound {
 | 
				
			||||||
 | 
					                entity: "Subscription".into(),
 | 
				
			||||||
 | 
					            })?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(subscription_model)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										129
									
								
								apps/recorder/src/models/subscriptions/registry.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								apps/recorder/src/models/subscriptions/registry.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					use std::{fmt::Debug, sync::Arc};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use async_trait::async_trait;
 | 
				
			||||||
 | 
					use sea_orm::{DeriveActiveEnum, DeriveDisplay, EnumIter};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    app::AppContextTrait,
 | 
				
			||||||
 | 
					    errors::{RecorderError, RecorderResult},
 | 
				
			||||||
 | 
					    extract::mikan::{
 | 
				
			||||||
 | 
					        MikanBangumiSubscription, MikanSeasonSubscription, MikanSubscriberSubscription,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    models::subscriptions::{self, SubscriptionTrait},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(
 | 
				
			||||||
 | 
					    Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, DeriveDisplay,
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					#[sea_orm(
 | 
				
			||||||
 | 
					    rs_type = "String",
 | 
				
			||||||
 | 
					    db_type = "Enum",
 | 
				
			||||||
 | 
					    enum_name = "subscription_category"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					#[serde(rename_all = "snake_case")]
 | 
				
			||||||
 | 
					pub enum SubscriptionCategory {
 | 
				
			||||||
 | 
					    #[sea_orm(string_value = "mikan_subscriber")]
 | 
				
			||||||
 | 
					    MikanSubscriber,
 | 
				
			||||||
 | 
					    #[sea_orm(string_value = "mikan_season")]
 | 
				
			||||||
 | 
					    MikanSeason,
 | 
				
			||||||
 | 
					    #[sea_orm(string_value = "mikan_bangumi")]
 | 
				
			||||||
 | 
					    MikanBangumi,
 | 
				
			||||||
 | 
					    #[sea_orm(string_value = "manual")]
 | 
				
			||||||
 | 
					    Manual,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
 | 
				
			||||||
 | 
					#[serde(tag = "category")]
 | 
				
			||||||
 | 
					pub enum Subscription {
 | 
				
			||||||
 | 
					    #[serde(rename = "mikan_subscriber")]
 | 
				
			||||||
 | 
					    MikanSubscriber(MikanSubscriberSubscription),
 | 
				
			||||||
 | 
					    #[serde(rename = "mikan_season")]
 | 
				
			||||||
 | 
					    MikanSeason(MikanSeasonSubscription),
 | 
				
			||||||
 | 
					    #[serde(rename = "mikan_bangumi")]
 | 
				
			||||||
 | 
					    MikanBangumi(MikanBangumiSubscription),
 | 
				
			||||||
 | 
					    #[serde(rename = "manual")]
 | 
				
			||||||
 | 
					    Manual,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Subscription {
 | 
				
			||||||
 | 
					    pub fn category(&self) -> SubscriptionCategory {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::MikanSubscriber(_) => SubscriptionCategory::MikanSubscriber,
 | 
				
			||||||
 | 
					            Self::MikanSeason(_) => SubscriptionCategory::MikanSeason,
 | 
				
			||||||
 | 
					            Self::MikanBangumi(_) => SubscriptionCategory::MikanBangumi,
 | 
				
			||||||
 | 
					            Self::Manual => SubscriptionCategory::Manual,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl SubscriptionTrait for Subscription {
 | 
				
			||||||
 | 
					    fn get_subscriber_id(&self) -> i32 {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::MikanSubscriber(subscription) => subscription.get_subscriber_id(),
 | 
				
			||||||
 | 
					            Self::MikanSeason(subscription) => subscription.get_subscriber_id(),
 | 
				
			||||||
 | 
					            Self::MikanBangumi(subscription) => subscription.get_subscriber_id(),
 | 
				
			||||||
 | 
					            Self::Manual => unreachable!(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_subscription_id(&self) -> i32 {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::MikanSubscriber(subscription) => subscription.get_subscription_id(),
 | 
				
			||||||
 | 
					            Self::MikanSeason(subscription) => subscription.get_subscription_id(),
 | 
				
			||||||
 | 
					            Self::MikanBangumi(subscription) => subscription.get_subscription_id(),
 | 
				
			||||||
 | 
					            Self::Manual => unreachable!(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn sync_feeds_incremental(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::MikanSubscriber(subscription) => subscription.sync_feeds_incremental(ctx).await,
 | 
				
			||||||
 | 
					            Self::MikanSeason(subscription) => subscription.sync_feeds_incremental(ctx).await,
 | 
				
			||||||
 | 
					            Self::MikanBangumi(subscription) => subscription.sync_feeds_incremental(ctx).await,
 | 
				
			||||||
 | 
					            Self::Manual => Ok(()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn sync_feeds_full(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::MikanSubscriber(subscription) => subscription.sync_feeds_full(ctx).await,
 | 
				
			||||||
 | 
					            Self::MikanSeason(subscription) => subscription.sync_feeds_full(ctx).await,
 | 
				
			||||||
 | 
					            Self::MikanBangumi(subscription) => subscription.sync_feeds_full(ctx).await,
 | 
				
			||||||
 | 
					            Self::Manual => Ok(()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn sync_sources(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::MikanSubscriber(subscription) => subscription.sync_sources(ctx).await,
 | 
				
			||||||
 | 
					            Self::MikanSeason(subscription) => subscription.sync_sources(ctx).await,
 | 
				
			||||||
 | 
					            Self::MikanBangumi(subscription) => subscription.sync_sources(ctx).await,
 | 
				
			||||||
 | 
					            Self::Manual => Ok(()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn try_from_model(model: &subscriptions::Model) -> RecorderResult<Self> {
 | 
				
			||||||
 | 
					        match model.category {
 | 
				
			||||||
 | 
					            SubscriptionCategory::MikanSubscriber => {
 | 
				
			||||||
 | 
					                MikanSubscriberSubscription::try_from_model(model).map(Self::MikanSubscriber)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            SubscriptionCategory::MikanSeason => {
 | 
				
			||||||
 | 
					                MikanSeasonSubscription::try_from_model(model).map(Self::MikanSeason)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            SubscriptionCategory::MikanBangumi => {
 | 
				
			||||||
 | 
					                MikanBangumiSubscription::try_from_model(model).map(Self::MikanBangumi)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            SubscriptionCategory::Manual => Ok(Self::Manual),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TryFrom<&subscriptions::Model> for Subscription {
 | 
				
			||||||
 | 
					    type Error = RecorderError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn try_from(model: &subscriptions::Model) -> Result<Self, Self::Error> {
 | 
				
			||||||
 | 
					        Self::try_from_model(model)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										41
									
								
								apps/recorder/src/web/controller/feeds/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								apps/recorder/src/web/controller/feeds/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use axum::{
 | 
				
			||||||
 | 
					    Extension, Router,
 | 
				
			||||||
 | 
					    extract::{Path, State},
 | 
				
			||||||
 | 
					    response::IntoResponse,
 | 
				
			||||||
 | 
					    routing::get,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use http::StatusCode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    app::AppContextTrait,
 | 
				
			||||||
 | 
					    errors::{RecorderError, RecorderResult},
 | 
				
			||||||
 | 
					    extract::http::ForwardedRelatedInfo,
 | 
				
			||||||
 | 
					    models::feeds,
 | 
				
			||||||
 | 
					    web::controller::Controller,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const CONTROLLER_PREFIX: &str = "/api/feeds";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn rss_handler(
 | 
				
			||||||
 | 
					    State(ctx): State<Arc<dyn AppContextTrait>>,
 | 
				
			||||||
 | 
					    Path(token): Path<String>,
 | 
				
			||||||
 | 
					    forwarded_info: Extension<ForwardedRelatedInfo>,
 | 
				
			||||||
 | 
					) -> RecorderResult<impl IntoResponse> {
 | 
				
			||||||
 | 
					    let api_base = forwarded_info
 | 
				
			||||||
 | 
					        .resolved_origin()
 | 
				
			||||||
 | 
					        .ok_or(RecorderError::MissingOriginError)?;
 | 
				
			||||||
 | 
					    let channel = feeds::Model::find_rss_feed_by_token(ctx.as_ref(), &token, &api_base).await?;
 | 
				
			||||||
 | 
					    Ok((
 | 
				
			||||||
 | 
					        StatusCode::OK,
 | 
				
			||||||
 | 
					        [("Content-Type", "application/rss+xml")],
 | 
				
			||||||
 | 
					        channel.to_string(),
 | 
				
			||||||
 | 
					    ))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn create(_ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller> {
 | 
				
			||||||
 | 
					    let router = Router::<Arc<dyn AppContextTrait>>::new().route("rss/{token}", get(rss_handler));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(Controller::from_prefix(CONTROLLER_PREFIX, router))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
pub mod core;
 | 
					pub mod core;
 | 
				
			||||||
 | 
					pub mod feeds;
 | 
				
			||||||
pub mod graphql;
 | 
					pub mod graphql;
 | 
				
			||||||
pub mod metadata;
 | 
					pub mod metadata;
 | 
				
			||||||
pub mod oidc;
 | 
					pub mod oidc;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,8 @@
 | 
				
			|||||||
use std::sync::Arc;
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use axum::{
 | 
					use axum::{
 | 
				
			||||||
    Json, Router,
 | 
					    Extension, Json, Router,
 | 
				
			||||||
    extract::{Query, Request, State},
 | 
					    extract::{Query, State},
 | 
				
			||||||
    routing::get,
 | 
					    routing::get,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use snafu::ResultExt;
 | 
					use snafu::ResultExt;
 | 
				
			||||||
@ -42,12 +42,11 @@ async fn oidc_callback(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async fn oidc_auth(
 | 
					async fn oidc_auth(
 | 
				
			||||||
    State(ctx): State<Arc<dyn AppContextTrait>>,
 | 
					    State(ctx): State<Arc<dyn AppContextTrait>>,
 | 
				
			||||||
    request: Request,
 | 
					    forwarded_info: Extension<ForwardedRelatedInfo>,
 | 
				
			||||||
) -> Result<Json<OidcAuthRequest>, AuthError> {
 | 
					) -> Result<Json<OidcAuthRequest>, AuthError> {
 | 
				
			||||||
    let auth_service = ctx.auth();
 | 
					    let auth_service = ctx.auth();
 | 
				
			||||||
    if let AuthService::Oidc(oidc_auth_service) = auth_service {
 | 
					    if let AuthService::Oidc(oidc_auth_service) = auth_service {
 | 
				
			||||||
        let (parts, _) = request.into_parts();
 | 
					        let mut redirect_uri = forwarded_info
 | 
				
			||||||
        let mut redirect_uri = ForwardedRelatedInfo::from_request_parts(&parts)
 | 
					 | 
				
			||||||
            .resolved_origin()
 | 
					            .resolved_origin()
 | 
				
			||||||
            .ok_or(url::ParseError::EmptyHost)
 | 
					            .ok_or(url::ParseError::EmptyHost)
 | 
				
			||||||
            .context(OidcRequestRedirectUriSnafu)?;
 | 
					            .context(OidcRequestRedirectUriSnafu)?;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user