1use crate::entity::episode::EpisodeType;
4use crate::entity::manga::{CollectedMembersVo, GlossaryVo, MangaCardVo, MangaDetailVo};
5use crate::entity::rss::{EpisodeRssRow, RssMangaRow, WorkReminderRssRow, RSS_MAX_ITEMS};
6use crate::sea_entity::{
7 authortb, glossary, magazine, mangaauthor, mangaepisodedetail, mangaepisodetb, mangamagazine,
8 mangatb, membertb, workreminder,
9};
10use chrono::{DateTime, Utc};
11use sea_orm::{
12 ColumnTrait, Condition, ConnectionTrait, DatabaseConnection, DbBackend, EntityTrait,
13 PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Statement, Value,
14};
15use std::collections::HashMap;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum MangaListSortField {
20 UpdateTime,
22 SetupTime,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum MangaListSortOrder {
29 Asc,
31 Desc,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct MangaListSort {
38 field: MangaListSortField,
39 order: MangaListSortOrder,
40}
41
42impl Default for MangaListSort {
43 fn default() -> Self {
44 Self {
45 field: MangaListSortField::UpdateTime,
46 order: MangaListSortOrder::Desc,
47 }
48 }
49}
50
51impl MangaListSort {
52 pub fn from_query(sort_prop: Option<&str>, sort_order: Option<&str>) -> Self {
54 let field = match sort_prop.map(normalize_sort_token).as_deref() {
55 Some(
56 "setuptime" | "createtime" | "createdat" | "addtime" | "addedtime" | "created"
57 | "添加时间",
58 ) => MangaListSortField::SetupTime,
59 Some("updatetime" | "updatedat" | "updated" | "更新时间") | None => {
60 MangaListSortField::UpdateTime
61 }
62 Some(_) => MangaListSortField::UpdateTime,
63 };
64 let order = match sort_order.map(normalize_sort_token).as_deref() {
65 Some("ascending" | "asc") => MangaListSortOrder::Asc,
66 Some("descending" | "desc") | None => MangaListSortOrder::Desc,
67 Some(_) => MangaListSortOrder::Desc,
68 };
69 Self { field, order }
70 }
71
72 pub fn cache_field(self) -> &'static str {
74 match self.field {
75 MangaListSortField::UpdateTime => "updateTime",
76 MangaListSortField::SetupTime => "setupTime",
77 }
78 }
79
80 pub fn cache_order(self) -> &'static str {
82 match self.order {
83 MangaListSortOrder::Asc => "ascending",
84 MangaListSortOrder::Desc => "descending",
85 }
86 }
87}
88
89fn normalize_sort_token(value: &str) -> String {
90 value
91 .trim()
92 .replace(['_', '-', ' '], "")
93 .to_ascii_lowercase()
94}
95
96struct MangaListContext {
98 author1: HashMap<i32, (i32, String)>,
100 author2: HashMap<i32, (i32, String)>,
102 magazines: HashMap<i32, (i32, String)>,
104 latest_details: HashMap<i32, mangaepisodedetail::Model>,
106}
107
108pub async fn count_list(
110 db: &DatabaseConnection,
111 manga_tran_name: Option<&str>,
112 manga_ori_name: Option<&str>,
113 category: Option<i16>,
114 manga_status: Option<i16>,
115 author_name: Option<&str>,
116 magazine_name: Option<&str>,
117) -> crate::error::ApiResult<i64> {
118 let filter = list_filter(
119 db,
120 manga_tran_name,
121 manga_ori_name,
122 category,
123 manga_status,
124 author_name,
125 magazine_name,
126 )
127 .await?;
128 let n = mangatb::Entity::find().filter(filter).count(db).await?;
129 Ok(n as i64)
130}
131
132pub async fn list_page(
134 db: &DatabaseConnection,
135 manga_tran_name: Option<&str>,
136 manga_ori_name: Option<&str>,
137 category: Option<i16>,
138 manga_status: Option<i16>,
139 author_name: Option<&str>,
140 magazine_name: Option<&str>,
141 limit: i32,
142 offset: i32,
143) -> crate::error::ApiResult<Vec<MangaCardVo>> {
144 list_page_sorted(
145 db,
146 manga_tran_name,
147 manga_ori_name,
148 category,
149 manga_status,
150 author_name,
151 magazine_name,
152 MangaListSort::default(),
153 limit,
154 offset,
155 )
156 .await
157}
158
159pub async fn list_page_sorted(
161 db: &DatabaseConnection,
162 manga_tran_name: Option<&str>,
163 manga_ori_name: Option<&str>,
164 category: Option<i16>,
165 manga_status: Option<i16>,
166 author_name: Option<&str>,
167 magazine_name: Option<&str>,
168 sort: MangaListSort,
169 limit: i32,
170 offset: i32,
171) -> crate::error::ApiResult<Vec<MangaCardVo>> {
172 let filter = list_filter(
173 db,
174 manga_tran_name,
175 manga_ori_name,
176 category,
177 manga_status,
178 author_name,
179 magazine_name,
180 )
181 .await?;
182 let query = mangatb::Entity::find().filter(filter);
183 let query = match (sort.field, sort.order) {
184 (MangaListSortField::UpdateTime, MangaListSortOrder::Desc) => query
185 .order_by_desc(mangatb::Column::UpdateTime)
186 .order_by_desc(mangatb::Column::Id),
187 (MangaListSortField::UpdateTime, MangaListSortOrder::Asc) => query
188 .order_by_asc(mangatb::Column::UpdateTime)
189 .order_by_asc(mangatb::Column::Id),
190 (MangaListSortField::SetupTime, MangaListSortOrder::Desc) => query
191 .order_by_desc(mangatb::Column::SetupTime)
192 .order_by_desc(mangatb::Column::Id),
193 (MangaListSortField::SetupTime, MangaListSortOrder::Asc) => query
194 .order_by_asc(mangatb::Column::SetupTime)
195 .order_by_asc(mangatb::Column::Id),
196 };
197 let rows = query
198 .limit(limit.max(0) as u64)
199 .offset(offset.max(0) as u64)
200 .all(db)
201 .await?;
202 assemble_manga_cards(db, &rows, true).await
203}
204
205pub async fn list_by_author(
207 db: &DatabaseConnection,
208 author_id: i32,
209) -> crate::error::ApiResult<Vec<MangaCardVo>> {
210 let (_, cards) = list_by_author_page(db, author_id, 1, i32::MAX).await?;
211 Ok(cards)
212}
213
214pub async fn list_by_author_page(
216 db: &DatabaseConnection,
217 author_id: i32,
218 page: i32,
219 page_size: i32,
220) -> crate::error::ApiResult<(i64, Vec<MangaCardVo>)> {
221 let page = page.max(1);
222 let page_size = page_size.max(1);
223 let offset = ((page - 1) * page_size) as i64;
224 let (total, rows) = tokio::try_join!(
225 count_by_author(db, author_id),
226 fetch_by_author_page(db, author_id, page_size, offset),
227 )?;
228 if rows.is_empty() {
229 return Ok((total, vec![]));
230 }
231 let cards = assemble_manga_cards(db, &rows, true).await?;
232 Ok((total, cards))
233}
234
235async fn count_by_author(db: &DatabaseConnection, author_id: i32) -> crate::error::ApiResult<i64> {
237 let sql = r#"SELECT COUNT(DISTINCT m."Id")::bigint AS cnt
238 FROM mangaauthor ma
239 INNER JOIN mangatb m ON m."Id" = ma."mangaId"
240 WHERE ma."authorId" = $1"#;
241 let row = db
242 .query_one(Statement::from_sql_and_values(
243 DbBackend::Postgres,
244 sql,
245 [Value::from(author_id)],
246 ))
247 .await?
248 .ok_or_else(|| crate::error::AppError::business("统计作者漫画失败喵"))?;
249 Ok(row.try_get("", "cnt")?)
250}
251
252async fn fetch_by_author_page(
254 db: &DatabaseConnection,
255 author_id: i32,
256 page_size: i32,
257 offset: i64,
258) -> crate::error::ApiResult<Vec<mangatb::Model>> {
259 let sql = r#"SELECT DISTINCT m."Id", m."mangaTranName", m."mangaOriName", m.image, m.category,
260 m."mangaStatus", m."setupTime", m."updateTime", m.link, m.introduction
261 FROM mangaauthor ma
262 INNER JOIN mangatb m ON m."Id" = ma."mangaId"
263 WHERE ma."authorId" = $1
264 ORDER BY m."updateTime" DESC
265 LIMIT $2 OFFSET $3"#;
266 let rows = db
267 .query_all(Statement::from_sql_and_values(
268 DbBackend::Postgres,
269 sql,
270 [
271 Value::from(author_id),
272 Value::from(page_size),
273 Value::from(offset),
274 ],
275 ))
276 .await?;
277 rows.into_iter()
278 .map(row_to_mangatb_model)
279 .collect::<crate::error::ApiResult<Vec<_>>>()
280}
281
282pub async fn list_by_magazine(
284 db: &DatabaseConnection,
285 magazine_id: i32,
286) -> crate::error::ApiResult<Vec<MangaCardVo>> {
287 let (_, cards) = list_by_magazine_page(db, magazine_id, 1, i32::MAX).await?;
288 Ok(cards)
289}
290
291pub async fn list_by_magazine_page(
293 db: &DatabaseConnection,
294 magazine_id: i32,
295 page: i32,
296 page_size: i32,
297) -> crate::error::ApiResult<(i64, Vec<MangaCardVo>)> {
298 let page = page.max(1);
299 let page_size = page_size.max(1);
300 let offset = ((page - 1) * page_size) as i64;
301 let (total, rows) = tokio::try_join!(
302 count_by_magazine(db, magazine_id),
303 fetch_by_magazine_page(db, magazine_id, page_size, offset),
304 )?;
305 if rows.is_empty() {
306 return Ok((total, vec![]));
307 }
308 let cards = assemble_manga_cards(db, &rows, true).await?;
309 Ok((total, cards))
310}
311
312async fn count_by_magazine(
314 db: &DatabaseConnection,
315 magazine_id: i32,
316) -> crate::error::ApiResult<i64> {
317 let sql = r#"SELECT COUNT(DISTINCT m."Id")::bigint AS cnt
318 FROM mangamagazine mm
319 INNER JOIN mangatb m ON m."Id" = mm."mangaId"
320 WHERE mm."magazineId" = $1 AND mm."mangaId" IS NOT NULL"#;
321 let row = db
322 .query_one(Statement::from_sql_and_values(
323 DbBackend::Postgres,
324 sql,
325 [Value::from(magazine_id)],
326 ))
327 .await?
328 .ok_or_else(|| crate::error::AppError::business("统计杂志漫画失败喵"))?;
329 Ok(row.try_get("", "cnt")?)
330}
331
332async fn fetch_by_magazine_page(
334 db: &DatabaseConnection,
335 magazine_id: i32,
336 page_size: i32,
337 offset: i64,
338) -> crate::error::ApiResult<Vec<mangatb::Model>> {
339 let sql = r#"SELECT DISTINCT m."Id", m."mangaTranName", m."mangaOriName", m.image, m.category,
340 m."mangaStatus", m."setupTime", m."updateTime", m.link, m.introduction
341 FROM mangamagazine mm
342 INNER JOIN mangatb m ON m."Id" = mm."mangaId"
343 WHERE mm."magazineId" = $1 AND mm."mangaId" IS NOT NULL
344 ORDER BY m."updateTime" DESC
345 LIMIT $2 OFFSET $3"#;
346 let rows = db
347 .query_all(Statement::from_sql_and_values(
348 DbBackend::Postgres,
349 sql,
350 [
351 Value::from(magazine_id),
352 Value::from(page_size),
353 Value::from(offset),
354 ],
355 ))
356 .await?;
357 rows.into_iter()
358 .map(row_to_mangatb_model)
359 .collect::<crate::error::ApiResult<Vec<_>>>()
360}
361
362pub async fn get_manga_card_by_id(
364 db: &DatabaseConnection,
365 id: i32,
366) -> crate::error::ApiResult<Option<MangaCardVo>> {
367 let row = mangatb::Entity::find_by_id(id).one(db).await?;
368 let Some(m) = row else {
369 return Ok(None);
370 };
371 let cards = assemble_manga_cards(db, std::slice::from_ref(&m), true).await?;
372 Ok(cards.into_iter().next())
373}
374
375pub async fn get_manga_detail_by_id(
377 db: &DatabaseConnection,
378 id: i32,
379) -> crate::error::ApiResult<Option<MangaDetailVo>> {
380 let row = mangatb::Entity::find_by_id(id).one(db).await?;
381 let Some(m) = row else {
382 return Ok(None);
383 };
384 let ctx = load_list_context(db, std::slice::from_ref(&m), true, false).await?;
385 Ok(Some(to_manga_detail(&m, &ctx)))
386}
387
388pub async fn get_manga_response_by_id(
390 db: &DatabaseConnection,
391 id: i32,
392) -> crate::error::ApiResult<Option<crate::entity::manga::MangaResponse>> {
393 let row = mangatb::Entity::find_by_id(id).one(db).await?;
394 let Some(m) = row else {
395 return Ok(None);
396 };
397 let ctx = load_list_context(db, std::slice::from_ref(&m), true, true).await?;
398 Ok(Some(to_manga_response(&m, &ctx)))
399}
400
401pub async fn get_manga_responses_by_ids(
403 db: &DatabaseConnection,
404 ids: &[i32],
405) -> crate::error::ApiResult<HashMap<i32, crate::entity::manga::MangaResponse>> {
406 if ids.is_empty() {
407 return Ok(HashMap::new());
408 }
409 let rows = mangatb::Entity::find()
410 .filter(mangatb::Column::Id.is_in(ids.to_vec()))
411 .all(db)
412 .await?;
413 let ctx = load_list_context(db, &rows, true, true).await?;
414 Ok(rows
415 .into_iter()
416 .map(|m| (m.id, to_manga_response(&m, &ctx)))
417 .collect())
418}
419
420pub async fn get_collect_list(
422 db: &DatabaseConnection,
423 member_id: i32,
424) -> crate::error::ApiResult<Vec<MangaCardVo>> {
425 let (_, cards) = get_collect_list_page(db, member_id, 1, i32::MAX).await?;
426 Ok(cards)
427}
428
429pub async fn get_collect_list_page(
433 db: &DatabaseConnection,
434 member_id: i32,
435 page: i32,
436 page_size: i32,
437) -> crate::error::ApiResult<(i64, Vec<MangaCardVo>)> {
438 let page = page.max(1);
439 let page_size = page_size.max(1);
440 let offset = ((page - 1) * page_size) as i64;
441 let (total, rows) = tokio::try_join!(
442 count_collect_list(db, member_id),
443 fetch_collect_list_page(db, member_id, page_size, offset),
444 )?;
445 if rows.is_empty() {
446 return Ok((total, vec![]));
447 }
448 let cards = assemble_manga_cards(db, &rows, false).await?;
449 Ok((total, cards))
450}
451
452async fn count_collect_list(
454 db: &DatabaseConnection,
455 member_id: i32,
456) -> crate::error::ApiResult<i64> {
457 let sql = r#"SELECT COUNT(*)::bigint AS cnt
458 FROM mangacollect c
459 INNER JOIN mangatb m ON m."Id" = c."mangaId"
460 WHERE c."memberId" = $1"#;
461 let row = db
462 .query_one(Statement::from_sql_and_values(
463 DbBackend::Postgres,
464 sql,
465 [Value::from(member_id)],
466 ))
467 .await?
468 .ok_or_else(|| crate::error::AppError::business("统计收藏失败喵"))?;
469 Ok(row.try_get("", "cnt")?)
470}
471
472async fn fetch_collect_list_page(
474 db: &DatabaseConnection,
475 member_id: i32,
476 page_size: i32,
477 offset: i64,
478) -> crate::error::ApiResult<Vec<mangatb::Model>> {
479 let sql = r#"SELECT m."Id", m."mangaTranName", m."mangaOriName", m.image, m.category,
480 m."mangaStatus", m."setupTime", m."updateTime", m.link, m.introduction
481 FROM mangacollect c
482 INNER JOIN mangatb m ON m."Id" = c."mangaId"
483 WHERE c."memberId" = $1
484 ORDER BY m."updateTime" DESC
485 LIMIT $2 OFFSET $3"#;
486 let rows = db
487 .query_all(Statement::from_sql_and_values(
488 DbBackend::Postgres,
489 sql,
490 [
491 Value::from(member_id),
492 Value::from(page_size),
493 Value::from(offset),
494 ],
495 ))
496 .await?;
497 rows.into_iter().map(row_to_mangatb_model).collect()
498}
499
500fn row_to_mangatb_model(row: sea_orm::QueryResult) -> crate::error::ApiResult<mangatb::Model> {
502 Ok(mangatb::Model {
503 id: row.try_get("", "Id")?,
504 manga_tran_name: row.try_get("", "mangaTranName")?,
505 manga_ori_name: row.try_get("", "mangaOriName")?,
506 image: row.try_get("", "image")?,
507 category: row.try_get("", "category")?,
508 manga_status: row.try_get("", "mangaStatus")?,
509 setup_time: row.try_get("", "setupTime")?,
510 update_time: row.try_get("", "updateTime")?,
511 link: row.try_get("", "link").ok(),
512 introduction: row.try_get("", "introduction").ok(),
513 })
514}
515
516pub async fn get_collected_members(
518 db: &DatabaseConnection,
519 manga_id: i32,
520) -> crate::error::ApiResult<Vec<CollectedMembersVo>> {
521 let sql = r#"SELECT m."Id" AS id,
522 m.username,
523 m.intern,
524 m."registrationTime" AS registration_time,
525 m."lastSubmitTime" AS last_submit_time,
526 COUNT(ms."Id")::int AS station_count,
527 MIN(c."Id") AS first_collect_id
528 FROM mangacollect c
529 INNER JOIN membertb m ON m."Id" = c."memberId"
530 LEFT JOIN mangastationed ms ON ms."memberId" = m."Id"
531 WHERE c."mangaId" = $1
532 GROUP BY m."Id", m.username, m.intern, m."registrationTime", m."lastSubmitTime"
533 ORDER BY first_collect_id"#;
534 let rows = db
535 .query_all(Statement::from_sql_and_values(
536 DbBackend::Postgres,
537 sql,
538 [Value::from(manga_id)],
539 ))
540 .await?;
541 rows.into_iter()
542 .map(|row| {
543 let id: i32 = row.try_get("", "id")?;
544 let username: String = row.try_get("", "username")?;
545 let intern: i32 = row.try_get("", "intern")?;
546 let registration_time: chrono::NaiveDateTime = row.try_get("", "registration_time")?;
547 let last_submit_time: chrono::NaiveDateTime = row.try_get("", "last_submit_time")?;
548 let station_count: i32 = row.try_get("", "station_count")?;
549 Ok(CollectedMembersVo {
550 id,
551 username: Some(username),
552 intern: intern as i16,
553 posts: vec![],
554 station_count: (station_count > 0).then_some(station_count),
555 registration_time: dt_legacy(registration_time),
556 last_submit_time: dt_legacy(last_submit_time),
557 })
558 })
559 .collect()
560}
561
562pub async fn get_manga_rss(db: &DatabaseConnection) -> crate::error::ApiResult<Vec<RssMangaRow>> {
564 let rows = mangatb::Entity::find()
565 .order_by_desc(mangatb::Column::SetupTime)
566 .limit(RSS_MAX_ITEMS)
567 .all(db)
568 .await?;
569 assemble_rss_manga(db, &rows, false).await
570}
571
572pub async fn get_manga_updated_rss(
574 db: &DatabaseConnection,
575) -> crate::error::ApiResult<Vec<RssMangaRow>> {
576 let rows = mangatb::Entity::find()
577 .order_by_desc(mangatb::Column::UpdateTime)
578 .limit(RSS_MAX_ITEMS)
579 .all(db)
580 .await?;
581 assemble_rss_manga(db, &rows, true).await
582}
583
584fn rss_episode_type(value: &str) -> EpisodeType {
586 if value == EpisodeType::Extra.as_str() {
587 EpisodeType::Extra
588 } else {
589 EpisodeType::Main
590 }
591}
592
593pub async fn get_episode_rss(
595 db: &DatabaseConnection,
596) -> crate::error::ApiResult<Vec<EpisodeRssRow>> {
597 let episodes = mangaepisodetb::Entity::find()
598 .order_by_desc(mangaepisodetb::Column::Id)
599 .limit(RSS_MAX_ITEMS)
600 .all(db)
601 .await?;
602 if episodes.is_empty() {
603 return Ok(vec![]);
604 }
605 let manga_ids: Vec<i32> = episodes.iter().map(|e| e.manga_id).collect();
606 let mangas: HashMap<i32, mangatb::Model> = mangatb::Entity::find()
607 .filter(mangatb::Column::Id.is_in(manga_ids))
608 .all(db)
609 .await?
610 .into_iter()
611 .map(|m| (m.id, m))
612 .collect();
613 let mut member_ids = Vec::new();
614 for ep in &episodes {
615 if let Some(id) = ep.provider_id {
616 member_ids.push(id);
617 }
618 if let Some(id) = ep.translator_id {
619 member_ids.push(id);
620 }
621 }
622 member_ids.sort_unstable();
623 member_ids.dedup();
624 let members: HashMap<i32, membertb::Model> = if member_ids.is_empty() {
625 HashMap::new()
626 } else {
627 membertb::Entity::find()
628 .filter(membertb::Column::Id.is_in(member_ids))
629 .all(db)
630 .await?
631 .into_iter()
632 .map(|m| (m.id, m))
633 .collect()
634 };
635 Ok(episodes
636 .iter()
637 .map(|ep| {
638 let manga = mangas.get(&ep.manga_id);
639 let provider = ep.provider_id.and_then(|id| members.get(&id));
640 let translator = ep.translator_id.and_then(|id| members.get(&id));
641 EpisodeRssRow {
642 id: ep.id,
643 manga_id: ep.manga_id,
644 manga_episode: Some(ep.manga_episode.clone()),
645 manga_episode_name: ep.manga_episode_name.clone(),
646 episode_type: rss_episode_type(&ep.episode_type),
647 setup_time: dt_legacy(ep.setup_time),
648 manga_name: manga.map(|m| m.manga_tran_name.clone()),
649 manga_ori_name: manga.map(|m| m.manga_ori_name.clone()),
650 category: manga.map(|m| m.category as i16),
651 manga_status: manga.map(|m| m.manga_status as i16),
652 manga_setup_time: manga.and_then(|m| dt_legacy(m.setup_time)),
653 image: manga.map(|m| m.image.clone()),
654 provider_name: provider.map(|m| m.username.clone()),
655 translator_name: translator.map(|m| m.username.clone()),
656 intern: translator.map(|m| m.intern as i16),
657 email: translator.and_then(|m| m.email.clone()),
658 publish_link: ep.publish_link.clone(),
659 }
660 })
661 .collect())
662}
663
664pub async fn get_work_reminder_rss(
666 db: &DatabaseConnection,
667 post_name: &str,
668) -> crate::error::ApiResult<Vec<WorkReminderRssRow>> {
669 let reminders = workreminder::Entity::find()
670 .filter(workreminder::Column::MyName.eq(post_name))
671 .order_by_desc(workreminder::Column::Id)
672 .limit(RSS_MAX_ITEMS)
673 .all(db)
674 .await?;
675 if reminders.is_empty() {
676 return Ok(vec![]);
677 }
678 let ep_ids: Vec<i32> = reminders.iter().filter_map(|r| r.episode_id).collect();
679 let episodes: HashMap<i32, mangaepisodetb::Model> = if ep_ids.is_empty() {
680 HashMap::new()
681 } else {
682 mangaepisodetb::Entity::find()
683 .filter(mangaepisodetb::Column::Id.is_in(ep_ids))
684 .all(db)
685 .await?
686 .into_iter()
687 .map(|e| (e.id, e))
688 .collect()
689 };
690 let manga_ids: Vec<i32> = episodes.values().map(|e| e.manga_id).collect();
691 let mangas: HashMap<i32, mangatb::Model> = if manga_ids.is_empty() {
692 HashMap::new()
693 } else {
694 mangatb::Entity::find()
695 .filter(mangatb::Column::Id.is_in(manga_ids))
696 .all(db)
697 .await?
698 .into_iter()
699 .map(|m| (m.id, m))
700 .collect()
701 };
702 let mut member_ids = Vec::new();
703 for r in &reminders {
704 if let Some(id) = r.member_id {
705 member_ids.push(id);
706 }
707 if let Some(ep_id) = r.episode_id {
708 if let Some(ep) = episodes.get(&ep_id) {
709 if let Some(my_name) = r.my_name.as_deref() {
710 if let Some(id) = next_post_member_id(ep, my_name) {
711 member_ids.push(id);
712 }
713 }
714 }
715 }
716 }
717 member_ids.sort_unstable();
718 member_ids.dedup();
719 let members: HashMap<i32, membertb::Model> = if member_ids.is_empty() {
720 HashMap::new()
721 } else {
722 membertb::Entity::find()
723 .filter(membertb::Column::Id.is_in(member_ids))
724 .all(db)
725 .await?
726 .into_iter()
727 .map(|m| (m.id, m))
728 .collect()
729 };
730 Ok(reminders
731 .into_iter()
732 .filter_map(|r| {
733 let ep_id = r.episode_id?;
734 let ep = episodes.get(&ep_id);
735 let manga = ep.and_then(|e| mangas.get(&e.manga_id));
736 let mem_now = r.member_id.and_then(|id| members.get(&id));
737 let my_name = r.my_name.clone().unwrap_or_default();
738 let next_id = ep.and_then(|e| next_post_member_id(e, &my_name));
739 let mem_next = next_id.and_then(|id| members.get(&id));
740 Some(WorkReminderRssRow {
741 episode_id: ep_id,
742 manga_id: manga.map(|m| m.id).unwrap_or(0),
743 manga_episode: ep.map(|e| e.manga_episode.clone()),
744 manga_episode_name: ep.and_then(|e| e.manga_episode_name.clone()),
745 episode_type: ep
746 .map(|e| rss_episode_type(&e.episode_type))
747 .unwrap_or_default(),
748 setup_time: ep.and_then(|e| dt_legacy(e.setup_time)),
749 manga_name: manga.map(|m| m.manga_tran_name.clone()),
750 manga_ori_name: manga.map(|m| m.manga_ori_name.clone()),
751 category: manga.map(|m| m.category as i16),
752 manga_status: manga.map(|m| m.manga_status as i16),
753 manga_setup_time: manga.and_then(|m| dt_legacy(m.setup_time)),
754 image: manga.map(|m| m.image.clone()),
755 my_name,
756 username_now: mem_now.map(|m| m.username.clone()),
757 intern_now: mem_now.map(|m| m.intern as i16),
758 email_now: mem_now.and_then(|m| m.email.clone()),
759 username: mem_next.map(|m| m.username.clone()),
760 intern: mem_next.map(|m| m.intern as i16),
761 email: mem_next.and_then(|m| m.email.clone()),
762 })
763 })
764 .collect())
765}
766
767pub fn glossary_to_vo(g: glossary::Model, update_member_name: Option<String>) -> GlossaryVo {
769 GlossaryVo {
770 id: g.id,
771 title: g.title,
772 content: g.content,
773 image: g.image,
774 r#type: g.r#type.map(|v| v as i16),
775 manga_id: g.manga_id.map(|v| v as i16),
776 first: g.first,
777 update_time: dt_legacy_opt(g.update_time),
778 update_by: g.update_by,
779 update_member_name,
780 }
781}
782
783async fn list_filter(
785 db: &DatabaseConnection,
786 manga_tran_name: Option<&str>,
787 manga_ori_name: Option<&str>,
788 category: Option<i16>,
789 manga_status: Option<i16>,
790 author_name: Option<&str>,
791 magazine_name: Option<&str>,
792) -> crate::error::ApiResult<Condition> {
793 let mut cond = Condition::all();
794 if let Some(v) = manga_tran_name {
795 cond = cond.add(mangatb::Column::MangaTranName.like(format!("%{v}%")));
796 }
797 if let Some(v) = manga_ori_name {
798 cond = cond.add(mangatb::Column::MangaOriName.eq(v));
799 }
800 if let Some(v) = category {
801 cond = cond.add(mangatb::Column::Category.eq(v as i32));
802 }
803 if let Some(v) = manga_status {
804 cond = cond.add(mangatb::Column::MangaStatus.eq(v as i32));
805 }
806 if let Some(name) = author_name {
807 let authors = authortb::Entity::find()
808 .filter(authortb::Column::AuthorName.eq(name))
809 .all(db)
810 .await?;
811 if authors.is_empty() {
812 return Ok(Condition::all().add(mangatb::Column::Id.eq(-1)));
813 }
814 let author_ids: Vec<i32> = authors.iter().map(|a| a.id).collect();
815 let links = mangaauthor::Entity::find()
816 .filter(mangaauthor::Column::AuthorId.is_in(author_ids))
817 .all(db)
818 .await?;
819 if links.is_empty() {
820 return Ok(Condition::all().add(mangatb::Column::Id.eq(-1)));
821 }
822 let manga_ids: Vec<i32> = links.iter().map(|l| l.manga_id).collect();
823 cond = cond.add(mangatb::Column::Id.is_in(manga_ids));
824 }
825 if let Some(name) = magazine_name {
826 let mags = magazine::Entity::find()
827 .filter(magazine::Column::MagazineName.eq(name))
828 .all(db)
829 .await?;
830 if mags.is_empty() {
831 return Ok(Condition::all().add(mangatb::Column::Id.eq(-1)));
832 }
833 let mag_ids: Vec<i32> = mags.iter().map(|m| m.id).collect();
834 let links = mangamagazine::Entity::find()
835 .filter(mangamagazine::Column::MagazineId.is_in(mag_ids))
836 .all(db)
837 .await?;
838 if links.is_empty() {
839 return Ok(Condition::all().add(mangatb::Column::Id.eq(-1)));
840 }
841 let manga_ids: Vec<i32> = links.iter().filter_map(|l| l.manga_id).collect();
842 cond = cond.add(mangatb::Column::Id.is_in(manga_ids));
843 }
844 Ok(cond)
845}
846
847async fn assemble_manga_cards(
849 db: &DatabaseConnection,
850 mangas: &[mangatb::Model],
851 with_magazine: bool,
852) -> crate::error::ApiResult<Vec<MangaCardVo>> {
853 if mangas.is_empty() {
854 return Ok(vec![]);
855 }
856 let ctx = load_list_context(db, mangas, with_magazine, true).await?;
857 Ok(mangas.iter().map(|m| to_manga_card(m, &ctx)).collect())
858}
859
860async fn assemble_rss_manga(
862 db: &DatabaseConnection,
863 mangas: &[mangatb::Model],
864 include_update_time: bool,
865) -> crate::error::ApiResult<Vec<RssMangaRow>> {
866 if mangas.is_empty() {
867 return Ok(vec![]);
868 }
869 let ctx = load_list_context(db, mangas, false, false).await?;
870 Ok(mangas
871 .iter()
872 .map(|m| to_rss_manga(m, &ctx, include_update_time))
873 .collect())
874}
875
876async fn load_list_context(
878 db: &DatabaseConnection,
879 mangas: &[mangatb::Model],
880 with_magazine: bool,
881 with_latest_detail: bool,
882) -> crate::error::ApiResult<MangaListContext> {
883 if mangas.is_empty() {
884 return Ok(MangaListContext {
885 author1: HashMap::new(),
886 author2: HashMap::new(),
887 magazines: HashMap::new(),
888 latest_details: HashMap::new(),
889 });
890 }
891 let manga_ids: Vec<i32> = mangas.iter().map(|m| m.id).collect();
892 let author_links = mangaauthor::Entity::find()
893 .filter(mangaauthor::Column::MangaId.is_in(manga_ids.clone()))
894 .all(db)
895 .await?;
896 let author_ids: Vec<i32> = author_links.iter().map(|l| l.author_id).collect();
897 let authors: HashMap<i32, authortb::Model> = if author_ids.is_empty() {
898 HashMap::new()
899 } else {
900 authortb::Entity::find()
901 .filter(authortb::Column::Id.is_in(author_ids))
902 .all(db)
903 .await?
904 .into_iter()
905 .map(|a| (a.id, a))
906 .collect()
907 };
908 let mut author1 = HashMap::new();
909 let mut author2 = HashMap::new();
910 for link in author_links {
911 let Some(author) = authors.get(&link.author_id) else {
912 continue;
913 };
914 let entry = (author.id, author.author_name.clone());
915 if link.r#type == 1 {
916 author1.insert(link.manga_id, entry);
917 } else if link.r#type == 2 {
918 author2.insert(link.manga_id, entry);
919 }
920 }
921 let magazines = if with_magazine {
922 load_magazines(db, &manga_ids).await?
923 } else {
924 HashMap::new()
925 };
926 let latest_details = if with_latest_detail {
927 load_latest_details(db, &manga_ids).await?
928 } else {
929 HashMap::new()
930 };
931 Ok(MangaListContext {
932 author1,
933 author2,
934 magazines,
935 latest_details,
936 })
937}
938
939async fn load_magazines(
941 db: &DatabaseConnection,
942 manga_ids: &[i32],
943) -> crate::error::ApiResult<HashMap<i32, (i32, String)>> {
944 let links = mangamagazine::Entity::find()
945 .filter(mangamagazine::Column::MangaId.is_in(manga_ids.to_vec()))
946 .all(db)
947 .await?;
948 let mag_ids: Vec<i32> = links.iter().filter_map(|l| l.magazine_id).collect();
949 let mags: HashMap<i32, magazine::Model> = if mag_ids.is_empty() {
950 HashMap::new()
951 } else {
952 magazine::Entity::find()
953 .filter(magazine::Column::Id.is_in(mag_ids))
954 .all(db)
955 .await?
956 .into_iter()
957 .map(|m| (m.id, m))
958 .collect()
959 };
960 let mut out = HashMap::new();
961 for link in links {
962 let (Some(manga_id), Some(mag_id)) = (link.manga_id, link.magazine_id) else {
963 continue;
964 };
965 if let Some(mag) = mags.get(&mag_id) {
966 out.insert(manga_id, (mag.id, mag.magazine_name.clone()));
967 }
968 }
969 Ok(out)
970}
971
972async fn load_latest_details(
974 db: &DatabaseConnection,
975 manga_ids: &[i32],
976) -> crate::error::ApiResult<HashMap<i32, mangaepisodedetail::Model>> {
977 if manga_ids.is_empty() {
978 return Ok(HashMap::new());
979 }
980 let placeholders: String = (1..=manga_ids.len())
981 .map(|i| format!("${i}"))
982 .collect::<Vec<_>>()
983 .join(", ");
984 let sql = format!(
985 r#"SELECT DISTINCT ON (me."mangaId") me."mangaId", me."Id" AS "episodeId"
986 FROM mangaepisodetb me
987 WHERE me."mangaId" IN ({placeholders})
988 ORDER BY me."mangaId", me."Id" DESC"#
989 );
990 let values: Vec<Value> = manga_ids.iter().copied().map(Value::from).collect();
991 let rows = db
992 .query_all(Statement::from_sql_and_values(
993 DbBackend::Postgres,
994 &sql,
995 values,
996 ))
997 .await?;
998 let mut latest_ep: HashMap<i32, i32> = HashMap::new();
999 for row in rows {
1000 let manga_id: i32 = row.try_get("", "mangaId")?;
1001 let episode_id: i32 = row.try_get("", "episodeId")?;
1002 latest_ep.insert(manga_id, episode_id);
1003 }
1004 let ep_ids: Vec<i32> = latest_ep.values().copied().collect();
1005 if ep_ids.is_empty() {
1006 return Ok(HashMap::new());
1007 }
1008 let details: HashMap<i32, mangaepisodedetail::Model> = mangaepisodedetail::Entity::find()
1009 .filter(mangaepisodedetail::Column::EpisodeId.is_in(ep_ids))
1010 .all(db)
1011 .await?
1012 .into_iter()
1013 .map(|d| (d.episode_id, d))
1014 .collect();
1015 let mut out = HashMap::new();
1016 for (manga_id, ep_id) in latest_ep {
1017 if let Some(d) = details.get(&ep_id) {
1018 out.insert(manga_id, d.clone());
1019 }
1020 }
1021 Ok(out)
1022}
1023
1024fn to_manga_card(m: &mangatb::Model, ctx: &MangaListContext) -> MangaCardVo {
1026 let med = ctx.latest_details.get(&m.id);
1027 let a1 = ctx.author1.get(&m.id);
1028 let a2 = ctx.author2.get(&m.id);
1029 MangaCardVo {
1030 id: m.id,
1031 manga_tran_name: Some(m.manga_tran_name.clone()),
1032 manga_ori_name: Some(m.manga_ori_name.clone()),
1033 image: Some(m.image.clone()),
1034 category: Some(m.category as i16),
1035 manga_status: Some(m.manga_status as i16),
1036 setup_time: dt_legacy(m.setup_time),
1037 update_time: dt_legacy(m.update_time),
1038 author_name: a1.map(|(_, n)| n.clone()),
1039 author_name2: a2.map(|(_, n)| n.clone()),
1040 author_id2: a2.map(|(id, _)| *id),
1041 magazine_name: ctx.magazines.get(&m.id).map(|(_, n)| n.clone()),
1042 translator_setup_time: med.and_then(|d| dt_legacy_opt(d.translator_setup_time)),
1043 translator_update_time: med.and_then(|d| dt_legacy_opt(d.translator_update_time)),
1044 proofreader_setup_time: med.and_then(|d| dt_legacy_opt(d.proofreader_setup_time)),
1045 proofreader_update_time: med.and_then(|d| dt_legacy_opt(d.proofreader_update_time)),
1046 letterer_setup_time: med.and_then(|d| dt_legacy_opt(d.letterer_setup_time)),
1047 letterer_update_time: med.and_then(|d| dt_legacy_opt(d.letterer_update_time)),
1048 reviewer_setup_time: med.and_then(|d| dt_legacy_opt(d.reviewer_setup_time)),
1049 reviewer_update_time: med.and_then(|d| dt_legacy_opt(d.reviewer_update_time)),
1050 timer_setup_time: med.and_then(|d| dt_legacy_opt(d.timer_setup_time)),
1051 timer_update_time: med.and_then(|d| dt_legacy_opt(d.timer_update_time)),
1052 }
1053}
1054
1055fn to_manga_detail(m: &mangatb::Model, ctx: &MangaListContext) -> MangaDetailVo {
1057 let a1 = ctx.author1.get(&m.id);
1058 let a2 = ctx.author2.get(&m.id);
1059 let mag = ctx.magazines.get(&m.id);
1060 MangaDetailVo {
1061 id: m.id,
1062 manga_tran_name: Some(m.manga_tran_name.clone()),
1063 manga_ori_name: Some(m.manga_ori_name.clone()),
1064 image: Some(m.image.clone()),
1065 category: Some(m.category as i16),
1066 manga_status: Some(m.manga_status as i16),
1067 setup_time: dt_legacy(m.setup_time),
1068 update_time: dt_legacy(m.update_time),
1069 link: m.link.clone(),
1070 introduction: m.introduction.clone(),
1071 author_name: a1.map(|(_, n)| n.clone()),
1072 author_id: a1.map(|(id, _)| *id),
1073 author_name2: a2.map(|(_, n)| n.clone()),
1074 author_id2: a2.map(|(id, _)| *id),
1075 magazine_name: mag.map(|(_, n)| n.clone()),
1076 magazine_id: mag.map(|(id, _)| *id),
1077 }
1078}
1079
1080fn to_manga_response(
1082 m: &mangatb::Model,
1083 ctx: &MangaListContext,
1084) -> crate::entity::manga::MangaResponse {
1085 let med = ctx.latest_details.get(&m.id);
1086 let a1 = ctx.author1.get(&m.id);
1087 let a2 = ctx.author2.get(&m.id);
1088 let mag = ctx.magazines.get(&m.id);
1089 crate::entity::manga::MangaResponse {
1090 id: Some(m.id),
1091 manga_tran_name: Some(m.manga_tran_name.clone()),
1092 manga_ori_name: Some(m.manga_ori_name.clone()),
1093 image: Some(m.image.clone()),
1094 category: Some(m.category as i16),
1095 manga_status: Some(m.manga_status as i16),
1096 setup_time: dt_legacy(m.setup_time),
1097 update_time: dt_legacy(m.update_time),
1098 link: m.link.clone(),
1099 introduction: m.introduction.clone(),
1100 author_name: a1.map(|(_, n)| n.clone()),
1101 author_id: a1.map(|(id, _)| *id),
1102 author_name2: a2.map(|(_, n)| n.clone()),
1103 author_id2: a2.map(|(id, _)| *id),
1104 magazine_name: mag.map(|(_, n)| n.clone()),
1105 magazine_id: mag.map(|(id, _)| *id),
1106 translator_setup_time: med.and_then(|d| dt_legacy_opt(d.translator_setup_time)),
1107 translator_update_time: med.and_then(|d| dt_legacy_opt(d.translator_update_time)),
1108 proofreader_setup_time: med.and_then(|d| dt_legacy_opt(d.proofreader_setup_time)),
1109 proofreader_update_time: med.and_then(|d| dt_legacy_opt(d.proofreader_update_time)),
1110 letterer_setup_time: med.and_then(|d| dt_legacy_opt(d.letterer_setup_time)),
1111 letterer_update_time: med.and_then(|d| dt_legacy_opt(d.letterer_update_time)),
1112 reviewer_setup_time: med.and_then(|d| dt_legacy_opt(d.reviewer_setup_time)),
1113 reviewer_update_time: med.and_then(|d| dt_legacy_opt(d.reviewer_update_time)),
1114 }
1115}
1116
1117fn to_rss_manga(m: &mangatb::Model, ctx: &MangaListContext, include_update: bool) -> RssMangaRow {
1119 let a1 = ctx.author1.get(&m.id);
1120 let a2 = ctx.author2.get(&m.id);
1121 RssMangaRow {
1122 id: m.id,
1123 manga_tran_name: Some(m.manga_tran_name.clone()),
1124 manga_ori_name: Some(m.manga_ori_name.clone()),
1125 category: Some(m.category as i16),
1126 manga_status: Some(m.manga_status as i16),
1127 image: Some(m.image.clone()),
1128 setup_time: dt_legacy(m.setup_time),
1129 update_time: if include_update {
1130 dt_legacy(m.update_time)
1131 } else {
1132 None
1133 },
1134 author_id: a1.map(|(id, _)| *id),
1135 author_name: a1.map(|(_, n)| n.clone()),
1136 author_id2: a2.map(|(id, _)| *id),
1137 author_name2: a2.map(|(_, n)| n.clone()),
1138 }
1139}
1140
1141fn next_post_member_id(ep: &mangaepisodetb::Model, my_name: &str) -> Option<i32> {
1143 match my_name {
1144 "translator" => ep.proofreader_id,
1145 "proofreader" => ep.letterer_id,
1146 "letterer" => ep.reviewer_id,
1147 "timer" => ep.timer_id,
1148 _ => None,
1149 }
1150}
1151
1152fn dt_legacy(n: chrono::NaiveDateTime) -> Option<DateTime<Utc>> {
1154 Some(crate::utils::legacy_time::naive_db_to_api_utc(n))
1155}
1156
1157fn dt_legacy_opt(n: Option<chrono::NaiveDateTime>) -> Option<DateTime<Utc>> {
1159 n.map(crate::utils::legacy_time::naive_db_to_api_utc)
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164 use super::{rss_episode_type, MangaListSort, MangaListSortField, MangaListSortOrder};
1165 use crate::entity::episode::EpisodeType;
1166
1167 #[test]
1169 fn rss_episode_type_defaults_unknown_values_to_main() {
1170 assert_eq!(rss_episode_type("EXTRA"), EpisodeType::Extra);
1171 assert_eq!(rss_episode_type("MAIN"), EpisodeType::Main);
1172 assert_eq!(rss_episode_type(""), EpisodeType::Main);
1173 }
1174
1175 #[test]
1176 fn manga_list_sort_accepts_time_aliases() {
1177 let sort = MangaListSort::from_query(Some("createTime"), Some("ascending"));
1178 assert_eq!(sort.field, MangaListSortField::SetupTime);
1179 assert_eq!(sort.order, MangaListSortOrder::Asc);
1180 assert_eq!(sort.cache_field(), "setupTime");
1181 assert_eq!(sort.cache_order(), "ascending");
1182
1183 let sort = MangaListSort::from_query(Some("updated_at"), Some("desc"));
1184 assert_eq!(sort.field, MangaListSortField::UpdateTime);
1185 assert_eq!(sort.order, MangaListSortOrder::Desc);
1186 }
1187
1188 #[test]
1189 fn manga_list_sort_defaults_to_update_time_desc() {
1190 let sort = MangaListSort::from_query(Some("unknown"), Some("sideways"));
1191 assert_eq!(sort.field, MangaListSortField::UpdateTime);
1192 assert_eq!(sort.order, MangaListSortOrder::Desc);
1193 assert_eq!(sort.cache_field(), "updateTime");
1194 assert_eq!(sort.cache_order(), "descending");
1195 }
1196}