refactor: continue

This commit is contained in:
2025-05-13 01:23:59 +08:00
parent 760cb2344e
commit bf270e4e87
34 changed files with 1210 additions and 1427 deletions

View File

@@ -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(

View File

@@ -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(())
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)]

View File

@@ -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,

View File

@@ -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)
}
}