diff --git a/Cargo.lock b/Cargo.lock index dda531b..9a50a16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -165,6 +175,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -191,6 +207,98 @@ dependencies = [ "zstd-safe", ] +[[package]] +name = "async-graphql" +version = "7.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fd6bd734afb8b6e4d0f84a3e77305ce0a7ccc60d70f6001cb5e1c3f38d8ff1" +dependencies = [ + "async-graphql-derive", + "async-graphql-parser", + "async-graphql-value", + "async-stream", + "async-trait", + "base64 0.22.1", + "bytes", + "fast_chemail", + "fnv", + "futures-timer", + "futures-util", + "handlebars", + "http 1.2.0", + "indexmap 2.7.0", + "mime", + "multer", + "num-traits", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "static_assertions_next", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "async-graphql-axum" +version = "7.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8c1bb47161c37286e40e2fa58055e97b2a2b6cf1022a6686967e10636fa5d7" +dependencies = [ + "async-graphql", + "async-trait", + "axum", + "bytes", + "futures-util", + "serde_json", + "tokio", + "tokio-stream", + "tokio-util", + "tower-service", +] + +[[package]] +name = "async-graphql-derive" +version = "7.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac38b4dd452d529d6c0248b51df23603f0a875770352e26ae8c346ce6c149b3e" +dependencies = [ + "Inflector", + "async-graphql-parser", + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "strum", + "syn 2.0.92", + "thiserror 1.0.69", +] + +[[package]] +name = "async-graphql-parser" +version = "7.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d271ddda2f55b13970928abbcbc3423cfc18187c60e8769b48f21a93b7adaa" +dependencies = [ + "async-graphql-value", + "pest", + "serde", + "serde_json", +] + +[[package]] +name = "async-graphql-value" +version = "7.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aefe909173a037eaf3281b046dc22580b59a38b765d7b8d5116f2ffef098048d" +dependencies = [ + "bytes", + "indexmap 2.7.0", + "serde", + "serde_json", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -282,6 +390,7 @@ dependencies = [ "async-trait", "axum-core", "axum-macros", + "base64 0.22.1", "bytes", "futures-util", "http 1.2.0", @@ -300,8 +409,10 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", + "sha1", "sync_wrapper", "tokio", + "tokio-tungstenite", "tower 0.5.2", "tower-layer", "tower-service", @@ -722,6 +833,9 @@ name = "bytes" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +dependencies = [ + "serde", +] [[package]] name = "bytesize" @@ -1576,6 +1690,15 @@ dependencies = [ "regex-syntax 0.8.5", ] +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1768,6 +1891,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -1904,6 +2033,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "handlebars" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -4369,6 +4512,8 @@ name = "recorder" version = "0.1.0" dependencies = [ "anyhow", + "async-graphql", + "async-graphql-axum", "async-trait", "axum", "axum-auth", @@ -5874,6 +6019,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "static_assertions_next" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" + [[package]] name = "string_cache" version = "0.8.7" @@ -5945,6 +6096,22 @@ name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.92", +] [[package]] name = "subtle" @@ -6328,6 +6495,18 @@ dependencies = [ "xattr", ] +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -6336,6 +6515,7 @@ checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -6559,6 +6739,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.2.0", + "httparse", + "log", + "rand", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + [[package]] name = "typed-builder" version = "0.20.0" diff --git a/apps/recorder/Cargo.toml b/apps/recorder/Cargo.toml index a2eb6ce..b73a680 100644 --- a/apps/recorder/Cargo.toml +++ b/apps/recorder/Cargo.toml @@ -44,7 +44,7 @@ axum = "0.7.9" uuid = { version = "1.6.0", features = ["v4"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } sea-orm-migration = { version = "1", features = ["runtime-tokio-rustls"] } -reqwest = "0.12.9" +reqwest = { version = "0.12.9" } thiserror = "2" rss = "2" bytes = "1.9" @@ -85,6 +85,8 @@ testcontainers-modules = { version = "0.11.4", optional = true } log = "0.4.22" anyhow = "1.0.95" bollard = { version = "0.18", optional = true } +async-graphql = "7.0.13" +async-graphql-axum = "7.0.13" [dev-dependencies] diff --git a/apps/recorder/src/app.rs b/apps/recorder/src/app.rs index 88df8ba..974d8b6 100644 --- a/apps/recorder/src/app.rs +++ b/apps/recorder/src/app.rs @@ -20,7 +20,7 @@ use crate::{ dal::{AppDalClient, AppDalInitalizer}, extract::mikan::{client::AppMikanClientInitializer, AppMikanClient}, migrations::Migrator, - models::entities::subscribers, + models::subscribers, workers::subscription_worker::SubscriptionWorker, }; diff --git a/apps/recorder/src/auth/config.rs b/apps/recorder/src/auth/config.rs index 83ef12d..9382aa6 100644 --- a/apps/recorder/src/auth/config.rs +++ b/apps/recorder/src/auth/config.rs @@ -24,7 +24,7 @@ pub struct OidcAuthConfig { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] +#[serde(tag = "auth_type", rename_all = "snake_case")] pub enum AppAuthConfig { Basic(BasicAuthConfig), Oidc(OidcAuthConfig), diff --git a/apps/recorder/src/controllers/subscribers.rs b/apps/recorder/src/controllers/subscribers.rs index 2dbddbb..f4e3484 100644 --- a/apps/recorder/src/controllers/subscribers.rs +++ b/apps/recorder/src/controllers/subscribers.rs @@ -1,6 +1,6 @@ use loco_rs::prelude::*; -use crate::{models::entities::subscribers, views::subscribers::CurrentResponse}; +use crate::{models::subscribers, views::subscribers::CurrentResponse}; async fn current(State(ctx): State) -> Result { let subscriber = subscribers::Model::find_root(&ctx).await?; diff --git a/apps/recorder/src/migrations/defs.rs b/apps/recorder/src/migrations/defs.rs index 29db28a..6f0c025 100644 --- a/apps/recorder/src/migrations/defs.rs +++ b/apps/recorder/src/migrations/defs.rs @@ -38,7 +38,6 @@ pub enum Bangumi { Id, MikanBangumiId, DisplayName, - SubscriptionId, SubscriberId, RawName, Season, @@ -54,6 +53,14 @@ pub enum Bangumi { Extra, } +#[derive(DeriveIden)] +pub enum SubscriptionBangumi { + Table, + Id, + SubscriptionId, + BangumiId, +} + #[derive(DeriveIden)] pub enum Episodes { Table, @@ -62,7 +69,6 @@ pub enum Episodes { RawName, DisplayName, BangumiId, - SubscriptionId, SubscriberId, DownloadId, SavePath, @@ -79,13 +85,23 @@ pub enum Episodes { Extra, } +#[derive(DeriveIden)] +pub enum SubscriptionEpisode { + Table, + Id, + SubscriptionId, + EpisodeId, +} + #[derive(DeriveIden)] pub enum Downloads { Table, Id, - OriginalName, + RawName, DisplayName, - SubscriptionId, + SubscriberId, + DownloaderId, + EpisodeId, Status, CurrSize, AllSize, diff --git a/apps/recorder/src/migrations/m20220101_000001_init.rs b/apps/recorder/src/migrations/m20220101_000001_init.rs index a221e85..eb9ab52 100644 --- a/apps/recorder/src/migrations/m20220101_000001_init.rs +++ b/apps/recorder/src/migrations/m20220101_000001_init.rs @@ -2,7 +2,8 @@ use loco_rs::schema::jsonb_null; use sea_orm_migration::{prelude::*, schema::*}; use super::defs::{ - Bangumi, CustomSchemaManagerExt, Episodes, GeneralIds, Subscribers, Subscriptions, + Bangumi, CustomSchemaManagerExt, Episodes, GeneralIds, Subscribers, SubscriptionBangumi, + SubscriptionEpisode, Subscriptions, }; use crate::models::{ subscribers::SEED_SUBSCRIBER, @@ -37,12 +38,15 @@ impl MigrationTrait for Migration { ) .await?; - let insert = Query::insert() - .into_table(Subscribers::Table) - .columns([Subscribers::Pid, Subscribers::DisplayName]) - .values_panic([SEED_SUBSCRIBER.into(), SEED_SUBSCRIBER.into()]) - .to_owned(); - manager.exec_stmt(insert).await?; + manager + .exec_stmt( + Query::insert() + .into_table(Subscribers::Table) + .columns([Subscribers::Pid, Subscribers::DisplayName]) + .values_panic([SEED_SUBSCRIBER.into(), SEED_SUBSCRIBER.into()]) + .to_owned(), + ) + .await?; create_postgres_enum_for_active_enum!( manager, @@ -70,7 +74,7 @@ impl MigrationTrait for Migration { .name("fk_subscriptions_subscriber_id") .from(Subscriptions::Table, Subscriptions::SubscriberId) .to(Subscribers::Table, Subscribers::Id) - .on_update(ForeignKeyAction::Restrict) + .on_update(ForeignKeyAction::Cascade) .on_delete(ForeignKeyAction::Cascade), ) .to_owned(), @@ -89,7 +93,6 @@ impl MigrationTrait for Migration { table_auto(Bangumi::Table) .col(pk_auto(Bangumi::Id)) .col(text_null(Bangumi::MikanBangumiId)) - .col(integer(Bangumi::SubscriptionId)) .col(integer(Bangumi::SubscriberId)) .col(text(Bangumi::DisplayName)) .col(text(Bangumi::RawName)) @@ -104,22 +107,24 @@ impl MigrationTrait for Migration { .col(boolean(Bangumi::Deleted).default(false)) .col(text_null(Bangumi::Homepage)) .col(jsonb_null(Bangumi::Extra)) - .foreign_key( - ForeignKey::create() - .name("fk_bangumi_subscription_id") - .from(Bangumi::Table, Bangumi::SubscriptionId) - .to(Subscriptions::Table, Subscriptions::Id) - .on_update(ForeignKeyAction::Restrict) - .on_delete(ForeignKeyAction::Cascade), - ) .foreign_key( ForeignKey::create() .name("fk_bangumi_subscriber_id") .from(Bangumi::Table, Bangumi::SubscriberId) .to(Subscribers::Table, Subscribers::Id) - .on_update(ForeignKeyAction::Restrict) + .on_update(ForeignKeyAction::Cascade) .on_delete(ForeignKeyAction::Cascade), ) + .index( + Index::create() + .if_not_exists() + .name("idx_bangumi_mikan_bangumi_id_mikan_fansub_id_subscriber_id") + .table(Bangumi::Table) + .col(Bangumi::MikanBangumiId) + .col(Bangumi::MikanFansubId) + .col(Bangumi::SubscriberId) + .unique(), + ) .to_owned(), ) .await?; @@ -150,6 +155,44 @@ impl MigrationTrait for Migration { .create_postgres_auto_update_ts_trigger_for_col(Bangumi::Table, GeneralIds::UpdatedAt) .await?; + manager + .create_table( + table_auto(SubscriptionBangumi::Table) + .col(pk_auto(SubscriptionBangumi::Id)) + .col(integer(SubscriptionBangumi::SubscriptionId)) + .col(integer(SubscriptionBangumi::BangumiId)) + .foreign_key( + ForeignKey::create() + .name("fk_subscription_bangumi_subscription_id") + .from( + SubscriptionBangumi::Table, + SubscriptionBangumi::SubscriptionId, + ) + .to(Subscriptions::Table, Subscriptions::Id) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_subscription_bangumi_bangumi_id") + .from(SubscriptionBangumi::Table, SubscriptionBangumi::BangumiId) + .to(Bangumi::Table, Bangumi::Id) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade), + ) + .index( + Index::create() + .if_not_exists() + .name("constraint_subscription_bangumi_subscription_id_bangumi_id") + .table(SubscriptionBangumi::Table) + .col(SubscriptionBangumi::SubscriptionId) + .col(SubscriptionBangumi::BangumiId) + .unique(), + ) + .to_owned(), + ) + .await?; + manager .create_table( table_auto(Episodes::Table) @@ -158,7 +201,6 @@ impl MigrationTrait for Migration { .col(text(Episodes::RawName)) .col(text(Episodes::DisplayName)) .col(integer(Episodes::BangumiId)) - .col(integer(Episodes::SubscriptionId)) .col(integer(Episodes::SubscriberId)) .col(text_null(Episodes::SavePath)) .col(text_null(Episodes::Resolution)) @@ -172,20 +214,12 @@ impl MigrationTrait for Migration { .col(boolean(Episodes::Deleted).default(false)) .col(text_null(Episodes::Source)) .col(jsonb_null(Episodes::Extra)) - .foreign_key( - ForeignKey::create() - .name("fk_episodes_subscription_id") - .from(Episodes::Table, Episodes::SubscriptionId) - .to(Subscriptions::Table, Subscriptions::Id) - .on_update(ForeignKeyAction::Restrict) - .on_delete(ForeignKeyAction::Cascade), - ) .foreign_key( ForeignKey::create() .name("fk_episodes_bangumi_id") .from(Episodes::Table, Episodes::BangumiId) .to(Bangumi::Table, Bangumi::Id) - .on_update(ForeignKeyAction::Restrict) + .on_update(ForeignKeyAction::Cascade) .on_delete(ForeignKeyAction::Cascade), ) .foreign_key( @@ -193,7 +227,7 @@ impl MigrationTrait for Migration { .name("fk_episodes_subscriber_id") .from(Episodes::Table, Episodes::SubscriberId) .to(Subscribers::Table, Subscribers::Id) - .on_update(ForeignKeyAction::Restrict) + .on_update(ForeignKeyAction::Cascade) .on_delete(ForeignKeyAction::Cascade), ) .to_owned(), @@ -228,12 +262,50 @@ impl MigrationTrait for Migration { .create_postgres_auto_update_ts_trigger_for_col(Episodes::Table, GeneralIds::UpdatedAt) .await?; + manager + .create_table( + table_auto(SubscriptionEpisode::Table) + .col(pk_auto(SubscriptionEpisode::Id)) + .col(integer(SubscriptionEpisode::SubscriptionId)) + .col(integer(SubscriptionEpisode::EpisodeId)) + .foreign_key( + ForeignKey::create() + .name("fk_subscription_episode_subscription_id") + .from( + SubscriptionEpisode::Table, + SubscriptionEpisode::SubscriptionId, + ) + .to(Subscriptions::Table, Subscriptions::Id) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_subscription_episode_episode_id") + .from(SubscriptionEpisode::Table, SubscriptionEpisode::EpisodeId) + .to(Episodes::Table, Episodes::Id) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade), + ) + .index( + Index::create() + .if_not_exists() + .name("constraint_subscription_episode_subscription_id_episode_id") + .table(SubscriptionEpisode::Table) + .col(SubscriptionEpisode::SubscriptionId) + .col(SubscriptionEpisode::EpisodeId) + .unique(), + ) + .to_owned(), + ) + .await?; + Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(Episodes::Table).to_owned()) + .drop_table(Table::drop().table(SubscriptionEpisode::Table).to_owned()) .await?; manager @@ -241,7 +313,11 @@ impl MigrationTrait for Migration { .await?; manager - .drop_table(Table::drop().table(Bangumi::Table).to_owned()) + .drop_table(Table::drop().table(Episodes::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(SubscriptionBangumi::Table).to_owned()) .await?; manager @@ -249,7 +325,7 @@ impl MigrationTrait for Migration { .await?; manager - .drop_table(Table::drop().table(Subscriptions::Table).to_owned()) + .drop_table(Table::drop().table(Bangumi::Table).to_owned()) .await?; manager @@ -260,7 +336,7 @@ impl MigrationTrait for Migration { .await?; manager - .drop_table(Table::drop().table(Subscribers::Table).to_owned()) + .drop_table(Table::drop().table(Subscriptions::Table).to_owned()) .await?; manager @@ -268,17 +344,21 @@ impl MigrationTrait for Migration { .await?; manager - .drop_postgres_enum_for_active_enum(subscriptions::SubscriptionCategoryEnum) + .drop_table(Table::drop().table(Subscribers::Table).to_owned()) .await?; manager - .drop_postgres_auto_update_ts_fn_for_col(GeneralIds::UpdatedAt) + .drop_postgres_enum_for_active_enum(subscriptions::SubscriptionCategoryEnum) .await?; manager .drop_postgres_enum_for_active_enum(SubscriptionCategoryEnum) .await?; + manager + .drop_postgres_auto_update_ts_fn_for_col(GeneralIds::UpdatedAt) + .await?; + Ok(()) } } diff --git a/apps/recorder/src/migrations/m20240224_082543_add_downloads.rs b/apps/recorder/src/migrations/m20240224_082543_add_downloads.rs index 2f39eba..74781cb 100644 --- a/apps/recorder/src/migrations/m20240224_082543_add_downloads.rs +++ b/apps/recorder/src/migrations/m20240224_082543_add_downloads.rs @@ -2,9 +2,12 @@ use loco_rs::schema::table_auto; use sea_orm_migration::{prelude::*, schema::*}; use super::defs::*; -use crate::models::prelude::{ - downloads::{DownloadMimeEnum, DownloadStatusEnum}, - DownloadMime, DownloadStatus, +use crate::models::{ + downloaders::DownloaderCategoryEnum, + prelude::{ + downloads::{DownloadMimeEnum, DownloadStatusEnum}, + DownloadMime, DownloadStatus, DownloaderCategory, + }, }; #[derive(DeriveMigrationName)] @@ -13,6 +16,48 @@ pub struct Migration; #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + create_postgres_enum_for_active_enum!( + manager, + DownloaderCategoryEnum, + DownloaderCategory::QBittorrent + ) + .await?; + + manager + .create_table( + table_auto(Downloaders::Table) + .col(pk_auto(Downloaders::Id)) + .col(text(Downloaders::Endpoint)) + .col(string_null(Downloaders::Username)) + .col(string_null(Downloaders::Password)) + .col(enumeration( + Downloaders::Category, + DownloaderCategoryEnum, + DownloaderCategory::iden_values(), + )) + .col(text(Downloaders::SavePath)) + .col(integer(Downloaders::SubscriberId)) + .foreign_key( + ForeignKey::create() + .name("fk_downloader_subscriber_id") + .from_tbl(Downloaders::Table) + .from_col(Downloaders::SubscriberId) + .to_tbl(Subscribers::Table) + .to_col(Subscribers::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_postgres_auto_update_ts_trigger_for_col( + Downloaders::Table, + GeneralIds::UpdatedAt, + ) + .await?; + create_postgres_enum_for_active_enum!( manager, DownloadMimeEnum, @@ -37,9 +82,11 @@ impl MigrationTrait for Migration { .create_table( table_auto(Downloads::Table) .col(pk_auto(Downloads::Id)) - .col(string(Downloads::OriginalName)) + .col(string(Downloads::RawName)) .col(string(Downloads::DisplayName)) - .col(integer(Downloads::SubscriptionId)) + .col(integer(Downloads::SubscriberId)) + .col(integer(Downloads::DownloaderId)) + .col(integer(Downloads::EpisodeId)) .col(enumeration( Downloads::Status, DownloadStatusEnum, @@ -57,16 +104,42 @@ impl MigrationTrait for Migration { .col(text_null(Downloads::SavePath)) .foreign_key( ForeignKey::create() - .name("fk_downloads_subscription_id") - .from(Downloads::Table, Downloads::SubscriptionId) - .to(Subscriptions::Table, Subscriptions::Id) - .on_update(ForeignKeyAction::Restrict) - .on_delete(ForeignKeyAction::Cascade), + .name("fk_downloads_subscriber_id") + .from_tbl(Downloads::Table) + .from_col(Downloads::SubscriberId) + .to_tbl(Subscribers::Table) + .to_col(Subscribers::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_downloads_downloader_id") + .from_tbl(Downloads::Table) + .from_col(Downloads::DownloaderId) + .to_tbl(Downloaders::Table) + .to_col(Downloaders::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_downloads_episode_id") + .from_tbl(Downloads::Table) + .from_col(Downloads::EpisodeId) + .to_tbl(Episodes::Table) + .to_col(Episodes::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), ) .to_owned(), ) .await?; + manager + .create_postgres_auto_update_ts_trigger_for_col(Downloads::Table, GeneralIds::UpdatedAt) + .await?; + manager .create_index( Index::create() @@ -78,37 +151,12 @@ impl MigrationTrait for Migration { ) .await?; - manager - .alter_table( - Table::alter() - .table(Episodes::Table) - .add_column_if_not_exists(integer_null(Episodes::DownloadId)) - .add_foreign_key( - TableForeignKey::new() - .name("fk_episodes_download_id") - .from_tbl(Episodes::Table) - .from_col(Episodes::DownloadId) - .to_tbl(Downloads::Table) - .to_col(Downloads::Id) - .on_update(ForeignKeyAction::Restrict) - .on_delete(ForeignKeyAction::SetNull), - ) - .to_owned(), - ) - .await?; - Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .alter_table( - Table::alter() - .table(Episodes::Table) - .drop_foreign_key(Alias::new("fk_episodes_download_id")) - .drop_column(Episodes::DownloadId) - .to_owned(), - ) + .drop_postgres_auto_update_ts_trigger_for_col(Downloads::Table, GeneralIds::UpdatedAt) .await?; manager @@ -123,6 +171,18 @@ impl MigrationTrait for Migration { .drop_postgres_enum_for_active_enum(DownloadStatusEnum) .await?; + manager + .drop_postgres_auto_update_ts_trigger_for_col(Downloaders::Table, GeneralIds::UpdatedAt) + .await?; + + manager + .drop_table(Table::drop().table(Downloaders::Table).to_owned()) + .await?; + + manager + .drop_postgres_enum_for_active_enum(DownloaderCategoryEnum) + .await?; + Ok(()) } } diff --git a/apps/recorder/src/migrations/m20241231_000001_auth.rs b/apps/recorder/src/migrations/m20241231_000001_auth.rs index 660e8ce..4cf90b7 100644 --- a/apps/recorder/src/migrations/m20241231_000001_auth.rs +++ b/apps/recorder/src/migrations/m20241231_000001_auth.rs @@ -3,7 +3,10 @@ use sea_orm_migration::{prelude::*, schema::*}; use super::defs::Auth; use crate::{ migrations::defs::{CustomSchemaManagerExt, GeneralIds, Subscribers}, - models::auth::{AuthType, AuthTypeEnum}, + models::{ + auth::{AuthType, AuthTypeEnum}, + subscribers::SEED_SUBSCRIBER, + }, }; #[derive(DeriveMigrationName)] @@ -40,7 +43,7 @@ impl MigrationTrait for Migration { .to_tbl(Subscribers::Table) .to_col(Subscribers::Id) .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Restrict), + .on_update(ForeignKeyAction::Cascade), ) .to_owned(), ) @@ -62,6 +65,20 @@ impl MigrationTrait for Migration { .create_postgres_auto_update_ts_trigger_for_col(Auth::Table, GeneralIds::UpdatedAt) .await?; + manager + .exec_stmt( + Query::insert() + .into_table(Auth::Table) + .columns([Auth::Pid, Auth::AuthType, Auth::SubscriberId]) + .values_panic([ + SEED_SUBSCRIBER.into(), + SimpleExpr::from(AuthType::Basic).as_enum(AuthTypeEnum), + 1.into(), + ]) + .to_owned(), + ) + .await?; + Ok(()) } diff --git a/apps/recorder/src/migrations/mod.rs b/apps/recorder/src/migrations/mod.rs index 5a84f45..cc9850f 100644 --- a/apps/recorder/src/migrations/mod.rs +++ b/apps/recorder/src/migrations/mod.rs @@ -4,7 +4,6 @@ pub use sea_orm_migration::prelude::*; pub mod defs; pub mod m20220101_000001_init; pub mod m20240224_082543_add_downloads; -pub mod m20240225_060853_subscriber_add_downloader; pub mod m20241231_000001_auth; pub struct Migrator; @@ -15,7 +14,6 @@ impl MigratorTrait for Migrator { vec![ Box::new(m20220101_000001_init::Migration), Box::new(m20240224_082543_add_downloads::Migration), - Box::new(m20240225_060853_subscriber_add_downloader::Migration), Box::new(m20241231_000001_auth::Migration), ] } diff --git a/apps/recorder/src/models/auth.rs b/apps/recorder/src/models/auth.rs index 9d7edb5..db3e6bc 100644 --- a/apps/recorder/src/models/auth.rs +++ b/apps/recorder/src/models/auth.rs @@ -1,6 +1,54 @@ use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -pub use super::entities::auth::*; +#[derive( + Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize, +)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "auth_type")] +#[serde(rename_all = "snake_case")] +pub enum AuthType { + #[sea_orm(string_value = "basic")] + Basic, + #[sea_orm(string_value = "oidc")] + Oidc, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)] +#[sea_orm(table_name = "auth")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub pid: String, + pub subscriber_id: i32, + pub auth_type: AuthType, + pub avatar_url: Option, +} + +#[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" + )] + SubscriberId, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SubscriberId.def() + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] +pub enum RelatedEntity { + #[sea_orm(entity = "super::subscribers::Entity")] + Subscriber, +} #[async_trait::async_trait] impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/recorder/src/models/bangumi.rs b/apps/recorder/src/models/bangumi.rs index 6583f85..3233317 100644 --- a/apps/recorder/src/models/bangumi.rs +++ b/apps/recorder/src/models/bangumi.rs @@ -1,7 +1,87 @@ use loco_rs::app::AppContext; -use sea_orm::{entity::prelude::*, ActiveValue, TryIntoModel}; +use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue, FromJsonQueryResult}; +use serde::{Deserialize, Serialize}; -pub use super::entities::bangumi::*; +use super::subscription_bangumi; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)] +pub struct BangumiFilter { + pub name: Option>, + pub group: Option>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)] +pub struct BangumiExtra { + 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 = "bangumi")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub mikan_bangumi_id: Option, + pub subscriber_id: i32, + pub display_name: String, + pub raw_name: String, + pub season: i32, + pub season_raw: Option, + pub fansub: Option, + pub mikan_fansub_id: Option, + pub filter: Option, + pub rss_link: Option, + pub poster_link: Option, + pub save_path: Option, + #[sea_orm(default = "false")] + pub deleted: bool, + pub homepage: Option, + pub extra: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::subscriptions::Entity")] + Subscription, + #[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::episodes::Entity")] + Episode, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Episode.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::subscription_bangumi::Relation::Subscription.def() + } + + fn via() -> Option { + Some(super::subscription_bangumi::Relation::Bangumi.def().rev()) + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Subscriber.def() + } +} impl Model { pub async fn get_or_insert_from_mikan( @@ -30,12 +110,37 @@ impl Model { let mut bgm = ActiveModel { mikan_bangumi_id: ActiveValue::Set(Some(mikan_bangumi_id)), mikan_fansub_id: ActiveValue::Set(Some(mikan_fansub_id)), - subscription_id: ActiveValue::Set(subscription_id), subscriber_id: ActiveValue::Set(subscriber_id), ..Default::default() }; f(&mut bgm).await?; - let bgm: Model = bgm.save(db).await?.try_into_model()?; + let bgm = Entity::insert(bgm) + .on_conflict( + OnConflict::columns([ + Column::MikanBangumiId, + Column::MikanFansubId, + Column::SubscriberId, + ]) + .update_columns([ + Column::RawName, + Column::Extra, + Column::Fansub, + Column::PosterLink, + Column::Season, + Column::SeasonRaw, + ]) + .to_owned(), + ) + .exec_with_returning(db) + .await?; + subscription_bangumi::Entity::insert(subscription_bangumi::ActiveModel { + subscription_id: ActiveValue::Set(subscription_id), + bangumi_id: ActiveValue::Set(bgm.id), + ..Default::default() + }) + .on_conflict_do_nothing() + .exec(db) + .await?; Ok(bgm) } } diff --git a/apps/recorder/src/models/downloaders.rs b/apps/recorder/src/models/downloaders.rs index 5fc35aa..101cdde 100644 --- a/apps/recorder/src/models/downloaders.rs +++ b/apps/recorder/src/models/downloaders.rs @@ -1,7 +1,59 @@ -use sea_orm::prelude::*; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; use url::Url; -pub use crate::models::entities::downloaders::*; +#[derive( + Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize, +)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "downloader_type")] +#[serde(rename_all = "snake_case")] +pub enum DownloaderCategory { + #[sea_orm(string_value = "qbittorrent")] + QBittorrent, +} + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "downloaders")] +pub struct Model { + #[sea_orm(column_type = "Timestamp")] + pub created_at: DateTime, + #[sea_orm(column_type = "Timestamp")] + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub category: DownloaderCategory, + pub endpoint: String, + pub password: String, + pub username: String, + pub subscriber_id: i32, + pub save_path: 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(has_many = "super::downloads::Entity")] + Download, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Subscriber.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Download.def() + } +} #[async_trait::async_trait] impl ActiveModelBehavior for ActiveModel {} @@ -10,6 +62,7 @@ impl Model { pub fn get_endpoint(&self) -> String { self.endpoint.clone() } + pub fn endpoint_url(&self) -> Result { let url = Url::parse(&self.endpoint)?; Ok(url) diff --git a/apps/recorder/src/models/downloads.rs b/apps/recorder/src/models/downloads.rs index b7ccfa6..162ae3e 100644 --- a/apps/recorder/src/models/downloads.rs +++ b/apps/recorder/src/models/downloads.rs @@ -1,27 +1,107 @@ -use sea_orm::{prelude::*, ActiveValue}; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -use crate::extract::mikan::MikanRssItem; -pub use crate::models::entities::downloads::*; +#[derive( + Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize, +)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "download_status")] +#[serde(rename_all = "snake_case")] +pub enum DownloadStatus { + #[sea_orm(string_value = "pending")] + Pending, + #[sea_orm(string_value = "downloading")] + Downloading, + #[sea_orm(string_value = "paused")] + Paused, + #[sea_orm(string_value = "completed")] + Completed, + #[sea_orm(string_value = "failed")] + Failed, + #[sea_orm(string_value = "deleted")] + Deleted, +} + +#[derive( + Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay, Serialize, Deserialize, +)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "download_mime")] +pub enum DownloadMime { + #[sea_orm(string_value = "application/octet-stream")] + #[serde(rename = "application/octet-stream")] + OctetStream, + #[sea_orm(string_value = "application/x-bittorrent")] + #[serde(rename = "application/x-bittorrent")] + BitTorrent, +} + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "downloads")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub raw_name: String, + pub display_name: String, + pub downloader_id: i32, + pub episode_id: i32, + pub subscriber_id: i32, + pub status: DownloadStatus, + pub mime: DownloadMime, + pub url: String, + pub all_size: Option, + pub curr_size: Option, + pub homepage: Option, + pub save_path: Option, +} + +#[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::downloaders::Entity", + from = "Column::DownloaderId", + to = "super::downloaders::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Downloader, + #[sea_orm( + belongs_to = "super::episodes::Entity", + from = "Column::EpisodeId", + to = "super::episodes::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Episode, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Subscriber.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Downloader.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Episode.def() + } +} #[async_trait::async_trait] impl ActiveModelBehavior for ActiveModel {} -impl ActiveModel { - pub fn from_mikan_rss_item(m: MikanRssItem, subscription_id: i32) -> Self { - let _ = Self { - origin_name: ActiveValue::Set(m.title.clone()), - display_name: ActiveValue::Set(m.title), - subscription_id: ActiveValue::Set(subscription_id), - status: ActiveValue::Set(DownloadStatus::Pending), - mime: ActiveValue::Set(DownloadMime::BitTorrent), - url: ActiveValue::Set(m.url.to_string()), - curr_size: ActiveValue::Set(m.content_length.as_ref().map(|_| 0)), - all_size: ActiveValue::Set(m.content_length), - homepage: ActiveValue::Set(Some(m.homepage.to_string())), - ..Default::default() - }; - todo!() - } -} - -impl Model {} +impl ActiveModel {} diff --git a/apps/recorder/src/models/episodes.rs b/apps/recorder/src/models/episodes.rs index cd94364..2295ad4 100644 --- a/apps/recorder/src/models/episodes.rs +++ b/apps/recorder/src/models/episodes.rs @@ -1,10 +1,10 @@ use std::sync::Arc; use loco_rs::app::AppContext; -use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue}; +use sea_orm::{entity::prelude::*, sea_query::OnConflict, ActiveValue, FromJsonQueryResult}; +use serde::{Deserialize, Serialize}; -use super::bangumi; -pub use super::entities::episodes::*; +use super::{bangumi, query::InsertManyReturningExt, subscription_episode}; use crate::{ app::AppContextExt, extract::{ @@ -13,6 +13,92 @@ use crate::{ }, }; +#[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")] + Subscriptions, + #[sea_orm(has_one = "super::downloads::Entity")] + Downloads, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Bangumi.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Downloads.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Subscriptions.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Subscriber.def() + } +} + #[derive(Clone, Debug, PartialEq)] pub struct MikanEpsiodeCreation { pub episode: MikanEpisodeMeta, @@ -22,6 +108,7 @@ pub struct MikanEpsiodeCreation { impl Model { pub async fn add_episodes( ctx: &AppContext, + subscription_id: i32, creations: impl IntoIterator, ) -> eyre::Result<()> { let db = &ctx.db; @@ -35,13 +122,33 @@ impl Model { }) .flatten(); - Entity::insert_many(new_episode_active_modes) + let inserted_episodes = Entity::insert_many(new_episode_active_modes) .on_conflict( OnConflict::columns([Column::BangumiId, Column::MikanEpisodeId]) .do_nothing() .to_owned(), ) - .on_empty_do_nothing() + .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?; @@ -72,7 +179,6 @@ impl ActiveModel { raw_name: ActiveValue::Set(item.episode_title.clone()), display_name: ActiveValue::Set(item.episode_title.clone()), bangumi_id: ActiveValue::Set(bgm.id), - subscription_id: ActiveValue::Set(bgm.subscription_id), subscriber_id: ActiveValue::Set(bgm.subscriber_id), resolution: ActiveValue::Set(raw_meta.resolution), season: ActiveValue::Set(if raw_meta.season > 0 { diff --git a/apps/recorder/src/models/mod.rs b/apps/recorder/src/models/mod.rs index 5d5bdb4..1dc7407 100644 --- a/apps/recorder/src/models/mod.rs +++ b/apps/recorder/src/models/mod.rs @@ -2,10 +2,10 @@ pub mod auth; pub mod bangumi; pub mod downloaders; pub mod downloads; -pub mod entities; pub mod episodes; -pub mod notifications; pub mod prelude; pub mod query; pub mod subscribers; +pub mod subscription_bangumi; +pub mod subscription_episode; pub mod subscriptions; diff --git a/apps/recorder/src/models/query/mod.rs b/apps/recorder/src/models/query/mod.rs index 5fa270e..1093dc3 100644 --- a/apps/recorder/src/models/query/mod.rs +++ b/apps/recorder/src/models/query/mod.rs @@ -1,7 +1,8 @@ use sea_orm::{ prelude::Expr, sea_query::{Alias, IntoColumnRef, IntoTableRef, Query, SelectStatement}, - Value, + ActiveModelTrait, ColumnTrait, ConnectionTrait, DbErr, EntityTrait, Insert, IntoActiveModel, + Iterable, QueryResult, QueryTrait, SelectModel, SelectorRaw, Value, }; pub fn filter_values_in< @@ -24,3 +25,76 @@ pub fn filter_values_in< .and_where(Expr::col(col_ref).is_not_null()) .to_owned() } + +#[async_trait::async_trait] +pub trait InsertManyReturningExt: Sized +where + ::Model: IntoActiveModel, + A: ActiveModelTrait, +{ + fn exec_with_returning_models( + self, + db: &C, + ) -> SelectorRaw::Model>> + where + C: ConnectionTrait; + + async fn exec_with_returning_columns( + self, + db: &C, + columns: I, + ) -> Result, DbErr> + where + C: ConnectionTrait, + I: IntoIterator::Column> + Send; +} + +#[async_trait::async_trait] +impl InsertManyReturningExt for Insert +where + ::Model: IntoActiveModel, + A: ActiveModelTrait + Send, +{ + fn exec_with_returning_models( + self, + db: &C, + ) -> SelectorRaw::Model>> + where + C: ConnectionTrait, + { + let mut insert_statement = self.into_query(); + let db_backend = db.get_database_backend(); + let returning = Query::returning().exprs( + ::Column::iter() + .map(|c| c.select_as(c.into_returning_expr(db_backend))), + ); + insert_statement.returning(returning); + let insert_statement = db_backend.build(&insert_statement); + SelectorRaw::::Model>>::from_statement( + insert_statement, + ) + } + + async fn exec_with_returning_columns( + self, + db: &C, + columns: I, + ) -> Result, DbErr> + where + C: ConnectionTrait, + I: IntoIterator::Column> + Send, + { + let mut insert_statement = self.into_query(); + let db_backend = db.get_database_backend(); + let returning = Query::returning().exprs( + columns + .into_iter() + .map(|c| c.select_as(c.into_returning_expr(db_backend))), + ); + insert_statement.returning(returning); + + let statement = db_backend.build(&insert_statement); + + db.query_all(statement).await + } +} diff --git a/apps/recorder/src/models/subscribers.rs b/apps/recorder/src/models/subscribers.rs index 33ad6bb..99f6d43 100644 --- a/apps/recorder/src/models/subscribers.rs +++ b/apps/recorder/src/models/subscribers.rs @@ -2,13 +2,73 @@ use loco_rs::{ app::AppContext, model::{ModelError, ModelResult}, }; -use sea_orm::{entity::prelude::*, ActiveValue, TransactionTrait}; +use sea_orm::{entity::prelude::*, ActiveValue, FromJsonQueryResult, TransactionTrait}; use serde::{Deserialize, Serialize}; -pub use super::entities::subscribers::*; - pub const SEED_SUBSCRIBER: &str = "konobangu"; +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)] +pub struct SubscriberBangumiConfig { + pub leading_group_tag: Option, +} + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "subscribers")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub pid: String, + pub display_name: String, + pub bangumi_conf: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::subscriptions::Entity")] + Subscription, + #[sea_orm(has_many = "super::downloaders::Entity")] + Downloader, + #[sea_orm(has_many = "super::bangumi::Entity")] + Bangumi, + #[sea_orm(has_many = "super::episodes::Entity")] + Episode, + #[sea_orm(has_many = "super::auth::Entity")] + Auth, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Subscription.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Downloader.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Bangumi.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Episode.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Auth.def() + } +} + #[derive(Debug, Deserialize, Serialize)] pub struct SubscriberIdParams { pub id: String, diff --git a/apps/recorder/src/models/subscription_bangumi.rs b/apps/recorder/src/models/subscription_bangumi.rs new file mode 100644 index 0000000..547ea03 --- /dev/null +++ b/apps/recorder/src/models/subscription_bangumi.rs @@ -0,0 +1,56 @@ +use sea_orm::{entity::prelude::*, ActiveValue}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "subscription_bangumi")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub subscription_id: i32, + pub bangumi_id: 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::bangumi::Entity", + from = "Column::BangumiId", + to = "super::bangumi::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Bangumi, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Subscription.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Bangumi.def() + } +} + +#[async_trait::async_trait] +impl ActiveModelBehavior for ActiveModel {} + +impl ActiveModel { + pub fn from_subscription_and_bangumi(subscription_id: i32, bangumi_id: i32) -> Self { + Self { + subscription_id: ActiveValue::Set(subscription_id), + bangumi_id: ActiveValue::Set(bangumi_id), + ..Default::default() + } + } +} diff --git a/apps/recorder/src/models/subscription_episode.rs b/apps/recorder/src/models/subscription_episode.rs new file mode 100644 index 0000000..3e99997 --- /dev/null +++ b/apps/recorder/src/models/subscription_episode.rs @@ -0,0 +1,56 @@ +use sea_orm::{entity::prelude::*, ActiveValue}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "subscription_episode")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub subscription_id: i32, + pub episode_id: 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::episodes::Entity", + from = "Column::EpisodeId", + to = "super::episodes::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Episode, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Subscription.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Episode.def() + } +} + +#[async_trait::async_trait] +impl ActiveModelBehavior for ActiveModel {} + +impl ActiveModel { + pub fn from_subscription_and_episode(subscription_id: i32, episode_id: i32) -> Self { + Self { + subscription_id: ActiveValue::Set(subscription_id), + episode_id: ActiveValue::Set(episode_id), + ..Default::default() + } + } +} diff --git a/apps/recorder/src/models/subscriptions.rs b/apps/recorder/src/models/subscriptions.rs index 490d6ad..9887b8a 100644 --- a/apps/recorder/src/models/subscriptions.rs +++ b/apps/recorder/src/models/subscriptions.rs @@ -5,7 +5,6 @@ use loco_rs::app::AppContext; use sea_orm::{entity::prelude::*, ActiveValue}; use serde::{Deserialize, Serialize}; -pub use super::entities::subscriptions::{self, *}; use super::{bangumi, episodes, query::filter_values_in}; use crate::{ app::AppContextExt, @@ -24,6 +23,92 @@ use crate::{ models::episodes::MikanEpsiodeCreation, }; +#[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")] + Mikan, + #[sea_orm(string_value = "manual")] + Manual, +} + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "subscriptions")] +pub struct Model { + #[sea_orm(column_type = "Timestamp")] + pub created_at: DateTime, + #[sea_orm(column_type = "Timestamp")] + pub updated_at: DateTime, + #[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, +} + +#[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, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Subscriber.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::subscription_bangumi::Relation::Bangumi.def() + } + + fn via() -> Option { + Some( + super::subscription_bangumi::Relation::Subscription + .def() + .rev(), + ) + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::subscription_episode::Relation::Episode.def() + } + + fn via() -> Option { + Some( + super::subscription_episode::Relation::Subscription + .def() + .rev(), + ) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct SubscriptionCreateFromRssDto { pub rss_link: String, @@ -77,7 +162,7 @@ impl Model { Ok(subscription.insert(db).await?) } - pub async fn toggle_iters( + pub async fn toggle_with_ids( ctx: &AppContext, ids: impl Iterator, enabled: bool, @@ -91,7 +176,7 @@ impl Model { Ok(()) } - pub async fn delete_iters( + pub async fn delete_with_ids( ctx: &AppContext, ids: impl Iterator, ) -> eyre::Result<()> { @@ -213,6 +298,7 @@ impl Model { ); episodes::Model::add_episodes( ctx, + self.id, new_ep_metas.into_iter().map(|item| MikanEpsiodeCreation { episode: item, bangumi: bgm.clone(), diff --git a/apps/recorder/src/views/subscribers.rs b/apps/recorder/src/views/subscribers.rs index b6d11e0..0aa3498 100644 --- a/apps/recorder/src/views/subscribers.rs +++ b/apps/recorder/src/views/subscribers.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::models::entities::subscribers; +use crate::models::subscribers; #[derive(Debug, Deserialize, Serialize)] pub struct CurrentResponse {