refactor: continue
This commit is contained in:
@@ -1,18 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_graphql::SimpleObject;
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{
|
||||
ActiveValue, FromJsonQueryResult, FromQueryResult, IntoSimpleExpr, JoinType, QuerySelect,
|
||||
ActiveValue, Condition, FromJsonQueryResult, FromQueryResult, IntoSimpleExpr, JoinType,
|
||||
QuerySelect,
|
||||
entity::prelude::*,
|
||||
sea_query::{IntoCondition, OnConflict},
|
||||
sea_query::{Alias, IntoCondition, OnConflict},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::subscription_bangumi;
|
||||
use crate::{
|
||||
app::AppContextTrait,
|
||||
errors::{RecorderError, RecorderResult},
|
||||
errors::RecorderResult,
|
||||
extract::{
|
||||
mikan::{
|
||||
MikanBangumiHash, MikanBangumiMeta, build_mikan_bangumi_subscription_rss_url,
|
||||
@@ -63,8 +62,6 @@ pub struct Model {
|
||||
pub rss_link: Option<String>,
|
||||
pub poster_link: Option<String>,
|
||||
pub save_path: Option<String>,
|
||||
#[sea_orm(default = "false")]
|
||||
pub deleted: bool,
|
||||
pub homepage: Option<String>,
|
||||
pub extra: Option<BangumiExtra>,
|
||||
}
|
||||
@@ -139,7 +136,7 @@ impl ActiveModel {
|
||||
let storage_service = ctx.storage();
|
||||
let mikan_base_url = mikan_client.base_url();
|
||||
|
||||
let raw_meta = parse_episode_meta_from_raw_name(&meta.bangumi_title)?;
|
||||
let rawname_meta = parse_episode_meta_from_raw_name(&meta.bangumi_title)?;
|
||||
|
||||
let rss_url = build_mikan_bangumi_subscription_rss_url(
|
||||
mikan_base_url.clone(),
|
||||
@@ -166,12 +163,20 @@ impl ActiveModel {
|
||||
subscriber_id: ActiveValue::Set(subscriber_id),
|
||||
display_name: ActiveValue::Set(meta.bangumi_title.clone()),
|
||||
raw_name: ActiveValue::Set(meta.bangumi_title),
|
||||
season: ActiveValue::Set(raw_meta.season),
|
||||
season_raw: ActiveValue::Set(raw_meta.season_raw),
|
||||
season: ActiveValue::Set(rawname_meta.season),
|
||||
season_raw: ActiveValue::Set(rawname_meta.season_raw),
|
||||
fansub: ActiveValue::Set(Some(meta.fansub)),
|
||||
poster_link: ActiveValue::Set(poster_link),
|
||||
homepage: ActiveValue::Set(Some(meta.homepage.to_string())),
|
||||
rss_link: ActiveValue::Set(Some(rss_url.to_string())),
|
||||
extra: ActiveValue::Set(Some(BangumiExtra {
|
||||
name_zh: rawname_meta.name_zh,
|
||||
name_en: rawname_meta.name_en,
|
||||
name_jp: rawname_meta.name_jp,
|
||||
s_name_en: rawname_meta.name_en_no_season,
|
||||
s_name_jp: rawname_meta.name_jp_no_season,
|
||||
s_name_zh: rawname_meta.name_zh_no_season,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -183,35 +188,60 @@ impl ActiveModelBehavior for ActiveModel {}
|
||||
impl Model {
|
||||
pub async fn get_or_insert_from_mikan<F>(
|
||||
ctx: &dyn AppContextTrait,
|
||||
hash: MikanBangumiHash,
|
||||
subscriber_id: i32,
|
||||
subscription_id: i32,
|
||||
mikan_bangumi_id: String,
|
||||
mikan_fansub_id: String,
|
||||
f: F,
|
||||
) -> RecorderResult<Model>
|
||||
create_bangumi_fn: F,
|
||||
) -> RecorderResult<Self>
|
||||
where
|
||||
F: AsyncFnOnce(&mut ActiveModel) -> RecorderResult<()>,
|
||||
F: AsyncFnOnce() -> RecorderResult<ActiveModel>,
|
||||
{
|
||||
#[derive(FromQueryResult)]
|
||||
struct ModelWithIsSubscribed {
|
||||
#[sea_orm(nested)]
|
||||
bangumi: Model,
|
||||
is_subscribed: bool,
|
||||
}
|
||||
|
||||
let db = ctx.db();
|
||||
if let Some(existed) = Entity::find()
|
||||
|
||||
let subscription_bangumi_alias = Alias::new("sb");
|
||||
let mut is_subscribed = false;
|
||||
let new_bangumi_model = if let Some(existed) = Entity::find()
|
||||
.filter(
|
||||
Column::MikanBangumiId
|
||||
.eq(Some(mikan_bangumi_id.clone()))
|
||||
.and(Column::MikanFansubId.eq(Some(mikan_fansub_id.clone()))),
|
||||
Condition::all()
|
||||
.add(Column::MikanBangumiId.eq(Some(hash.mikan_bangumi_id)))
|
||||
.add(Column::MikanFansubId.eq(Some(hash.mikan_fansub_id)))
|
||||
.add(Column::SubscriberId.eq(subscriber_id)),
|
||||
)
|
||||
.column_as(
|
||||
Expr::col((
|
||||
subscription_bangumi_alias.clone(),
|
||||
subscription_bangumi::Column::SubscriptionId,
|
||||
)),
|
||||
"is_subscribed",
|
||||
)
|
||||
.join_as_rev(
|
||||
JoinType::LeftJoin,
|
||||
subscription_bangumi::Relation::Bangumi
|
||||
.def()
|
||||
.on_condition(move |_left, right| {
|
||||
Expr::col((right, subscription_bangumi::Column::SubscriptionId))
|
||||
.eq(subscription_id)
|
||||
.into_condition()
|
||||
}),
|
||||
subscription_bangumi_alias.clone(),
|
||||
)
|
||||
.into_model::<ModelWithIsSubscribed>()
|
||||
.one(db)
|
||||
.await?
|
||||
{
|
||||
Ok(existed)
|
||||
is_subscribed = existed.is_subscribed;
|
||||
existed.bangumi
|
||||
} else {
|
||||
let mut bgm = ActiveModel {
|
||||
mikan_bangumi_id: ActiveValue::Set(Some(mikan_bangumi_id)),
|
||||
mikan_fansub_id: ActiveValue::Set(Some(mikan_fansub_id)),
|
||||
subscriber_id: ActiveValue::Set(subscriber_id),
|
||||
..Default::default()
|
||||
};
|
||||
f(&mut bgm).await?;
|
||||
let bgm = Entity::insert(bgm)
|
||||
let new_bangumi_active_model = create_bangumi_fn().await?;
|
||||
|
||||
Entity::insert(new_bangumi_active_model)
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
Column::MikanBangumiId,
|
||||
@@ -220,26 +250,30 @@ impl Model {
|
||||
])
|
||||
.update_columns([
|
||||
Column::RawName,
|
||||
Column::Extra,
|
||||
Column::Fansub,
|
||||
Column::PosterLink,
|
||||
Column::Season,
|
||||
Column::SeasonRaw,
|
||||
Column::RssLink,
|
||||
Column::Homepage,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec_with_returning(db)
|
||||
.await?;
|
||||
.await?
|
||||
};
|
||||
if !is_subscribed {
|
||||
subscription_bangumi::Entity::insert(subscription_bangumi::ActiveModel {
|
||||
subscription_id: ActiveValue::Set(subscription_id),
|
||||
bangumi_id: ActiveValue::Set(bgm.id),
|
||||
bangumi_id: ActiveValue::Set(new_bangumi_model.id),
|
||||
subscriber_id: ActiveValue::Set(subscriber_id),
|
||||
..Default::default()
|
||||
})
|
||||
.on_conflict_do_nothing()
|
||||
.exec(db)
|
||||
.await?;
|
||||
Ok(bgm)
|
||||
}
|
||||
Ok(new_bangumi_model)
|
||||
}
|
||||
|
||||
pub async fn get_existed_mikan_bangumi_list(
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{
|
||||
ActiveValue, ColumnTrait, FromJsonQueryResult, IntoSimpleExpr, JoinType, QuerySelect,
|
||||
entity::prelude::*,
|
||||
sea_query::{Alias, IntoCondition, OnConflict},
|
||||
ActiveValue, FromJsonQueryResult, IntoSimpleExpr, QuerySelect, entity::prelude::*,
|
||||
sea_query::OnConflict,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{bangumi, query::InsertManyReturningExt, subscription_bangumi, subscription_episode};
|
||||
use super::{bangumi, query::InsertManyReturningExt, subscription_episode};
|
||||
use crate::{
|
||||
app::AppContextTrait,
|
||||
errors::{RecorderError, RecorderResult},
|
||||
errors::RecorderResult,
|
||||
extract::{
|
||||
mikan::{MikanEpisodeHash, MikanEpisodeMeta, build_mikan_episode_homepage_url},
|
||||
rawname::parse_episode_meta_from_raw_name,
|
||||
@@ -52,8 +49,6 @@ pub struct Model {
|
||||
pub episode_index: i32,
|
||||
pub homepage: Option<String>,
|
||||
pub subtitle: Option<String>,
|
||||
#[sea_orm(default = "false")]
|
||||
pub deleted: bool,
|
||||
pub source: Option<String>,
|
||||
pub extra: EpisodeExtra,
|
||||
}
|
||||
@@ -132,56 +127,47 @@ pub enum RelatedEntity {
|
||||
SubscriptionEpisode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MikanEpsiodeCreation {
|
||||
pub episode: MikanEpisodeMeta,
|
||||
pub bangumi: Arc<bangumi::Model>,
|
||||
}
|
||||
|
||||
impl ActiveModel {
|
||||
pub fn from_mikan_episode_meta(
|
||||
#[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,
|
||||
creation: MikanEpsiodeCreation,
|
||||
bangumi: &bangumi::Model,
|
||||
episode: MikanEpisodeMeta,
|
||||
) -> RecorderResult<Self> {
|
||||
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_url(
|
||||
ctx.mikan().base_url().clone(),
|
||||
&item.mikan_episode_id,
|
||||
);
|
||||
let mikan_base_url = ctx.mikan().base_url().clone();
|
||||
let rawname_meta = parse_episode_meta_from_raw_name(&episode.episode_title)?;
|
||||
let homepage = build_mikan_episode_homepage_url(mikan_base_url, &episode.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
|
||||
mikan_episode_id: ActiveValue::Set(Some(episode.mikan_episode_id)),
|
||||
raw_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),
|
||||
resolution: ActiveValue::Set(rawname_meta.resolution),
|
||||
season: ActiveValue::Set(if rawname_meta.season > 0 {
|
||||
rawname_meta.season
|
||||
} else {
|
||||
bgm.season
|
||||
bangumi.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),
|
||||
season_raw: ActiveValue::Set(
|
||||
rawname_meta
|
||||
.season_raw
|
||||
.or_else(|| bangumi.season_raw.clone()),
|
||||
),
|
||||
fansub: ActiveValue::Set(rawname_meta.fansub.or_else(|| bangumi.fansub.clone())),
|
||||
poster_link: ActiveValue::Set(bangumi.poster_link.clone()),
|
||||
episode_index: ActiveValue::Set(rawname_meta.episode_index),
|
||||
homepage: ActiveValue::Set(Some(homepage.to_string())),
|
||||
subtitle: ActiveValue::Set(raw_meta.subtitle),
|
||||
source: ActiveValue::Set(raw_meta.source),
|
||||
subtitle: ActiveValue::Set(rawname_meta.subtitle),
|
||||
source: ActiveValue::Set(rawname_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,
|
||||
name_zh: rawname_meta.name_zh,
|
||||
name_en: rawname_meta.name_en,
|
||||
name_jp: rawname_meta.name_jp,
|
||||
s_name_en: rawname_meta.name_en_no_season,
|
||||
s_name_jp: rawname_meta.name_jp_no_season,
|
||||
s_name_zh: rawname_meta.name_zh_no_season,
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -197,13 +183,14 @@ impl Model {
|
||||
ids: impl Iterator<Item = MikanEpisodeHash>,
|
||||
subscriber_id: i32,
|
||||
_subscription_id: i32,
|
||||
) -> RecorderResult<impl Iterator<Item = (i32, MikanEpisodeHash)>> {
|
||||
) -> 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(),
|
||||
@@ -211,44 +198,39 @@ impl Model {
|
||||
])
|
||||
.in_tuples(
|
||||
ids.into_iter()
|
||||
.map(|id| (id.mikan_episode_token, subscriber_id)),
|
||||
.map(|id| (id.mikan_episode_id, subscriber_id)),
|
||||
),
|
||||
)
|
||||
.into_tuple::<(i32, String)>()
|
||||
.into_tuple::<(i32, String, i32)>()
|
||||
.all(db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(id, mikan_episode_id)| {
|
||||
.map(|(episode_id, mikan_episode_id, bangumi_id)| {
|
||||
(
|
||||
id,
|
||||
MikanEpisodeHash {
|
||||
mikan_episode_token: mikan_episode_id,
|
||||
},
|
||||
episode_id,
|
||||
MikanEpisodeHash { mikan_episode_id },
|
||||
bangumi_id,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn add_episodes(
|
||||
pub async fn add_mikan_episodes_for_subscription(
|
||||
ctx: &dyn AppContextTrait,
|
||||
creations: impl Iterator<Item = (&bangumi::Model, MikanEpisodeMeta)>,
|
||||
subscriber_id: i32,
|
||||
subscription_id: i32,
|
||||
creations: impl IntoIterator<Item = MikanEpsiodeCreation>,
|
||||
) -> RecorderResult<()> {
|
||||
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);
|
||||
}
|
||||
let new_episode_active_modes: Vec<ActiveModel> = creations
|
||||
.map(|(bangumi, episode_meta)| {
|
||||
ActiveModel::from_mikan_bangumi_and_episode_meta(ctx, bangumi, episode_meta)
|
||||
})
|
||||
.flatten();
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let inserted_episodes = Entity::insert_many(new_episode_active_modes)
|
||||
let new_episode_ids = Entity::insert_many(new_episode_active_modes)
|
||||
.on_conflict(
|
||||
OnConflict::columns([Column::BangumiId, Column::MikanEpisodeId])
|
||||
.do_nothing()
|
||||
OnConflict::columns([Column::MikanEpisodeId, Column::SubscriberId])
|
||||
.update_columns([Column::RawName, Column::PosterLink, Column::Homepage])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec_with_returning_columns(db, [Column::Id])
|
||||
@@ -256,25 +238,13 @@ impl Model {
|
||||
.into_iter()
|
||||
.flat_map(|r| r.try_get_many_by_index::<i32>());
|
||||
|
||||
let insert_subscription_episode_links = inserted_episodes.into_iter().map(|episode_id| {
|
||||
subscription_episode::ActiveModel::from_subscription_and_episode(
|
||||
subscriber_id,
|
||||
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?;
|
||||
subscription_episode::Model::add_episodes_for_subscription(
|
||||
ctx,
|
||||
new_episode_ids,
|
||||
subscriber_id,
|
||||
subscription_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ pub mod downloaders;
|
||||
pub mod downloads;
|
||||
pub mod episodes;
|
||||
pub mod query;
|
||||
pub mod subscriber_tasks;
|
||||
pub mod subscribers;
|
||||
pub mod subscription_bangumi;
|
||||
pub mod subscription_episode;
|
||||
|
||||
@@ -1,29 +1,9 @@
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, ConnectionTrait, DbErr, EntityTrait, Insert, IntoActiveModel,
|
||||
Iterable, QueryResult, QueryTrait, SelectModel, SelectorRaw, Value,
|
||||
prelude::Expr,
|
||||
sea_query::{Alias, IntoColumnRef, IntoTableRef, Query, SelectStatement},
|
||||
Iterable, QueryResult, QueryTrait, SelectModel, SelectorRaw, sea_query::Query,
|
||||
};
|
||||
|
||||
pub fn filter_values_in<
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<Value>,
|
||||
R: IntoTableRef,
|
||||
C: IntoColumnRef + Copy,
|
||||
>(
|
||||
tbl_ref: R,
|
||||
col_ref: C,
|
||||
values: I,
|
||||
) -> SelectStatement {
|
||||
Query::select()
|
||||
.expr(Expr::col(("t", "column1")))
|
||||
.from_values(values, "t")
|
||||
.left_join(tbl_ref, Expr::col(("t", "column1")).equals(col_ref))
|
||||
.and_where(Expr::col(col_ref).is_not_null())
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait InsertManyReturningExt<A>: Sized
|
||||
where
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use sea_orm::{ActiveValue, FromJsonQueryResult, JsonValue, TryIntoModel, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use crate::task::{SubscriberTaskType, SubscriberTaskTypeEnum};
|
||||
use crate::{
|
||||
app::AppContextTrait,
|
||||
errors::RecorderResult,
|
||||
task::{SubscriberTask, SubscriberTaskPayload},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromJsonQueryResult, PartialEq, Eq)]
|
||||
pub struct SubscriberTaskErrorSnapshot {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, DeriveEntityModel, PartialEq, Eq)]
|
||||
#[sea_orm(table_name = "subscriber_tasks")]
|
||||
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 subscriber_id: i32,
|
||||
pub task_type: SubscriberTaskType,
|
||||
pub request: JsonValue,
|
||||
pub yields: Vec<JsonValue>,
|
||||
pub result: Option<JsonValue>,
|
||||
pub error: Option<JsonValue>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::subscribers::Entity",
|
||||
from = "Column::SubscriberId",
|
||||
to = "super::subscribers::Column::Id"
|
||||
)]
|
||||
Subscriber,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl Model {
|
||||
pub async fn update_result<R>(
|
||||
ctx: Arc<dyn AppContextTrait>,
|
||||
task_id: i32,
|
||||
result: R,
|
||||
) -> RecorderResult<()>
|
||||
where
|
||||
R: Serialize,
|
||||
{
|
||||
let db = ctx.db();
|
||||
|
||||
let result_value = serde_json::to_value(result)?;
|
||||
|
||||
Entity::update_many()
|
||||
.filter(Column::Id.eq(task_id))
|
||||
.set(ActiveModel {
|
||||
result: ActiveValue::Set(Some(result_value)),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_error(
|
||||
ctx: Arc<dyn AppContextTrait>,
|
||||
task_id: i32,
|
||||
error: SubscriberTaskErrorSnapshot,
|
||||
) -> RecorderResult<()> {
|
||||
let db = ctx.db();
|
||||
|
||||
let error_value = serde_json::to_value(&error)?;
|
||||
|
||||
Entity::update_many()
|
||||
.filter(Column::Id.eq(task_id))
|
||||
.set(ActiveModel {
|
||||
error: ActiveValue::Set(Some(error_value)),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn append_yield<Y>(
|
||||
ctx: Arc<dyn AppContextTrait>,
|
||||
task_id: i32,
|
||||
item: Y,
|
||||
) -> RecorderResult<()>
|
||||
where
|
||||
Y: Serialize,
|
||||
{
|
||||
let db = ctx.db();
|
||||
|
||||
let yield_value = serde_json::to_value(item)?;
|
||||
|
||||
Entity::update_many()
|
||||
.filter(Column::Id.eq(task_id))
|
||||
.col_expr(
|
||||
Column::Yields,
|
||||
Expr::cust_with_values("array_append($1)", [yield_value]),
|
||||
)
|
||||
.exec(db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_subscriber_task(
|
||||
ctx: Arc<dyn AppContextTrait>,
|
||||
subscriber_id: i32,
|
||||
payload: SubscriberTaskPayload,
|
||||
) -> RecorderResult<SubscriberTask> {
|
||||
let task_type = payload.task_type();
|
||||
let request: JsonValue = payload.clone().try_into()?;
|
||||
|
||||
let am = ActiveModel {
|
||||
subscriber_id: ActiveValue::Set(subscriber_id),
|
||||
task_type: ActiveValue::Set(task_type.clone()),
|
||||
request: ActiveValue::Set(request.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let db = ctx.db();
|
||||
|
||||
let task_id = Entity::insert(am).exec(db).await?.last_insert_id;
|
||||
|
||||
let subscriber_task = SubscriberTask {
|
||||
id: task_id,
|
||||
subscriber_id,
|
||||
payload,
|
||||
};
|
||||
|
||||
ctx.task()
|
||||
.add_subscriber_task(subscriber_task.clone())
|
||||
.await?;
|
||||
|
||||
Ok(subscriber_task)
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,6 @@ pub enum Relation {
|
||||
Episode,
|
||||
#[sea_orm(has_many = "super::auth::Entity")]
|
||||
Auth,
|
||||
#[sea_orm(has_many = "super::subscriber_tasks::Entity")]
|
||||
SubscriberTasks,
|
||||
}
|
||||
|
||||
impl Related<super::subscriptions::Entity> for Entity {
|
||||
@@ -73,12 +71,6 @@ impl Related<super::auth::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::subscriber_tasks::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::SubscriberTasks.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
|
||||
pub enum RelatedEntity {
|
||||
#[sea_orm(entity = "super::subscriptions::Entity")]
|
||||
@@ -89,8 +81,6 @@ pub enum RelatedEntity {
|
||||
Bangumi,
|
||||
#[sea_orm(entity = "super::episodes::Entity")]
|
||||
Episode,
|
||||
#[sea_orm(entity = "super::subscriber_tasks::Entity")]
|
||||
SubscriberTasks,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
||||
@@ -57,21 +57,6 @@ pub enum RelatedEntity {
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl ActiveModel {
|
||||
pub fn from_subscription_and_episode(
|
||||
subscriber_id: i32,
|
||||
subscription_id: i32,
|
||||
episode_id: i32,
|
||||
) -> Self {
|
||||
Self {
|
||||
subscriber_id: ActiveValue::Set(subscriber_id),
|
||||
subscription_id: ActiveValue::Set(subscription_id),
|
||||
episode_id: ActiveValue::Set(episode_id),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub async fn add_episodes_for_subscription(
|
||||
ctx: &dyn AppContextTrait,
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use itertools::Itertools;
|
||||
use sea_orm::{ActiveValue, entity::prelude::*};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{bangumi, episodes, query::filter_values_in};
|
||||
use crate::{
|
||||
app::AppContextTrait,
|
||||
errors::{RecorderError, RecorderResult},
|
||||
extract::{
|
||||
mikan::{
|
||||
MikanBangumiPosterMeta, MikanBangumiSubscription, MikanSeasonSubscription,
|
||||
MikanSubscriberSubscription, build_mikan_bangumi_homepage_url,
|
||||
build_mikan_bangumi_subscription_rss_url,
|
||||
scrape_mikan_bangumi_meta_from_bangumi_homepage_url,
|
||||
scrape_mikan_episode_meta_from_episode_homepage_url,
|
||||
scrape_mikan_poster_meta_from_image_url,
|
||||
},
|
||||
rawname::extract_season_from_title_body,
|
||||
extract::mikan::{
|
||||
MikanBangumiSubscription, MikanSeasonSubscription, MikanSubscriberSubscription,
|
||||
},
|
||||
models::episodes::MikanEpsiodeCreation,
|
||||
};
|
||||
|
||||
#[derive(
|
||||
@@ -43,45 +32,6 @@ pub enum SubscriptionCategory {
|
||||
Manual,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "category")]
|
||||
pub enum SubscriptionPayload {
|
||||
#[serde(rename = "mikan_subscriber")]
|
||||
MikanSubscriber(MikanSubscriberSubscription),
|
||||
#[serde(rename = "mikan_season")]
|
||||
MikanSeason(MikanSeasonSubscription),
|
||||
#[serde(rename = "mikan_bangumi")]
|
||||
MikanBangumi(MikanBangumiSubscription),
|
||||
#[serde(rename = "manual")]
|
||||
Manual,
|
||||
}
|
||||
|
||||
impl SubscriptionPayload {
|
||||
pub fn category(&self) -> SubscriptionCategory {
|
||||
match self {
|
||||
Self::MikanSubscriber(_) => SubscriptionCategory::MikanSubscriber,
|
||||
Self::MikanSeason(_) => SubscriptionCategory::MikanSeason,
|
||||
Self::MikanBangumi(_) => SubscriptionCategory::MikanBangumi,
|
||||
Self::Manual => SubscriptionCategory::Manual,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_model(model: &Model) -> RecorderResult<Self> {
|
||||
Ok(match model.category {
|
||||
SubscriptionCategory::MikanSubscriber => {
|
||||
Self::MikanSubscriber(MikanSubscriberSubscription::try_from_model(model)?)
|
||||
}
|
||||
SubscriptionCategory::MikanSeason => {
|
||||
Self::MikanSeason(MikanSeasonSubscription::try_from_model(model)?)
|
||||
}
|
||||
SubscriptionCategory::MikanBangumi => {
|
||||
Self::MikanBangumi(MikanBangumiSubscription::try_from_model(model)?)
|
||||
}
|
||||
SubscriptionCategory::Manual => Self::Manual,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "subscriptions")]
|
||||
pub struct Model {
|
||||
@@ -95,6 +45,7 @@ pub struct Model {
|
||||
pub subscriber_id: i32,
|
||||
pub category: SubscriptionCategory,
|
||||
pub source_url: String,
|
||||
pub source_urls: Option<Vec<String>>,
|
||||
pub enabled: bool,
|
||||
pub credential_id: Option<i32>,
|
||||
}
|
||||
@@ -199,11 +150,6 @@ impl ActiveModelBehavior for ActiveModel {}
|
||||
impl ActiveModel {}
|
||||
|
||||
impl Model {
|
||||
pub async fn find_by_id(ctx: &dyn AppContextTrait, id: i32) -> RecorderResult<Option<Self>> {
|
||||
let db = ctx.db();
|
||||
Ok(Entity::find_by_id(id).one(db).await?)
|
||||
}
|
||||
|
||||
pub async fn toggle_with_ids(
|
||||
ctx: &dyn AppContextTrait,
|
||||
ids: impl Iterator<Item = i32>,
|
||||
@@ -230,127 +176,112 @@ impl Model {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn pull_subscription(&self, ctx: &dyn AppContextTrait) -> RecorderResult<()> {
|
||||
match payload {
|
||||
SubscriptionPayload::MikanSubscriber(payload) => {
|
||||
let mikan_client = ctx.mikan();
|
||||
let channel =
|
||||
extract_mikan_rss_channel_from_rss_link(mikan_client, &self.source_url).await?;
|
||||
|
||||
let items = channel.into_items();
|
||||
|
||||
let db = ctx.db();
|
||||
let items = items.into_iter().collect_vec();
|
||||
|
||||
let mut stmt = filter_values_in(
|
||||
episodes::Entity,
|
||||
episodes::Column::MikanEpisodeId,
|
||||
items
|
||||
.iter()
|
||||
.map(|s| Value::from(s.mikan_episode_id.clone())),
|
||||
);
|
||||
stmt.and_where(Expr::col(episodes::Column::SubscriberId).eq(self.subscriber_id));
|
||||
|
||||
let builder = &db.get_database_backend();
|
||||
|
||||
let old_rss_item_mikan_episode_ids_set = db
|
||||
.query_all(builder.build(&stmt))
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|qs| qs.try_get_by_index(0))
|
||||
.collect::<HashSet<String>>();
|
||||
|
||||
let new_rss_items = items
|
||||
.into_iter()
|
||||
.filter(|item| {
|
||||
!old_rss_item_mikan_episode_ids_set.contains(&item.mikan_episode_id)
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let mut new_metas = vec![];
|
||||
for new_rss_item in new_rss_items.iter() {
|
||||
new_metas.push(
|
||||
scrape_mikan_episode_meta_from_episode_homepage_url(
|
||||
mikan_client,
|
||||
new_rss_item.homepage.clone(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
let new_mikan_bangumi_groups = new_metas
|
||||
.into_iter()
|
||||
.into_group_map_by(|s| (s.mikan_bangumi_id.clone(), s.mikan_fansub_id.clone()));
|
||||
|
||||
for ((mikan_bangumi_id, mikan_fansub_id), new_ep_metas) in new_mikan_bangumi_groups
|
||||
{
|
||||
let mikan_base_url = ctx.mikan().base_url();
|
||||
let bgm_homepage = build_mikan_bangumi_homepage_url(
|
||||
mikan_base_url.clone(),
|
||||
&mikan_bangumi_id,
|
||||
Some(&mikan_fansub_id),
|
||||
);
|
||||
let bgm_rss_link = build_mikan_bangumi_subscription_rss_url(
|
||||
mikan_base_url.clone(),
|
||||
&mikan_bangumi_id,
|
||||
Some(&mikan_fansub_id),
|
||||
)?;
|
||||
let bgm = Arc::new(
|
||||
bangumi::Model::get_or_insert_from_mikan(
|
||||
ctx,
|
||||
self.subscriber_id,
|
||||
self.id,
|
||||
mikan_bangumi_id.to_string(),
|
||||
mikan_fansub_id.to_string(),
|
||||
async |am| -> RecorderResult<()> {
|
||||
let bgm_meta = scrape_mikan_bangumi_meta_from_bangumi_homepage_url(
|
||||
mikan_client,
|
||||
bgm_homepage.clone(),
|
||||
)
|
||||
.await?;
|
||||
let bgm_name = bgm_meta.bangumi_title;
|
||||
let (_, bgm_season_raw, bgm_season) =
|
||||
extract_season_from_title_body(&bgm_name);
|
||||
am.raw_name = ActiveValue::Set(bgm_name.clone());
|
||||
am.display_name = ActiveValue::Set(bgm_name);
|
||||
am.season = ActiveValue::Set(bgm_season);
|
||||
am.season_raw = ActiveValue::Set(bgm_season_raw);
|
||||
am.rss_link = ActiveValue::Set(Some(bgm_rss_link.to_string()));
|
||||
am.homepage = ActiveValue::Set(Some(bgm_homepage.to_string()));
|
||||
am.fansub = ActiveValue::Set(Some(bgm_meta.fansub));
|
||||
if let Some(origin_poster_src) = bgm_meta.origin_poster_src
|
||||
&& let MikanBangumiPosterMeta {
|
||||
poster_src: Some(poster_src),
|
||||
..
|
||||
} = scrape_mikan_poster_meta_from_image_url(
|
||||
mikan_client,
|
||||
ctx.storage(),
|
||||
origin_poster_src,
|
||||
self.subscriber_id,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
am.poster_link = ActiveValue::Set(Some(poster_src))
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
episodes::Model::add_episodes(
|
||||
ctx,
|
||||
self.subscriber_id,
|
||||
self.id,
|
||||
new_ep_metas.into_iter().map(|item| MikanEpsiodeCreation {
|
||||
episode: item,
|
||||
bangumi: bgm.clone(),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => todo!(),
|
||||
pub async fn sync_feeds(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
|
||||
let subscription = self.try_into()?;
|
||||
match subscription {
|
||||
Subscription::MikanSubscriber(subscription) => subscription.sync_feeds(ctx).await,
|
||||
Subscription::MikanSeason(subscription) => subscription.sync_feeds(ctx).await,
|
||||
Subscription::MikanBangumi(subscription) => subscription.sync_feeds(ctx).await,
|
||||
Subscription::Manual => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait SubscriptionTrait: Sized + Debug {
|
||||
fn get_subscriber_id(&self) -> i32;
|
||||
|
||||
fn get_subscription_id(&self) -> i32;
|
||||
|
||||
async fn sync_feeds(&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(&self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
|
||||
match self {
|
||||
Self::MikanSubscriber(subscription) => subscription.sync_feeds(ctx).await,
|
||||
Self::MikanSeason(subscription) => subscription.sync_feeds(ctx).await,
|
||||
Self::MikanBangumi(subscription) => subscription.sync_feeds(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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user