1use crate::entity::episode::{
4 EpisodeDetailVo, EpisodeListVo, EpisodeSimpleListVo, EpisodeType, NewestEpisodeVo, RoleTakerVo,
5 UploadPageVo,
6};
7use crate::repository::episode_sort;
8use crate::sea_entity::{
9 editor_page, episode_role_taker, mangaepisodedetail, mangaepisodetb, mangatb, membertb, oss,
10};
11use chrono::{DateTime, Utc};
12use sea_orm::{
13 sea_query::{NullOrdering, Order},
14 ColumnTrait, Condition, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
15 QueryOrder, QuerySelect,
16};
17use std::collections::HashMap;
18
19struct WorkflowContext {
21 details: HashMap<i32, mangaepisodedetail::Model>,
23 members: HashMap<i32, String>,
25 member_emails: HashMap<i32, String>,
27 oss_names: HashMap<i32, String>,
29 editor_page_counts: HashMap<i32, i64>,
31 role_takers: HashMap<(i32, String), Vec<RoleTakerVo>>,
33}
34
35pub async fn list_simple_by_manga_id(
37 db: &DatabaseConnection,
38 manga_id: i32,
39) -> crate::error::ApiResult<Vec<EpisodeSimpleListVo>> {
40 let episodes = episodes_by_manga_ordered(db, manga_id).await?;
41 let ctx = load_workflow_context(db, &episodes).await?;
42 Ok(episodes
43 .iter()
44 .map(|ep| to_simple_list_vo(ep, &ctx))
45 .collect())
46}
47
48pub async fn page_episodes_by_manga(
50 db: &DatabaseConnection,
51 manga_id: i32,
52 page: i32,
53 page_size: i32,
54 episode_type: EpisodeType,
55) -> crate::error::ApiResult<(i64, Vec<EpisodeListVo>)> {
56 let page = page.max(1);
57 let page_size = page_size.max(1);
58 let offset = ((page - 1) * page_size) as u64;
59 let filter = Condition::all()
60 .add(mangaepisodetb::Column::MangaId.eq(manga_id))
61 .add(mangaepisodetb::Column::EpisodeType.eq(episode_type.as_str()));
62 let order = episode_sort::episode_label_sort_expr(r#""mangaEpisode""#);
63 let filter_count = filter.clone();
64 let filter_page = filter.clone();
65 let (total, episodes) = tokio::try_join!(
66 async move {
67 Ok::<_, crate::error::AppError>(
68 mangaepisodetb::Entity::find()
69 .filter(filter_count)
70 .count(db)
71 .await? as i64,
72 )
73 },
74 async move {
75 Ok(mangaepisodetb::Entity::find()
76 .filter(filter_page)
77 .order_by_with_nulls(order, Order::Desc, NullOrdering::Last)
78 .limit(page_size as u64)
79 .offset(offset)
80 .all(db)
81 .await?)
82 },
83 )?;
84 if episodes.is_empty() {
85 return Ok((total, vec![]));
86 }
87 let ctx = load_workflow_context(db, &episodes).await?;
88 Ok((
89 total,
90 episodes.iter().map(|ep| to_list_vo(ep, &ctx)).collect(),
91 ))
92}
93
94pub async fn list_full_by_manga_id(
96 db: &DatabaseConnection,
97 manga_id: i32,
98) -> crate::error::ApiResult<Vec<EpisodeListVo>> {
99 let episodes = episodes_by_manga_ordered(db, manga_id).await?;
100 let ctx = load_workflow_context(db, &episodes).await?;
101 Ok(episodes.iter().map(|ep| to_list_vo(ep, &ctx)).collect())
102}
103
104pub async fn get_detail_by_id(
106 db: &DatabaseConnection,
107 id: i32,
108) -> crate::error::ApiResult<Option<EpisodeDetailVo>> {
109 let ep = mangaepisodetb::Entity::find_by_id(id).one(db).await?;
110 let Some(ep) = ep else {
111 return Ok(None);
112 };
113 let ctx = load_workflow_context(db, std::slice::from_ref(&ep)).await?;
114 Ok(Some(to_detail_vo(&ep, &ctx)))
115}
116
117pub async fn get_newest_by_manga_id(
119 db: &DatabaseConnection,
120 manga_id: i32,
121) -> crate::error::ApiResult<Option<NewestEpisodeVo>> {
122 let ep = mangaepisodetb::Entity::find()
123 .filter(mangaepisodetb::Column::MangaId.eq(manga_id))
124 .filter(mangaepisodetb::Column::EpisodeType.eq(EpisodeType::Main.as_str()))
125 .order_by_desc(mangaepisodetb::Column::Id)
126 .one(db)
127 .await?;
128 Ok(ep.map(|e| NewestEpisodeVo {
129 manga_episode: Some(e.manga_episode),
130 translator_id: e.translator_id,
131 proofreader_id: e.proofreader_id,
132 letterer_id: e.letterer_id,
133 reviewer_id: e.reviewer_id,
134 }))
135}
136
137pub async fn page_uploaded_submit(
139 db: &DatabaseConnection,
140 page: i32,
141 page_size: i32,
142 manga_tran_name: Option<&str>,
143 username: Option<&str>,
144) -> crate::error::ApiResult<(i64, Vec<UploadPageVo>)> {
145 let filter = uploaded_submit_filter(db, manga_tran_name, username).await?;
146 let page = page.max(1);
147 let page_size = page_size.max(1);
148 let offset = ((page - 1) * page_size) as u64;
149 let filter_count = filter.clone();
150 let filter_page = filter.clone();
151 let (total, episodes) = tokio::try_join!(
152 async move {
153 Ok::<_, crate::error::AppError>(
154 mangaepisodetb::Entity::find()
155 .filter(filter_count)
156 .count(db)
157 .await? as i64,
158 )
159 },
160 async move {
161 Ok(mangaepisodetb::Entity::find()
162 .filter(filter_page)
163 .order_by_desc(mangaepisodetb::Column::UpdateTime)
164 .limit(page_size as u64)
165 .offset(offset)
166 .all(db)
167 .await?)
168 },
169 )?;
170 if total == 0 {
171 return Ok((0, vec![]));
172 }
173 let ctx = load_upload_context(db, &episodes).await?;
174 Ok((
175 total,
176 episodes.iter().map(|ep| to_upload_vo(ep, &ctx)).collect(),
177 ))
178}
179
180async fn episodes_by_manga_ordered(
182 db: &DatabaseConnection,
183 manga_id: i32,
184) -> crate::error::ApiResult<Vec<mangaepisodetb::Model>> {
185 let rows = mangaepisodetb::Entity::find()
186 .filter(mangaepisodetb::Column::MangaId.eq(manga_id))
187 .order_by_with_nulls(
188 episode_sort::episode_label_sort_expr(r#""mangaEpisode""#),
189 Order::Desc,
190 NullOrdering::Last,
191 )
192 .all(db)
193 .await?;
194 Ok(rows)
195}
196
197async fn uploaded_submit_filter(
199 db: &DatabaseConnection,
200 manga_tran_name: Option<&str>,
201 username: Option<&str>,
202) -> crate::error::ApiResult<Condition> {
203 use mangaepisodetb::Column as C;
204 let file_cond = Condition::any()
205 .add(C::TranslatorFile.is_not_null())
206 .add(C::ProofreaderFile.is_not_null())
207 .add(C::TimerFile.is_not_null())
208 .add(C::TranslatorFileOssId.is_not_null())
209 .add(C::ProofreaderFileOssId.is_not_null())
210 .add(C::TimerFileOssId.is_not_null());
211 let mut all = Condition::all().add(file_cond);
212 if let Some(name) = manga_tran_name {
213 let manga = mangatb::Entity::find()
214 .filter(mangatb::Column::MangaTranName.eq(name))
215 .one(db)
216 .await?;
217 let Some(m) = manga else {
218 return Ok(Condition::all().add(C::Id.eq(-1)));
219 };
220 all = all.add(C::MangaId.eq(m.id));
221 }
222 if let Some(u) = username {
223 let members = membertb::Entity::find()
224 .filter(membertb::Column::Username.eq(u))
225 .all(db)
226 .await?;
227 if members.is_empty() {
228 return Ok(Condition::all().add(C::Id.eq(-1)));
229 }
230 let ids: Vec<i32> = members.iter().map(|m| m.id).collect();
231 all = all.add(
232 Condition::any()
233 .add(C::TranslatorId.is_in(ids.clone()))
234 .add(C::ProofreaderId.is_in(ids)),
235 );
236 }
237 Ok(all)
238}
239
240async fn load_workflow_context(
242 db: &DatabaseConnection,
243 episodes: &[mangaepisodetb::Model],
244) -> crate::error::ApiResult<WorkflowContext> {
245 if episodes.is_empty() {
246 return Ok(WorkflowContext {
247 details: HashMap::new(),
248 members: HashMap::new(),
249 member_emails: HashMap::new(),
250 oss_names: HashMap::new(),
251 editor_page_counts: HashMap::new(),
252 role_takers: HashMap::new(),
253 });
254 }
255 let ep_ids: Vec<i32> = episodes.iter().map(|e| e.id).collect();
256 let details: HashMap<i32, mangaepisodedetail::Model> = mangaepisodedetail::Entity::find()
257 .filter(mangaepisodedetail::Column::EpisodeId.is_in(ep_ids.clone()))
258 .all(db)
259 .await?
260 .into_iter()
261 .map(|d| (d.episode_id, d))
262 .collect();
263 let editor_page_counts = load_editor_page_counts(db, &ep_ids).await?;
264
265 let mut member_ids = Vec::new();
266 let mut oss_ids = Vec::new();
267 let role_taker_rows = episode_role_taker::Entity::find()
268 .filter(episode_role_taker::Column::EpisodeId.is_in(ep_ids.clone()))
269 .order_by_asc(episode_role_taker::Column::SetupTime)
270 .all(db)
271 .await?;
272 for ep in episodes {
273 for mid in [
274 ep.provider_id,
275 ep.translator_id,
276 ep.proofreader_id,
277 ep.letterer_id,
278 ep.timer_id,
279 ep.reviewer_id,
280 ] {
281 if let Some(id) = mid {
282 member_ids.push(id);
283 }
284 }
285 for oid in [
286 ep.provider_file_oss_id,
287 ep.translator_file_oss_id,
288 ep.proofreader_file_oss_id,
289 ep.letterer_file_oss_id,
290 ep.timer_file_oss_id,
291 ] {
292 if let Some(id) = oid {
293 oss_ids.push(id);
294 }
295 }
296 }
297 for taker in &role_taker_rows {
298 member_ids.push(taker.member_id);
299 if let Some(oss_id) = taker.file_oss_id {
300 oss_ids.push(oss_id);
301 }
302 }
303 member_ids.sort_unstable();
304 member_ids.dedup();
305 oss_ids.sort_unstable();
306 oss_ids.dedup();
307
308 let (members, member_emails, member_avatars) = load_members(db, &member_ids).await?;
309 let oss_names = load_oss_names(db, &oss_ids).await?;
310 let role_takers = group_role_takers(role_taker_rows, &members, &member_avatars, &oss_names);
311 Ok(WorkflowContext {
312 details,
313 members,
314 member_emails,
315 oss_names,
316 editor_page_counts,
317 role_takers,
318 })
319}
320
321async fn load_editor_page_counts(
323 db: &DatabaseConnection,
324 episode_ids: &[i32],
325) -> crate::error::ApiResult<HashMap<i32, i64>> {
326 if episode_ids.is_empty() {
327 return Ok(HashMap::new());
328 }
329 let rows: Vec<(i32, i64)> = editor_page::Entity::find()
330 .select_only()
331 .column(editor_page::Column::EpisodeId)
332 .column_as(editor_page::Column::Id.count(), "page_count")
333 .filter(editor_page::Column::EpisodeId.is_in(episode_ids.to_vec()))
334 .group_by(editor_page::Column::EpisodeId)
335 .into_tuple()
336 .all(db)
337 .await?;
338 Ok(rows.into_iter().collect())
339}
340
341struct UploadContext {
343 workflow: WorkflowContext,
345 manga_names: HashMap<i32, String>,
347}
348
349async fn load_upload_context(
351 db: &DatabaseConnection,
352 episodes: &[mangaepisodetb::Model],
353) -> crate::error::ApiResult<UploadContext> {
354 let workflow = load_workflow_context(db, episodes).await?;
355 let manga_ids: Vec<i32> = episodes.iter().map(|e| e.manga_id).collect();
356 let mangas: HashMap<i32, String> = if manga_ids.is_empty() {
357 HashMap::new()
358 } else {
359 mangatb::Entity::find()
360 .filter(mangatb::Column::Id.is_in(manga_ids))
361 .all(db)
362 .await?
363 .into_iter()
364 .map(|m| (m.id, m.manga_tran_name))
365 .collect()
366 };
367 Ok(UploadContext {
368 workflow,
369 manga_names: mangas,
370 })
371}
372
373async fn load_members(
375 db: &DatabaseConnection,
376 ids: &[i32],
377) -> crate::error::ApiResult<(
378 HashMap<i32, String>,
379 HashMap<i32, String>,
380 HashMap<i32, String>,
381)> {
382 if ids.is_empty() {
383 return Ok((HashMap::new(), HashMap::new(), HashMap::new()));
384 }
385 let rows = membertb::Entity::find()
386 .filter(membertb::Column::Id.is_in(ids.to_vec()))
387 .all(db)
388 .await?;
389 let mut names = HashMap::new();
390 let mut emails = HashMap::new();
391 let mut avatars = HashMap::new();
392 for m in rows {
393 let id = m.id;
394 names.insert(m.id, m.username);
395 if let Some(e) = m.email {
396 emails.insert(id, e);
397 }
398 if let Some(a) = m.avatar_url {
399 avatars.insert(id, a);
400 }
401 }
402 Ok((names, emails, avatars))
403}
404
405async fn load_oss_names(
407 db: &DatabaseConnection,
408 ids: &[i32],
409) -> crate::error::ApiResult<HashMap<i32, String>> {
410 if ids.is_empty() {
411 return Ok(HashMap::new());
412 }
413 Ok(oss::Entity::find()
414 .filter(oss::Column::Id.is_in(ids.to_vec()))
415 .all(db)
416 .await?
417 .into_iter()
418 .map(|o| (o.id, o.filename))
419 .collect())
420}
421
422fn group_role_takers(
424 rows: Vec<episode_role_taker::Model>,
425 members: &HashMap<i32, String>,
426 avatars: &HashMap<i32, String>,
427 oss_names: &HashMap<i32, String>,
428) -> HashMap<(i32, String), Vec<RoleTakerVo>> {
429 let mut grouped: HashMap<(i32, String), Vec<RoleTakerVo>> = HashMap::new();
430 for row in rows {
431 let role = row.role.clone();
432 grouped
433 .entry((row.episode_id, role))
434 .or_default()
435 .push(RoleTakerVo {
436 member_id: row.member_id,
437 member_name: members.get(&row.member_id).cloned(),
438 avatar_url: avatars.get(&row.member_id).cloned(),
439 setup_time: dt_legacy(row.setup_time),
440 update_time: dt_legacy_opt(row.update_time),
441 file_oss_id: row.file_oss_id,
442 filename: row.file_oss_id.and_then(|id| oss_names.get(&id).cloned()),
443 });
444 }
445 grouped
446}
447
448fn to_list_vo(ep: &mangaepisodetb::Model, ctx: &WorkflowContext) -> EpisodeListVo {
450 let med = ctx.details.get(&ep.id);
451 EpisodeListVo {
452 id: ep.id,
453 manga_id: ep.manga_id,
454 manga_episode: Some(ep.manga_episode.clone()),
455 manga_episode_name: ep.manga_episode_name.clone(),
456 episode_type: if ep.episode_type == EpisodeType::Extra.as_str() {
457 EpisodeType::Extra
458 } else {
459 EpisodeType::Main
460 },
461 provider_id: ep.provider_id,
462 translator_id: ep.translator_id,
463 proofreader_id: ep.proofreader_id,
464 letterer_id: ep.letterer_id,
465 timer_id: ep.timer_id,
466 reviewer_id: ep.reviewer_id,
467 setup_time: dt_legacy(ep.setup_time),
468 update_time: dt_legacy(ep.update_time),
469 translator_file: ep.translator_file.clone(),
470 proofreader_file: ep.proofreader_file.clone(),
471 timer_file: ep.timer_file.clone(),
472 publish_link: ep.publish_link.clone(),
473 provider_file_oss_id: ep.provider_file_oss_id,
474 editor_page_count: ctx.editor_page_counts.get(&ep.id).copied().unwrap_or(0),
475 translator_file_oss_id: ep.translator_file_oss_id,
476 proofreader_file_oss_id: ep.proofreader_file_oss_id,
477 letterer_file_oss_id: ep.letterer_file_oss_id,
478 timer_file_oss_id: ep.timer_file_oss_id,
479 translator_setup_time: med.and_then(|d| dt_legacy_opt(d.translator_setup_time)),
480 translator_update_time: med.and_then(|d| dt_legacy_opt(d.translator_update_time)),
481 proofreader_setup_time: med.and_then(|d| dt_legacy_opt(d.proofreader_setup_time)),
482 proofreader_update_time: med.and_then(|d| dt_legacy_opt(d.proofreader_update_time)),
483 letterer_setup_time: med.and_then(|d| dt_legacy_opt(d.letterer_setup_time)),
484 letterer_update_time: med.and_then(|d| dt_legacy_opt(d.letterer_update_time)),
485 timer_setup_time: med.and_then(|d| dt_legacy_opt(d.timer_setup_time)),
486 timer_update_time: med.and_then(|d| dt_legacy_opt(d.timer_update_time)),
487 reviewer_setup_time: med.and_then(|d| dt_legacy_opt(d.reviewer_setup_time)),
488 reviewer_update_time: med.and_then(|d| dt_legacy_opt(d.reviewer_update_time)),
489 provider_name: ep.provider_id.and_then(|id| ctx.members.get(&id).cloned()),
490 translator_name: ep
491 .translator_id
492 .and_then(|id| ctx.members.get(&id).cloned()),
493 proofreader_name: ep
494 .proofreader_id
495 .and_then(|id| ctx.members.get(&id).cloned()),
496 letterer_name: ep.letterer_id.and_then(|id| ctx.members.get(&id).cloned()),
497 timer_name: ep.timer_id.and_then(|id| ctx.members.get(&id).cloned()),
498 reviewer_name: ep.reviewer_id.and_then(|id| ctx.members.get(&id).cloned()),
499 provider_filename: ep
500 .provider_file_oss_id
501 .and_then(|id| ctx.oss_names.get(&id).cloned()),
502 translator_filename: ep
503 .translator_file_oss_id
504 .and_then(|id| ctx.oss_names.get(&id).cloned()),
505 proofreader_filename: ep
506 .proofreader_file_oss_id
507 .and_then(|id| ctx.oss_names.get(&id).cloned()),
508 letterer_filename: ep
509 .letterer_file_oss_id
510 .and_then(|id| ctx.oss_names.get(&id).cloned()),
511 timer_filename: ep
512 .timer_file_oss_id
513 .and_then(|id| ctx.oss_names.get(&id).cloned()),
514 translator_co_takers: ctx
515 .role_takers
516 .get(&(ep.id, "translator".to_string()))
517 .cloned()
518 .unwrap_or_default(),
519 proofreader_co_takers: ctx
520 .role_takers
521 .get(&(ep.id, "proofreader".to_string()))
522 .cloned()
523 .unwrap_or_default(),
524 }
525}
526
527fn to_simple_list_vo(ep: &mangaepisodetb::Model, ctx: &WorkflowContext) -> EpisodeSimpleListVo {
529 let full = to_list_vo(ep, ctx);
530 EpisodeSimpleListVo {
531 id: full.id,
532 manga_episode: full.manga_episode,
533 manga_episode_name: full.manga_episode_name,
534 episode_type: full.episode_type,
535 translator_file: full.translator_file,
536 proofreader_file: full.proofreader_file,
537 timer_file: full.timer_file,
538 provider_file_oss_id: full.provider_file_oss_id,
539 editor_page_count: full.editor_page_count,
540 translator_file_oss_id: full.translator_file_oss_id,
541 proofreader_file_oss_id: full.proofreader_file_oss_id,
542 letterer_file_oss_id: full.letterer_file_oss_id,
543 timer_file_oss_id: full.timer_file_oss_id,
544 publish_link: full.publish_link,
545 }
546}
547
548fn to_detail_vo(ep: &mangaepisodetb::Model, ctx: &WorkflowContext) -> EpisodeDetailVo {
550 let full = to_list_vo(ep, ctx);
551 EpisodeDetailVo {
552 id: full.id,
553 manga_id: full.manga_id,
554 manga_episode: full.manga_episode,
555 manga_episode_name: full.manga_episode_name,
556 episode_type: full.episode_type,
557 provider_id: full.provider_id,
558 translator_id: full.translator_id,
559 proofreader_id: full.proofreader_id,
560 letterer_id: full.letterer_id,
561 timer_id: full.timer_id,
562 reviewer_id: full.reviewer_id,
563 setup_time: full.setup_time,
564 update_time: full.update_time,
565 publish_link: full.publish_link,
566 }
567}
568
569fn to_upload_vo(ep: &mangaepisodetb::Model, ctx: &UploadContext) -> UploadPageVo {
571 let w = &ctx.workflow;
572 UploadPageVo {
573 id: ep.id,
574 manga_id: ep.manga_id,
575 manga_name: ctx.manga_names.get(&ep.manga_id).cloned(),
576 manga_episode: Some(ep.manga_episode.clone()),
577 update_time: dt_legacy(ep.update_time),
578 translator_file: ep.translator_file.clone(),
579 proofreader_file: ep.proofreader_file.clone(),
580 timer_file: ep.timer_file.clone(),
581 provider_id: ep.provider_id,
582 translator_id: ep.translator_id,
583 proofreader_id: ep.proofreader_id,
584 letterer_id: ep.letterer_id,
585 timer_id: ep.timer_id,
586 reviewer_id: ep.reviewer_id,
587 provider_name: None,
588 translator_name: ep.translator_id.and_then(|id| w.members.get(&id).cloned()),
589 proofreader_name: ep.proofreader_id.and_then(|id| w.members.get(&id).cloned()),
590 letterer_name: ep.letterer_id.and_then(|id| w.members.get(&id).cloned()),
591 timer_name: ep.timer_id.and_then(|id| w.members.get(&id).cloned()),
592 reviewer_name: ep.reviewer_id.and_then(|id| w.members.get(&id).cloned()),
593 provider_file_oss_id: ep.provider_file_oss_id,
594 translator_file_oss_id: ep.translator_file_oss_id,
595 proofreader_file_oss_id: ep.proofreader_file_oss_id,
596 letterer_file_oss_id: ep.letterer_file_oss_id,
597 timer_file_oss_id: ep.timer_file_oss_id,
598 provider_filename: ep
599 .provider_file_oss_id
600 .and_then(|id| w.oss_names.get(&id).cloned()),
601 translator_filename: ep
602 .translator_file_oss_id
603 .and_then(|id| w.oss_names.get(&id).cloned()),
604 proofreader_filename: ep
605 .proofreader_file_oss_id
606 .and_then(|id| w.oss_names.get(&id).cloned()),
607 letterer_filename: ep
608 .letterer_file_oss_id
609 .and_then(|id| w.oss_names.get(&id).cloned()),
610 timer_filename: ep
611 .timer_file_oss_id
612 .and_then(|id| w.oss_names.get(&id).cloned()),
613 email: ep
614 .translator_id
615 .and_then(|id| w.member_emails.get(&id).cloned()),
616 email_now: ep
617 .proofreader_id
618 .and_then(|id| w.member_emails.get(&id).cloned()),
619 }
620}
621
622fn dt_legacy(n: chrono::NaiveDateTime) -> Option<DateTime<Utc>> {
624 Some(crate::utils::legacy_time::naive_db_to_api_utc(n))
625}
626
627fn dt_legacy_opt(n: Option<chrono::NaiveDateTime>) -> Option<DateTime<Utc>> {
629 n.map(crate::utils::legacy_time::naive_db_to_api_utc)
630}