refactor: fix database

This commit is contained in:
master 2025-01-03 05:32:25 +08:00
parent 70932900cd
commit caaa5dc0cc
22 changed files with 1223 additions and 128 deletions

198
Cargo.lock generated
View File

@ -2,6 +2,16 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.24.2" version = "0.24.2"
@ -165,6 +175,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "ascii_utils"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
[[package]] [[package]]
name = "assert-json-diff" name = "assert-json-diff"
version = "2.0.2" version = "2.0.2"
@ -191,6 +207,98 @@ dependencies = [
"zstd-safe", "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]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.6" version = "0.3.6"
@ -282,6 +390,7 @@ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
"axum-macros", "axum-macros",
"base64 0.22.1",
"bytes", "bytes",
"futures-util", "futures-util",
"http 1.2.0", "http 1.2.0",
@ -300,8 +409,10 @@ dependencies = [
"serde_json", "serde_json",
"serde_path_to_error", "serde_path_to_error",
"serde_urlencoded", "serde_urlencoded",
"sha1",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-tungstenite",
"tower 0.5.2", "tower 0.5.2",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
@ -722,6 +833,9 @@ name = "bytes"
version = "1.9.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bytesize" name = "bytesize"
@ -1576,6 +1690,15 @@ dependencies = [
"regex-syntax 0.8.5", "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]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@ -1768,6 +1891,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.31" version = "0.3.31"
@ -1904,6 +2033,20 @@ dependencies = [
"tracing", "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]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@ -4369,6 +4512,8 @@ name = "recorder"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-graphql",
"async-graphql-axum",
"async-trait", "async-trait",
"axum", "axum",
"axum-auth", "axum-auth",
@ -5874,6 +6019,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "static_assertions_next"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766"
[[package]] [[package]]
name = "string_cache" name = "string_cache"
version = "0.8.7" version = "0.8.7"
@ -5945,6 +6096,22 @@ name = "strum"
version = "0.26.3" version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 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]] [[package]]
name = "subtle" name = "subtle"
@ -6328,6 +6495,18 @@ dependencies = [
"xattr", "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]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.13" version = "0.7.13"
@ -6336,6 +6515,7 @@ checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
"futures-io",
"futures-sink", "futures-sink",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
@ -6559,6 +6739,24 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 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]] [[package]]
name = "typed-builder" name = "typed-builder"
version = "0.20.0" version = "0.20.0"

View File

@ -44,7 +44,7 @@ axum = "0.7.9"
uuid = { version = "1.6.0", features = ["v4"] } uuid = { version = "1.6.0", features = ["v4"] }
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
sea-orm-migration = { version = "1", features = ["runtime-tokio-rustls"] } sea-orm-migration = { version = "1", features = ["runtime-tokio-rustls"] }
reqwest = "0.12.9" reqwest = { version = "0.12.9" }
thiserror = "2" thiserror = "2"
rss = "2" rss = "2"
bytes = "1.9" bytes = "1.9"
@ -85,6 +85,8 @@ testcontainers-modules = { version = "0.11.4", optional = true }
log = "0.4.22" log = "0.4.22"
anyhow = "1.0.95" anyhow = "1.0.95"
bollard = { version = "0.18", optional = true } bollard = { version = "0.18", optional = true }
async-graphql = "7.0.13"
async-graphql-axum = "7.0.13"
[dev-dependencies] [dev-dependencies]

View File

@ -20,7 +20,7 @@ use crate::{
dal::{AppDalClient, AppDalInitalizer}, dal::{AppDalClient, AppDalInitalizer},
extract::mikan::{client::AppMikanClientInitializer, AppMikanClient}, extract::mikan::{client::AppMikanClientInitializer, AppMikanClient},
migrations::Migrator, migrations::Migrator,
models::entities::subscribers, models::subscribers,
workers::subscription_worker::SubscriptionWorker, workers::subscription_worker::SubscriptionWorker,
}; };

View File

@ -24,7 +24,7 @@ pub struct OidcAuthConfig {
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "auth_type", rename_all = "snake_case")]
pub enum AppAuthConfig { pub enum AppAuthConfig {
Basic(BasicAuthConfig), Basic(BasicAuthConfig),
Oidc(OidcAuthConfig), Oidc(OidcAuthConfig),

View File

@ -1,6 +1,6 @@
use loco_rs::prelude::*; 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<AppContext>) -> Result<impl IntoResponse> { async fn current(State(ctx): State<AppContext>) -> Result<impl IntoResponse> {
let subscriber = subscribers::Model::find_root(&ctx).await?; let subscriber = subscribers::Model::find_root(&ctx).await?;

View File

@ -38,7 +38,6 @@ pub enum Bangumi {
Id, Id,
MikanBangumiId, MikanBangumiId,
DisplayName, DisplayName,
SubscriptionId,
SubscriberId, SubscriberId,
RawName, RawName,
Season, Season,
@ -54,6 +53,14 @@ pub enum Bangumi {
Extra, Extra,
} }
#[derive(DeriveIden)]
pub enum SubscriptionBangumi {
Table,
Id,
SubscriptionId,
BangumiId,
}
#[derive(DeriveIden)] #[derive(DeriveIden)]
pub enum Episodes { pub enum Episodes {
Table, Table,
@ -62,7 +69,6 @@ pub enum Episodes {
RawName, RawName,
DisplayName, DisplayName,
BangumiId, BangumiId,
SubscriptionId,
SubscriberId, SubscriberId,
DownloadId, DownloadId,
SavePath, SavePath,
@ -79,13 +85,23 @@ pub enum Episodes {
Extra, Extra,
} }
#[derive(DeriveIden)]
pub enum SubscriptionEpisode {
Table,
Id,
SubscriptionId,
EpisodeId,
}
#[derive(DeriveIden)] #[derive(DeriveIden)]
pub enum Downloads { pub enum Downloads {
Table, Table,
Id, Id,
OriginalName, RawName,
DisplayName, DisplayName,
SubscriptionId, SubscriberId,
DownloaderId,
EpisodeId,
Status, Status,
CurrSize, CurrSize,
AllSize, AllSize,

View File

@ -2,7 +2,8 @@ use loco_rs::schema::jsonb_null;
use sea_orm_migration::{prelude::*, schema::*}; use sea_orm_migration::{prelude::*, schema::*};
use super::defs::{ use super::defs::{
Bangumi, CustomSchemaManagerExt, Episodes, GeneralIds, Subscribers, Subscriptions, Bangumi, CustomSchemaManagerExt, Episodes, GeneralIds, Subscribers, SubscriptionBangumi,
SubscriptionEpisode, Subscriptions,
}; };
use crate::models::{ use crate::models::{
subscribers::SEED_SUBSCRIBER, subscribers::SEED_SUBSCRIBER,
@ -37,12 +38,15 @@ impl MigrationTrait for Migration {
) )
.await?; .await?;
let insert = Query::insert() manager
.into_table(Subscribers::Table) .exec_stmt(
.columns([Subscribers::Pid, Subscribers::DisplayName]) Query::insert()
.values_panic([SEED_SUBSCRIBER.into(), SEED_SUBSCRIBER.into()]) .into_table(Subscribers::Table)
.to_owned(); .columns([Subscribers::Pid, Subscribers::DisplayName])
manager.exec_stmt(insert).await?; .values_panic([SEED_SUBSCRIBER.into(), SEED_SUBSCRIBER.into()])
.to_owned(),
)
.await?;
create_postgres_enum_for_active_enum!( create_postgres_enum_for_active_enum!(
manager, manager,
@ -70,7 +74,7 @@ impl MigrationTrait for Migration {
.name("fk_subscriptions_subscriber_id") .name("fk_subscriptions_subscriber_id")
.from(Subscriptions::Table, Subscriptions::SubscriberId) .from(Subscriptions::Table, Subscriptions::SubscriberId)
.to(Subscribers::Table, Subscribers::Id) .to(Subscribers::Table, Subscribers::Id)
.on_update(ForeignKeyAction::Restrict) .on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade), .on_delete(ForeignKeyAction::Cascade),
) )
.to_owned(), .to_owned(),
@ -89,7 +93,6 @@ impl MigrationTrait for Migration {
table_auto(Bangumi::Table) table_auto(Bangumi::Table)
.col(pk_auto(Bangumi::Id)) .col(pk_auto(Bangumi::Id))
.col(text_null(Bangumi::MikanBangumiId)) .col(text_null(Bangumi::MikanBangumiId))
.col(integer(Bangumi::SubscriptionId))
.col(integer(Bangumi::SubscriberId)) .col(integer(Bangumi::SubscriberId))
.col(text(Bangumi::DisplayName)) .col(text(Bangumi::DisplayName))
.col(text(Bangumi::RawName)) .col(text(Bangumi::RawName))
@ -104,22 +107,24 @@ impl MigrationTrait for Migration {
.col(boolean(Bangumi::Deleted).default(false)) .col(boolean(Bangumi::Deleted).default(false))
.col(text_null(Bangumi::Homepage)) .col(text_null(Bangumi::Homepage))
.col(jsonb_null(Bangumi::Extra)) .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( .foreign_key(
ForeignKey::create() ForeignKey::create()
.name("fk_bangumi_subscriber_id") .name("fk_bangumi_subscriber_id")
.from(Bangumi::Table, Bangumi::SubscriberId) .from(Bangumi::Table, Bangumi::SubscriberId)
.to(Subscribers::Table, Subscribers::Id) .to(Subscribers::Table, Subscribers::Id)
.on_update(ForeignKeyAction::Restrict) .on_update(ForeignKeyAction::Cascade)
.on_delete(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(), .to_owned(),
) )
.await?; .await?;
@ -150,6 +155,44 @@ impl MigrationTrait for Migration {
.create_postgres_auto_update_ts_trigger_for_col(Bangumi::Table, GeneralIds::UpdatedAt) .create_postgres_auto_update_ts_trigger_for_col(Bangumi::Table, GeneralIds::UpdatedAt)
.await?; .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 manager
.create_table( .create_table(
table_auto(Episodes::Table) table_auto(Episodes::Table)
@ -158,7 +201,6 @@ impl MigrationTrait for Migration {
.col(text(Episodes::RawName)) .col(text(Episodes::RawName))
.col(text(Episodes::DisplayName)) .col(text(Episodes::DisplayName))
.col(integer(Episodes::BangumiId)) .col(integer(Episodes::BangumiId))
.col(integer(Episodes::SubscriptionId))
.col(integer(Episodes::SubscriberId)) .col(integer(Episodes::SubscriberId))
.col(text_null(Episodes::SavePath)) .col(text_null(Episodes::SavePath))
.col(text_null(Episodes::Resolution)) .col(text_null(Episodes::Resolution))
@ -172,20 +214,12 @@ impl MigrationTrait for Migration {
.col(boolean(Episodes::Deleted).default(false)) .col(boolean(Episodes::Deleted).default(false))
.col(text_null(Episodes::Source)) .col(text_null(Episodes::Source))
.col(jsonb_null(Episodes::Extra)) .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( .foreign_key(
ForeignKey::create() ForeignKey::create()
.name("fk_episodes_bangumi_id") .name("fk_episodes_bangumi_id")
.from(Episodes::Table, Episodes::BangumiId) .from(Episodes::Table, Episodes::BangumiId)
.to(Bangumi::Table, Bangumi::Id) .to(Bangumi::Table, Bangumi::Id)
.on_update(ForeignKeyAction::Restrict) .on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade), .on_delete(ForeignKeyAction::Cascade),
) )
.foreign_key( .foreign_key(
@ -193,7 +227,7 @@ impl MigrationTrait for Migration {
.name("fk_episodes_subscriber_id") .name("fk_episodes_subscriber_id")
.from(Episodes::Table, Episodes::SubscriberId) .from(Episodes::Table, Episodes::SubscriberId)
.to(Subscribers::Table, Subscribers::Id) .to(Subscribers::Table, Subscribers::Id)
.on_update(ForeignKeyAction::Restrict) .on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade), .on_delete(ForeignKeyAction::Cascade),
) )
.to_owned(), .to_owned(),
@ -228,12 +262,50 @@ impl MigrationTrait for Migration {
.create_postgres_auto_update_ts_trigger_for_col(Episodes::Table, GeneralIds::UpdatedAt) .create_postgres_auto_update_ts_trigger_for_col(Episodes::Table, GeneralIds::UpdatedAt)
.await?; .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(()) Ok(())
} }
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager manager
.drop_table(Table::drop().table(Episodes::Table).to_owned()) .drop_table(Table::drop().table(SubscriptionEpisode::Table).to_owned())
.await?; .await?;
manager manager
@ -241,7 +313,11 @@ impl MigrationTrait for Migration {
.await?; .await?;
manager 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?; .await?;
manager manager
@ -249,7 +325,7 @@ impl MigrationTrait for Migration {
.await?; .await?;
manager manager
.drop_table(Table::drop().table(Subscriptions::Table).to_owned()) .drop_table(Table::drop().table(Bangumi::Table).to_owned())
.await?; .await?;
manager manager
@ -260,7 +336,7 @@ impl MigrationTrait for Migration {
.await?; .await?;
manager manager
.drop_table(Table::drop().table(Subscribers::Table).to_owned()) .drop_table(Table::drop().table(Subscriptions::Table).to_owned())
.await?; .await?;
manager manager
@ -268,17 +344,21 @@ impl MigrationTrait for Migration {
.await?; .await?;
manager manager
.drop_postgres_enum_for_active_enum(subscriptions::SubscriptionCategoryEnum) .drop_table(Table::drop().table(Subscribers::Table).to_owned())
.await?; .await?;
manager manager
.drop_postgres_auto_update_ts_fn_for_col(GeneralIds::UpdatedAt) .drop_postgres_enum_for_active_enum(subscriptions::SubscriptionCategoryEnum)
.await?; .await?;
manager manager
.drop_postgres_enum_for_active_enum(SubscriptionCategoryEnum) .drop_postgres_enum_for_active_enum(SubscriptionCategoryEnum)
.await?; .await?;
manager
.drop_postgres_auto_update_ts_fn_for_col(GeneralIds::UpdatedAt)
.await?;
Ok(()) Ok(())
} }
} }

View File

@ -2,9 +2,12 @@ use loco_rs::schema::table_auto;
use sea_orm_migration::{prelude::*, schema::*}; use sea_orm_migration::{prelude::*, schema::*};
use super::defs::*; use super::defs::*;
use crate::models::prelude::{ use crate::models::{
downloads::{DownloadMimeEnum, DownloadStatusEnum}, downloaders::DownloaderCategoryEnum,
DownloadMime, DownloadStatus, prelude::{
downloads::{DownloadMimeEnum, DownloadStatusEnum},
DownloadMime, DownloadStatus, DownloaderCategory,
},
}; };
#[derive(DeriveMigrationName)] #[derive(DeriveMigrationName)]
@ -13,6 +16,48 @@ pub struct Migration;
#[async_trait::async_trait] #[async_trait::async_trait]
impl MigrationTrait for Migration { impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 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!( create_postgres_enum_for_active_enum!(
manager, manager,
DownloadMimeEnum, DownloadMimeEnum,
@ -37,9 +82,11 @@ impl MigrationTrait for Migration {
.create_table( .create_table(
table_auto(Downloads::Table) table_auto(Downloads::Table)
.col(pk_auto(Downloads::Id)) .col(pk_auto(Downloads::Id))
.col(string(Downloads::OriginalName)) .col(string(Downloads::RawName))
.col(string(Downloads::DisplayName)) .col(string(Downloads::DisplayName))
.col(integer(Downloads::SubscriptionId)) .col(integer(Downloads::SubscriberId))
.col(integer(Downloads::DownloaderId))
.col(integer(Downloads::EpisodeId))
.col(enumeration( .col(enumeration(
Downloads::Status, Downloads::Status,
DownloadStatusEnum, DownloadStatusEnum,
@ -57,16 +104,42 @@ impl MigrationTrait for Migration {
.col(text_null(Downloads::SavePath)) .col(text_null(Downloads::SavePath))
.foreign_key( .foreign_key(
ForeignKey::create() ForeignKey::create()
.name("fk_downloads_subscription_id") .name("fk_downloads_subscriber_id")
.from(Downloads::Table, Downloads::SubscriptionId) .from_tbl(Downloads::Table)
.to(Subscriptions::Table, Subscriptions::Id) .from_col(Downloads::SubscriberId)
.on_update(ForeignKeyAction::Restrict) .to_tbl(Subscribers::Table)
.on_delete(ForeignKeyAction::Cascade), .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(), .to_owned(),
) )
.await?; .await?;
manager
.create_postgres_auto_update_ts_trigger_for_col(Downloads::Table, GeneralIds::UpdatedAt)
.await?;
manager manager
.create_index( .create_index(
Index::create() Index::create()
@ -78,37 +151,12 @@ impl MigrationTrait for Migration {
) )
.await?; .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(()) Ok(())
} }
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager manager
.alter_table( .drop_postgres_auto_update_ts_trigger_for_col(Downloads::Table, GeneralIds::UpdatedAt)
Table::alter()
.table(Episodes::Table)
.drop_foreign_key(Alias::new("fk_episodes_download_id"))
.drop_column(Episodes::DownloadId)
.to_owned(),
)
.await?; .await?;
manager manager
@ -123,6 +171,18 @@ impl MigrationTrait for Migration {
.drop_postgres_enum_for_active_enum(DownloadStatusEnum) .drop_postgres_enum_for_active_enum(DownloadStatusEnum)
.await?; .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(()) Ok(())
} }
} }

View File

@ -3,7 +3,10 @@ use sea_orm_migration::{prelude::*, schema::*};
use super::defs::Auth; use super::defs::Auth;
use crate::{ use crate::{
migrations::defs::{CustomSchemaManagerExt, GeneralIds, Subscribers}, migrations::defs::{CustomSchemaManagerExt, GeneralIds, Subscribers},
models::auth::{AuthType, AuthTypeEnum}, models::{
auth::{AuthType, AuthTypeEnum},
subscribers::SEED_SUBSCRIBER,
},
}; };
#[derive(DeriveMigrationName)] #[derive(DeriveMigrationName)]
@ -40,7 +43,7 @@ impl MigrationTrait for Migration {
.to_tbl(Subscribers::Table) .to_tbl(Subscribers::Table)
.to_col(Subscribers::Id) .to_col(Subscribers::Id)
.on_delete(ForeignKeyAction::Cascade) .on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Restrict), .on_update(ForeignKeyAction::Cascade),
) )
.to_owned(), .to_owned(),
) )
@ -62,6 +65,20 @@ impl MigrationTrait for Migration {
.create_postgres_auto_update_ts_trigger_for_col(Auth::Table, GeneralIds::UpdatedAt) .create_postgres_auto_update_ts_trigger_for_col(Auth::Table, GeneralIds::UpdatedAt)
.await?; .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(()) Ok(())
} }

View File

@ -4,7 +4,6 @@ pub use sea_orm_migration::prelude::*;
pub mod defs; pub mod defs;
pub mod m20220101_000001_init; pub mod m20220101_000001_init;
pub mod m20240224_082543_add_downloads; pub mod m20240224_082543_add_downloads;
pub mod m20240225_060853_subscriber_add_downloader;
pub mod m20241231_000001_auth; pub mod m20241231_000001_auth;
pub struct Migrator; pub struct Migrator;
@ -15,7 +14,6 @@ impl MigratorTrait for Migrator {
vec![ vec![
Box::new(m20220101_000001_init::Migration), Box::new(m20220101_000001_init::Migration),
Box::new(m20240224_082543_add_downloads::Migration), Box::new(m20240224_082543_add_downloads::Migration),
Box::new(m20240225_060853_subscriber_add_downloader::Migration),
Box::new(m20241231_000001_auth::Migration), Box::new(m20241231_000001_auth::Migration),
] ]
} }

View File

@ -1,6 +1,54 @@
use sea_orm::entity::prelude::*; 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<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"
)]
SubscriberId,
}
impl Related<super::subscribers::Entity> 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] #[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,7 +1,87 @@
use loco_rs::app::AppContext; 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<Vec<String>>,
pub group: Option<Vec<String>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
pub struct BangumiExtra {
pub name_zh: Option<String>,
pub s_name_zh: Option<String>,
pub name_en: Option<String>,
pub s_name_en: Option<String>,
pub name_jp: Option<String>,
pub s_name_jp: Option<String>,
}
#[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<String>,
pub subscriber_id: i32,
pub display_name: String,
pub raw_name: String,
pub season: i32,
pub season_raw: Option<String>,
pub fansub: Option<String>,
pub mikan_fansub_id: Option<String>,
pub filter: Option<BangumiFilter>,
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>,
}
#[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<super::episodes::Entity> for Entity {
fn to() -> RelationDef {
Relation::Episode.def()
}
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
super::subscription_bangumi::Relation::Subscription.def()
}
fn via() -> Option<RelationDef> {
Some(super::subscription_bangumi::Relation::Bangumi.def().rev())
}
}
impl Related<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}
impl Model { impl Model {
pub async fn get_or_insert_from_mikan<F>( pub async fn get_or_insert_from_mikan<F>(
@ -30,12 +110,37 @@ impl Model {
let mut bgm = ActiveModel { let mut bgm = ActiveModel {
mikan_bangumi_id: ActiveValue::Set(Some(mikan_bangumi_id)), mikan_bangumi_id: ActiveValue::Set(Some(mikan_bangumi_id)),
mikan_fansub_id: ActiveValue::Set(Some(mikan_fansub_id)), mikan_fansub_id: ActiveValue::Set(Some(mikan_fansub_id)),
subscription_id: ActiveValue::Set(subscription_id),
subscriber_id: ActiveValue::Set(subscriber_id), subscriber_id: ActiveValue::Set(subscriber_id),
..Default::default() ..Default::default()
}; };
f(&mut bgm).await?; 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) Ok(bgm)
} }
} }

View File

@ -1,7 +1,59 @@
use sea_orm::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use url::Url; 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<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}
impl Related<super::downloads::Entity> for Entity {
fn to() -> RelationDef {
Relation::Download.def()
}
}
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
@ -10,6 +62,7 @@ impl Model {
pub fn get_endpoint(&self) -> String { pub fn get_endpoint(&self) -> String {
self.endpoint.clone() self.endpoint.clone()
} }
pub fn endpoint_url(&self) -> Result<Url, url::ParseError> { pub fn endpoint_url(&self) -> Result<Url, url::ParseError> {
let url = Url::parse(&self.endpoint)?; let url = Url::parse(&self.endpoint)?;
Ok(url) Ok(url)

View File

@ -1,27 +1,107 @@
use sea_orm::{prelude::*, ActiveValue}; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::extract::mikan::MikanRssItem; #[derive(
pub use crate::models::entities::downloads::*; 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<u64>,
pub curr_size: Option<u64>,
pub homepage: Option<String>,
pub save_path: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::subscribers::Entity",
from = "Column::SubscriberId",
to = "super::subscribers::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Subscriber,
#[sea_orm(
belongs_to = "super::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<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}
impl Related<super::downloaders::Entity> for Entity {
fn to() -> RelationDef {
Relation::Downloader.def()
}
}
impl Related<super::episodes::Entity> for Entity {
fn to() -> RelationDef {
Relation::Episode.def()
}
}
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl 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 {}

View File

@ -1,10 +1,10 @@
use std::sync::Arc; use std::sync::Arc;
use loco_rs::app::AppContext; 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; use super::{bangumi, query::InsertManyReturningExt, subscription_episode};
pub use super::entities::episodes::*;
use crate::{ use crate::{
app::AppContextExt, app::AppContextExt,
extract::{ extract::{
@ -13,6 +13,92 @@ use crate::{
}, },
}; };
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult, Default)]
pub struct EpisodeExtra {
pub name_zh: Option<String>,
pub s_name_zh: Option<String>,
pub name_en: Option<String>,
pub s_name_en: Option<String>,
pub name_jp: Option<String>,
pub s_name_jp: Option<String>,
}
#[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<String>,
pub raw_name: String,
pub display_name: String,
pub bangumi_id: i32,
pub subscriber_id: i32,
pub save_path: Option<String>,
pub resolution: Option<String>,
pub season: i32,
pub season_raw: Option<String>,
pub fansub: Option<String>,
pub poster_link: Option<String>,
pub episode_index: i32,
pub homepage: Option<String>,
pub subtitle: Option<Vec<String>>,
#[sea_orm(default = "false")]
pub deleted: bool,
pub source: Option<String>,
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<super::bangumi::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bangumi.def()
}
}
impl Related<super::downloads::Entity> for Entity {
fn to() -> RelationDef {
Relation::Downloads.def()
}
}
impl Related<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriptions.def()
}
}
impl Related<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct MikanEpsiodeCreation { pub struct MikanEpsiodeCreation {
pub episode: MikanEpisodeMeta, pub episode: MikanEpisodeMeta,
@ -22,6 +108,7 @@ pub struct MikanEpsiodeCreation {
impl Model { impl Model {
pub async fn add_episodes( pub async fn add_episodes(
ctx: &AppContext, ctx: &AppContext,
subscription_id: i32,
creations: impl IntoIterator<Item = MikanEpsiodeCreation>, creations: impl IntoIterator<Item = MikanEpsiodeCreation>,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
let db = &ctx.db; let db = &ctx.db;
@ -35,13 +122,33 @@ impl Model {
}) })
.flatten(); .flatten();
Entity::insert_many(new_episode_active_modes) let inserted_episodes = Entity::insert_many(new_episode_active_modes)
.on_conflict( .on_conflict(
OnConflict::columns([Column::BangumiId, Column::MikanEpisodeId]) OnConflict::columns([Column::BangumiId, Column::MikanEpisodeId])
.do_nothing() .do_nothing()
.to_owned(), .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::<i32>());
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) .exec(db)
.await?; .await?;
@ -72,7 +179,6 @@ impl ActiveModel {
raw_name: ActiveValue::Set(item.episode_title.clone()), raw_name: ActiveValue::Set(item.episode_title.clone()),
display_name: ActiveValue::Set(item.episode_title.clone()), display_name: ActiveValue::Set(item.episode_title.clone()),
bangumi_id: ActiveValue::Set(bgm.id), bangumi_id: ActiveValue::Set(bgm.id),
subscription_id: ActiveValue::Set(bgm.subscription_id),
subscriber_id: ActiveValue::Set(bgm.subscriber_id), subscriber_id: ActiveValue::Set(bgm.subscriber_id),
resolution: ActiveValue::Set(raw_meta.resolution), resolution: ActiveValue::Set(raw_meta.resolution),
season: ActiveValue::Set(if raw_meta.season > 0 { season: ActiveValue::Set(if raw_meta.season > 0 {

View File

@ -2,10 +2,10 @@ pub mod auth;
pub mod bangumi; pub mod bangumi;
pub mod downloaders; pub mod downloaders;
pub mod downloads; pub mod downloads;
pub mod entities;
pub mod episodes; pub mod episodes;
pub mod notifications;
pub mod prelude; pub mod prelude;
pub mod query; pub mod query;
pub mod subscribers; pub mod subscribers;
pub mod subscription_bangumi;
pub mod subscription_episode;
pub mod subscriptions; pub mod subscriptions;

View File

@ -1,7 +1,8 @@
use sea_orm::{ use sea_orm::{
prelude::Expr, prelude::Expr,
sea_query::{Alias, IntoColumnRef, IntoTableRef, Query, SelectStatement}, 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< pub fn filter_values_in<
@ -24,3 +25,76 @@ pub fn filter_values_in<
.and_where(Expr::col(col_ref).is_not_null()) .and_where(Expr::col(col_ref).is_not_null())
.to_owned() .to_owned()
} }
#[async_trait::async_trait]
pub trait InsertManyReturningExt<A>: Sized
where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
A: ActiveModelTrait,
{
fn exec_with_returning_models<C>(
self,
db: &C,
) -> SelectorRaw<SelectModel<<A::Entity as EntityTrait>::Model>>
where
C: ConnectionTrait;
async fn exec_with_returning_columns<C, I>(
self,
db: &C,
columns: I,
) -> Result<Vec<QueryResult>, DbErr>
where
C: ConnectionTrait,
I: IntoIterator<Item = <A::Entity as EntityTrait>::Column> + Send;
}
#[async_trait::async_trait]
impl<A> InsertManyReturningExt<A> for Insert<A>
where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
A: ActiveModelTrait + Send,
{
fn exec_with_returning_models<C>(
self,
db: &C,
) -> SelectorRaw<SelectModel<<A::Entity as EntityTrait>::Model>>
where
C: ConnectionTrait,
{
let mut insert_statement = self.into_query();
let db_backend = db.get_database_backend();
let returning = Query::returning().exprs(
<A::Entity as EntityTrait>::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::<SelectModel<<A::Entity as EntityTrait>::Model>>::from_statement(
insert_statement,
)
}
async fn exec_with_returning_columns<C, I>(
self,
db: &C,
columns: I,
) -> Result<Vec<QueryResult>, DbErr>
where
C: ConnectionTrait,
I: IntoIterator<Item = <A::Entity as EntityTrait>::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
}
}

View File

@ -2,13 +2,73 @@ use loco_rs::{
app::AppContext, app::AppContext,
model::{ModelError, ModelResult}, model::{ModelError, ModelResult},
}; };
use sea_orm::{entity::prelude::*, ActiveValue, TransactionTrait}; use sea_orm::{entity::prelude::*, ActiveValue, FromJsonQueryResult, TransactionTrait};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub use super::entities::subscribers::*;
pub const SEED_SUBSCRIBER: &str = "konobangu"; pub const SEED_SUBSCRIBER: &str = "konobangu";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
pub struct SubscriberBangumiConfig {
pub leading_group_tag: Option<bool>,
}
#[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<SubscriberBangumiConfig>,
}
#[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<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscription.def()
}
}
impl Related<super::downloaders::Entity> for Entity {
fn to() -> RelationDef {
Relation::Downloader.def()
}
}
impl Related<super::bangumi::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bangumi.def()
}
}
impl Related<super::episodes::Entity> for Entity {
fn to() -> RelationDef {
Relation::Episode.def()
}
}
impl Related<super::auth::Entity> for Entity {
fn to() -> RelationDef {
Relation::Auth.def()
}
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct SubscriberIdParams { pub struct SubscriberIdParams {
pub id: String, pub id: String,

View File

@ -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<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscription.def()
}
}
impl Related<super::bangumi::Entity> 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()
}
}
}

View File

@ -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<super::subscriptions::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscription.def()
}
}
impl Related<super::episodes::Entity> 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()
}
}
}

View File

@ -5,7 +5,6 @@ use loco_rs::app::AppContext;
use sea_orm::{entity::prelude::*, ActiveValue}; use sea_orm::{entity::prelude::*, ActiveValue};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub use super::entities::subscriptions::{self, *};
use super::{bangumi, episodes, query::filter_values_in}; use super::{bangumi, episodes, query::filter_values_in};
use crate::{ use crate::{
app::AppContextExt, app::AppContextExt,
@ -24,6 +23,92 @@ use crate::{
models::episodes::MikanEpsiodeCreation, 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<super::subscribers::Entity> for Entity {
fn to() -> RelationDef {
Relation::Subscriber.def()
}
}
impl Related<super::bangumi::Entity> for Entity {
fn to() -> RelationDef {
super::subscription_bangumi::Relation::Bangumi.def()
}
fn via() -> Option<RelationDef> {
Some(
super::subscription_bangumi::Relation::Subscription
.def()
.rev(),
)
}
}
impl Related<super::episodes::Entity> for Entity {
fn to() -> RelationDef {
super::subscription_episode::Relation::Episode.def()
}
fn via() -> Option<RelationDef> {
Some(
super::subscription_episode::Relation::Subscription
.def()
.rev(),
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SubscriptionCreateFromRssDto { pub struct SubscriptionCreateFromRssDto {
pub rss_link: String, pub rss_link: String,
@ -77,7 +162,7 @@ impl Model {
Ok(subscription.insert(db).await?) Ok(subscription.insert(db).await?)
} }
pub async fn toggle_iters( pub async fn toggle_with_ids(
ctx: &AppContext, ctx: &AppContext,
ids: impl Iterator<Item = i32>, ids: impl Iterator<Item = i32>,
enabled: bool, enabled: bool,
@ -91,7 +176,7 @@ impl Model {
Ok(()) Ok(())
} }
pub async fn delete_iters( pub async fn delete_with_ids(
ctx: &AppContext, ctx: &AppContext,
ids: impl Iterator<Item = i32>, ids: impl Iterator<Item = i32>,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
@ -213,6 +298,7 @@ impl Model {
); );
episodes::Model::add_episodes( episodes::Model::add_episodes(
ctx, ctx,
self.id,
new_ep_metas.into_iter().map(|item| MikanEpsiodeCreation { new_ep_metas.into_iter().map(|item| MikanEpsiodeCreation {
episode: item, episode: item,
bangumi: bgm.clone(), bangumi: bgm.clone(),

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::models::entities::subscribers; use crate::models::subscribers;
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct CurrentResponse { pub struct CurrentResponse {