konobangu/apps/recorder/src/models/episodes.rs

255 lines
8.6 KiB
Rust

use async_trait::async_trait;
use sea_orm::{
ActiveValue, IntoSimpleExpr, QuerySelect, entity::prelude::*, sea_query::OnConflict,
};
use serde::{Deserialize, Serialize};
use super::{bangumi, query::InsertManyReturningExt, subscription_episode};
use crate::{
app::AppContextTrait,
errors::RecorderResult,
extract::{
mikan::{MikanEpisodeHash, MikanEpisodeMeta, build_mikan_episode_homepage_url},
origin::extract_episode_meta_from_origin_name,
},
};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "episodes")]
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,
#[sea_orm(indexed)]
pub mikan_episode_id: Option<String>,
pub origin_name: String,
pub display_name: String,
pub bangumi_id: i32,
pub subscriber_id: i32,
pub save_path: Option<String>,
pub resolution: Option<String>,
pub season: i32,
pub season_raw: Option<String>,
pub fansub: Option<String>,
pub poster_link: Option<String>,
pub origin_poster_link: Option<String>,
pub episode_index: i32,
pub homepage: Option<String>,
pub subtitle: Option<String>,
pub source: Option<String>,
}
#[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<super::bangumi::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bangumi.def()
}
}
impl Related<super::downloads::Entity> for Entity {
fn to() -> RelationDef {
Relation::Download.def()
}
}
impl Related<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}
impl Related<super::subscription_episode::Entity> for Entity {
fn to() -> RelationDef {
Relation::SubscriptionEpisode.def()
}
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
super::subscription_episode::Relation::Subscription.def()
}
fn via() -> Option<RelationDef> {
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,
}
impl ActiveModel {
#[tracing::instrument(err, skip(ctx), fields(bangumi_id = ?bangumi.id, mikan_episode_id = ?episode.mikan_episode_id))]
pub fn from_mikan_bangumi_and_episode_meta(
ctx: &dyn AppContextTrait,
bangumi: &bangumi::Model,
episode: MikanEpisodeMeta,
) -> RecorderResult<Self> {
let mikan_base_url = ctx.mikan().base_url().clone();
let episode_extention_meta = extract_episode_meta_from_origin_name(&episode.episode_title)
.inspect_err(|err| {
tracing::error!(
err = ?err,
episode_title = ?episode.episode_title,
"Failed to parse episode extension meta from episode title, skip"
);
})
.ok();
let homepage = build_mikan_episode_homepage_url(mikan_base_url, &episode.mikan_episode_id);
let mut episode_active_model = Self {
mikan_episode_id: ActiveValue::Set(Some(episode.mikan_episode_id)),
origin_name: ActiveValue::Set(episode.episode_title.clone()),
display_name: ActiveValue::Set(episode.episode_title.clone()),
bangumi_id: ActiveValue::Set(bangumi.id),
subscriber_id: ActiveValue::Set(bangumi.subscriber_id),
homepage: ActiveValue::Set(Some(homepage.to_string())),
season_raw: ActiveValue::Set(bangumi.season_raw.clone()),
season: ActiveValue::Set(bangumi.season),
fansub: ActiveValue::Set(bangumi.fansub.clone()),
poster_link: ActiveValue::Set(bangumi.poster_link.clone()),
origin_poster_link: ActiveValue::Set(bangumi.origin_poster_link.clone()),
episode_index: ActiveValue::Set(0),
..Default::default()
};
if let Some(episode_extention_meta) = episode_extention_meta {
episode_active_model.episode_index =
ActiveValue::Set(episode_extention_meta.episode_index);
episode_active_model.subtitle = ActiveValue::Set(episode_extention_meta.subtitle);
episode_active_model.source = ActiveValue::Set(episode_extention_meta.source);
episode_active_model.resolution = ActiveValue::Set(episode_extention_meta.resolution);
if episode_extention_meta.season > 0 {
episode_active_model.season = ActiveValue::Set(episode_extention_meta.season);
}
if episode_extention_meta.season_raw.is_some() {
episode_active_model.season_raw =
ActiveValue::Set(episode_extention_meta.season_raw);
}
if episode_extention_meta.fansub.is_some() {
episode_active_model.fansub = ActiveValue::Set(episode_extention_meta.fansub);
}
}
Ok(episode_active_model)
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {}
impl Model {
pub async fn get_existed_mikan_episode_list(
ctx: &dyn AppContextTrait,
ids: impl Iterator<Item = MikanEpisodeHash>,
subscriber_id: i32,
_subscription_id: i32,
) -> RecorderResult<impl Iterator<Item = (i32, MikanEpisodeHash, i32)>> {
let db = ctx.db();
Ok(Entity::find()
.select_only()
.column(Column::Id)
.column(Column::MikanEpisodeId)
.column(Column::BangumiId)
.filter(
Expr::tuple([
Column::MikanEpisodeId.into_simple_expr(),
Column::SubscriberId.into_simple_expr(),
])
.in_tuples(
ids.into_iter()
.map(|id| (id.mikan_episode_id, subscriber_id)),
),
)
.into_tuple::<(i32, String, i32)>()
.all(db)
.await?
.into_iter()
.map(|(episode_id, mikan_episode_id, bangumi_id)| {
(
episode_id,
MikanEpisodeHash { mikan_episode_id },
bangumi_id,
)
}))
}
pub async fn add_mikan_episodes_for_subscription(
ctx: &dyn AppContextTrait,
creations: impl Iterator<Item = (&bangumi::Model, MikanEpisodeMeta)>,
subscriber_id: i32,
subscription_id: i32,
) -> RecorderResult<()> {
let db = ctx.db();
let new_episode_active_modes: Vec<ActiveModel> = creations
.map(|(bangumi, episode_meta)| {
ActiveModel::from_mikan_bangumi_and_episode_meta(ctx, bangumi, episode_meta)
})
.collect::<Result<_, _>>()?;
if new_episode_active_modes.is_empty() {
return Ok(());
}
let new_episode_ids = Entity::insert_many(new_episode_active_modes)
.on_conflict(
OnConflict::columns([Column::MikanEpisodeId, Column::SubscriberId])
.update_columns([Column::OriginName, Column::PosterLink, Column::Homepage])
.to_owned(),
)
.exec_with_returning_columns(db, [Column::Id])
.await?
.into_iter()
.flat_map(|r| r.try_get_many_by_index::<i32>());
subscription_episode::Model::add_episodes_for_subscription(
ctx,
new_episode_ids,
subscriber_id,
subscription_id,
)
.await?;
Ok(())
}
}