use std::sync::Arc; use async_trait::async_trait; use loco_rs::app::AppContext; use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue, FromJsonQueryResult}; use serde::{Deserialize, Serialize}; use super::{bangumi, query::InsertManyReturningExt, subscription_episode}; use crate::{ app::AppContextExt, extract::{ mikan::{build_mikan_episode_homepage, MikanEpisodeMeta}, rawname::parse_episode_meta_from_raw_name, }, }; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult, Default)] pub struct EpisodeExtra { pub name_zh: Option, pub s_name_zh: Option, pub name_en: Option, pub s_name_en: Option, pub name_jp: Option, pub s_name_jp: Option, } #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "episodes")] pub struct Model { pub created_at: DateTime, pub updated_at: DateTime, #[sea_orm(primary_key)] pub id: i32, #[sea_orm(indexed)] pub mikan_episode_id: Option, pub raw_name: String, pub display_name: String, pub bangumi_id: i32, pub subscriber_id: i32, pub save_path: Option, pub resolution: Option, pub season: i32, pub season_raw: Option, pub fansub: Option, pub poster_link: Option, pub episode_index: i32, pub homepage: Option, pub subtitle: Option, #[sea_orm(default = "false")] pub deleted: bool, pub source: Option, pub extra: EpisodeExtra, } #[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( belongs_to = "super::bangumi::Entity", from = "Column::BangumiId", to = "super::bangumi::Column::Id", on_update = "Cascade", on_delete = "Cascade" )] Bangumi, #[sea_orm(has_many = "super::subscriptions::Entity")] Subscription, #[sea_orm(has_one = "super::downloads::Entity")] Download, #[sea_orm(has_many = "super::subscription_episode::Entity")] SubscriptionEpisode, } impl Related for Entity { fn to() -> RelationDef { Relation::Bangumi.def() } } impl Related for Entity { fn to() -> RelationDef { Relation::Download.def() } } impl Related for Entity { fn to() -> RelationDef { Relation::Subscriber.def() } } impl Related for Entity { fn to() -> RelationDef { Relation::SubscriptionEpisode.def() } } impl Related for Entity { fn to() -> RelationDef { super::subscription_episode::Relation::Subscription.def() } fn via() -> Option { Some(Relation::Subscription.def()) } } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] pub enum RelatedEntity { #[sea_orm(entity = "super::subscribers::Entity")] Subscriber, #[sea_orm(entity = "super::downloads::Entity")] Subscription, #[sea_orm(entity = "super::bangumi::Entity")] Bangumi, #[sea_orm(entity = "super::subscriptions::Entity")] Download, #[sea_orm(entity = "super::subscription_episode::Entity")] SubscriptionEpisode, } #[derive(Clone, Debug, PartialEq)] pub struct MikanEpsiodeCreation { pub episode: MikanEpisodeMeta, pub bangumi: Arc, } impl Model { pub async fn add_episodes( ctx: &AppContext, subscription_id: i32, creations: impl IntoIterator, ) -> color_eyre::eyre::Result<()> { let db = &ctx.db; let new_episode_active_modes = creations .into_iter() .map(|cr| ActiveModel::from_mikan_episode_meta(ctx, cr)) .inspect(|result| { if let Err(e) = result { tracing::warn!("Failed to create episode: {:?}", e); } }) .flatten(); let inserted_episodes = Entity::insert_many(new_episode_active_modes) .on_conflict( OnConflict::columns([Column::BangumiId, Column::MikanEpisodeId]) .do_nothing() .to_owned(), ) .exec_with_returning_columns(db, [Column::Id]) .await? .into_iter() .flat_map(|r| r.try_get_many_by_index::()); let insert_subscription_episode_links = inserted_episodes.into_iter().map(|episode_id| { subscription_episode::ActiveModel::from_subscription_and_episode( subscription_id, episode_id, ) }); subscription_episode::Entity::insert_many(insert_subscription_episode_links) .on_conflict( OnConflict::columns([ subscription_episode::Column::SubscriptionId, subscription_episode::Column::EpisodeId, ]) .do_nothing() .to_owned(), ) .exec(db) .await?; Ok(()) } } impl ActiveModel { pub fn from_mikan_episode_meta( ctx: &AppContext, creation: MikanEpsiodeCreation, ) -> color_eyre::eyre::Result { let item = creation.episode; let bgm = creation.bangumi; let raw_meta = parse_episode_meta_from_raw_name(&item.episode_title) .inspect_err(|e| { tracing::warn!("Failed to parse episode meta: {:?}", e); }) .ok() .unwrap_or_default(); let homepage = build_mikan_episode_homepage( ctx.get_mikan_client().base_url(), &item.mikan_episode_id, )?; Ok(Self { mikan_episode_id: ActiveValue::Set(Some(item.mikan_episode_id)), raw_name: ActiveValue::Set(item.episode_title.clone()), display_name: ActiveValue::Set(item.episode_title.clone()), bangumi_id: ActiveValue::Set(bgm.id), subscriber_id: ActiveValue::Set(bgm.subscriber_id), resolution: ActiveValue::Set(raw_meta.resolution), season: ActiveValue::Set(if raw_meta.season > 0 { raw_meta.season } else { bgm.season }), season_raw: ActiveValue::Set(raw_meta.season_raw.or_else(|| bgm.season_raw.clone())), fansub: ActiveValue::Set(raw_meta.fansub.or_else(|| bgm.fansub.clone())), poster_link: ActiveValue::Set(bgm.poster_link.clone()), episode_index: ActiveValue::Set(raw_meta.episode_index), homepage: ActiveValue::Set(Some(homepage.to_string())), subtitle: ActiveValue::Set(raw_meta.subtitle), source: ActiveValue::Set(raw_meta.source), extra: ActiveValue::Set(EpisodeExtra { name_zh: raw_meta.name_zh, name_en: raw_meta.name_en, name_jp: raw_meta.name_jp, s_name_en: raw_meta.name_en_no_season, s_name_jp: raw_meta.name_jp_no_season, s_name_zh: raw_meta.name_zh_no_season, }), ..Default::default() }) } } #[async_trait] impl ActiveModelBehavior for ActiveModel {}