1use crate::entity::{
6 episode::{EpisodeTask, MemberTaskCount, TaskTrackingResponse},
7 manga::Mangatb,
8};
9use chrono::{DateTime, Utc};
10use sqlx::{MySql, Pool, Row};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone)]
15pub struct PendingEpisodeRow {
16 pub episode_id: i32,
18 pub manga_id: i32,
20 pub manga_episode: Option<String>,
22 pub manga_episode_name: Option<String>,
24 pub provider_id: Option<i32>,
26 pub translator_id: Option<i32>,
28 pub proofreader_id: Option<i32>,
30 pub letterer_id: Option<i32>,
32 pub timer_id: Option<i32>,
34 pub reviewer_id: Option<i32>,
36 pub setup_time: Option<DateTime<Utc>>,
38 pub update_time: Option<DateTime<Utc>>,
40 pub translator_file: Option<String>,
42 pub proofreader_file: Option<String>,
44 pub timer_file: Option<String>,
46 pub publish_link: Option<String>,
48 pub provider_file_oss_id: Option<i32>,
50 pub translator_file_oss_id: Option<i32>,
52 pub proofreader_file_oss_id: Option<i32>,
54 pub letterer_file_oss_id: Option<i32>,
56 pub timer_file_oss_id: Option<i32>,
58 pub reviewer_update_time: Option<DateTime<Utc>>,
60 pub manga_tran_name: Option<String>,
62 pub manga_ori_name: Option<String>,
64 pub category: Option<i16>,
66 pub manga_status: Option<i16>,
68 pub image: Option<String>,
70 pub manga_setup_time: Option<DateTime<Utc>>,
72 pub manga_update_time: Option<DateTime<Utc>>,
74 pub link: Option<String>,
76 pub introduction: Option<String>,
78}
79
80#[derive(Debug, Clone)]
82pub struct UnpublishedEpisodeRow {
83 pub episode_id: i32,
85 pub manga_id: i32,
87 pub manga_episode: Option<String>,
89 pub manga_episode_name: Option<String>,
91 pub manga_tran_name: Option<String>,
93 pub category: Option<i16>,
95 pub translator_id: Option<i32>,
97 pub proofreader_id: Option<i32>,
99 pub letterer_id: Option<i32>,
101 pub timer_id: Option<i32>,
103 pub reviewer_id: Option<i32>,
105 pub translator_setup_time: Option<DateTime<Utc>>,
107 pub translator_update_time: Option<DateTime<Utc>>,
109 pub proofreader_setup_time: Option<DateTime<Utc>>,
111 pub proofreader_update_time: Option<DateTime<Utc>>,
113 pub letterer_setup_time: Option<DateTime<Utc>>,
115 pub letterer_update_time: Option<DateTime<Utc>>,
117 pub timer_setup_time: Option<DateTime<Utc>>,
119 pub timer_update_time: Option<DateTime<Utc>>,
121 pub reviewer_setup_time: Option<DateTime<Utc>>,
123 pub reviewer_update_time: Option<DateTime<Utc>>,
125 pub episode_update_time: Option<DateTime<Utc>>,
127}
128
129#[derive(Debug, Clone)]
131pub struct MemberTaskCountRow {
132 pub user_id: i32,
134 pub user_name: Option<String>,
136 pub intern: Option<i32>,
138 pub email: Option<String>,
140 pub last_update_time: Option<String>,
142 pub translator_count: i64,
144 pub proofreader_count: i64,
146 pub letterer_count: i64,
148 pub timer_count: i64,
150 pub reviewer_count: i64,
152 pub stationed_count: i64,
154}
155
156pub struct TaskTrackingRepository {
158 pool: Pool<MySql>,
160}
161
162impl TaskTrackingRepository {
163 pub fn new(pool: Pool<MySql>) -> Self {
165 Self { pool }
166 }
167
168 #[tracing::instrument(skip_all, level = "debug")]
170 pub async fn list_member_task_counts(&self) -> crate::error::ApiResult<Vec<MemberTaskCountRow>> {
171 let rows = sqlx::query(
172 "SELECT member.Id AS userId, member.username AS userName, \
173 member.lastSubmitTime AS lastUpdateTime, member.intern AS intern, member.email AS email, \
174 COALESCE(sc.stationedCount, 0) AS stationedCount, \
175 COALESCE(tc.translatorCount, 0) AS translatorCount, \
176 COALESCE(pc.proofreaderCount, 0) AS proofreaderCount, \
177 COALESCE(lc.lettererCount, 0) AS lettererCount, \
178 COALESCE(tmc.timerCount, 0) AS timerCount, \
179 COALESCE(rc.reviewerCount, 0) AS reviewerCount \
180 FROM membertb member \
181 LEFT JOIN ( \
182 SELECT ms.memberId, COUNT(*) AS stationedCount \
183 FROM mangastationed ms WHERE ms.status = 1 GROUP BY ms.memberId \
184 ) sc ON sc.memberId = member.Id \
185 LEFT JOIN ( \
186 SELECT me.translatorId AS memberId, COUNT(*) AS translatorCount \
187 FROM mangaepisodetb me INNER JOIN mangaepisodedetail med ON med.episodeId = me.Id \
188 WHERE med.translatorUpdateTime IS NULL AND me.publishLink IS NULL \
189 GROUP BY me.translatorId \
190 ) tc ON tc.memberId = member.Id \
191 LEFT JOIN ( \
192 SELECT me.proofreaderId AS memberId, COUNT(*) AS proofreaderCount \
193 FROM mangaepisodetb me INNER JOIN mangaepisodedetail med ON med.episodeId = me.Id \
194 WHERE med.translatorUpdateTime IS NOT NULL AND med.proofreaderUpdateTime IS NULL \
195 AND me.publishLink IS NULL GROUP BY me.proofreaderId \
196 ) pc ON pc.memberId = member.Id \
197 LEFT JOIN ( \
198 SELECT me.lettererId AS memberId, COUNT(*) AS lettererCount \
199 FROM mangaepisodetb me INNER JOIN mangaepisodedetail med ON med.episodeId = me.Id \
200 WHERE med.proofreaderUpdateTime IS NOT NULL AND med.lettererUpdateTime IS NULL \
201 AND me.publishLink IS NULL GROUP BY me.lettererId \
202 ) lc ON lc.memberId = member.Id \
203 LEFT JOIN ( \
204 SELECT me.timerId AS memberId, COUNT(*) AS timerCount \
205 FROM mangaepisodetb me INNER JOIN mangaepisodedetail med ON med.episodeId = me.Id \
206 WHERE med.proofreaderUpdateTime IS NOT NULL AND med.timerUpdateTime IS NULL \
207 AND me.publishLink IS NULL GROUP BY me.timerId \
208 ) tmc ON tmc.memberId = member.Id \
209 LEFT JOIN ( \
210 SELECT me.reviewerId AS memberId, COUNT(*) AS reviewerCount \
211 FROM mangaepisodetb me INNER JOIN mangaepisodedetail med ON med.episodeId = me.Id \
212 WHERE med.lettererUpdateTime IS NOT NULL AND med.reviewerUpdateTime IS NULL \
213 AND me.publishLink IS NULL GROUP BY me.reviewerId \
214 ) rc ON rc.memberId = member.Id \
215 WHERE (COALESCE(tc.translatorCount, 0) + COALESCE(lc.lettererCount, 0) \
216 + COALESCE(tmc.timerCount, 0) + COALESCE(rc.reviewerCount, 0)) > 0 \
217 ORDER BY member.Id DESC",
218 )
219 .fetch_all(&self.pool)
220 .await?;
221 Ok(rows.into_iter().map(|r| row_to_member_task_count(&r)).collect())
222 }
223
224 pub fn to_member_task_counts(rows: &[MemberTaskCountRow]) -> Vec<MemberTaskCount> {
226 rows.iter()
227 .map(|r| MemberTaskCount {
228 user_name: r.user_name.clone(),
229 user_id: Some(r.user_id.to_string()),
230 intern: r.intern,
231 email: r.email.clone(),
232 last_update_time: r.last_update_time.clone(),
233 stationed_count: r.stationed_count as i32,
234 translator_count: r.translator_count as i32,
235 proofreader_count: r.proofreader_count as i32,
236 letterer_count: r.letterer_count as i32,
237 timer_count: r.timer_count as i32,
238 reviewer_count: r.reviewer_count as i32,
239 })
240 .collect()
241 }
242
243 #[tracing::instrument(skip_all, level = "debug")]
245 pub async fn list_unpublished_episodes(&self) -> crate::error::ApiResult<Vec<UnpublishedEpisodeRow>> {
246 let rows = sqlx::query(
247 "SELECT me.Id AS episodeId, me.mangaId, me.mangaEpisode, me.mangaEpisodeName, me.translatorId, me.proofreaderId, \
248 me.lettererId, me.timerId, me.reviewerId, me.updateTime AS episodeUpdateTime, \
249 m.mangaTranName, m.category, \
250 med.translatorSetupTime, med.translatorUpdateTime, \
251 med.proofreaderSetupTime, med.proofreaderUpdateTime, \
252 med.lettererSetupTime, med.lettererUpdateTime, \
253 med.timerSetupTime, med.timerUpdateTime, \
254 med.reviewerSetupTime, med.reviewerUpdateTime \
255 FROM mangaepisodetb me \
256 LEFT JOIN mangaepisodedetail med ON med.episodeId = me.Id \
257 LEFT JOIN mangatb m ON m.Id = me.mangaId \
258 WHERE (me.publishLink IS NULL OR me.publishLink = '')",
259 )
260 .fetch_all(&self.pool)
261 .await?;
262 Ok(rows.into_iter().map(|r| row_to_unpublished(&r)).collect())
263 }
264
265 pub fn build_task_response(episodes: &[UnpublishedEpisodeRow]) -> TaskTrackingResponse {
267 let mut resp = TaskTrackingResponse::empty();
268 for ep in episodes {
269 if ep.reviewer_update_time.is_some() {
270 push_task(
271 &mut resp,
272 ep,
273 "PUBLISHER",
274 None,
275 ep.reviewer_setup_time,
276 ep.reviewer_update_time,
277 |r, t| r.publish_task_list.push(t),
278 );
279 continue;
280 }
281 if ep.letterer_update_time.is_some() || ep.timer_update_time.is_some() {
282 let prev = ep
283 .letterer_update_time
284 .or(ep.timer_update_time);
285 push_task(
286 &mut resp,
287 ep,
288 "REVIEWER",
289 ep.reviewer_id,
290 ep.reviewer_setup_time,
291 prev,
292 |r, t| r.reviewer_task_list.push(t),
293 );
294 continue;
295 }
296 if ep.proofreader_update_time.is_some() {
297 let is_animation = ep.category == Some(4);
298 if is_animation {
299 push_task(
300 &mut resp,
301 ep,
302 "TIMER",
303 ep.timer_id,
304 ep.timer_setup_time,
305 ep.proofreader_update_time,
306 |r, t| r.timer_task_list.push(t),
307 );
308 } else {
309 push_task(
310 &mut resp,
311 ep,
312 "LETTERER",
313 ep.letterer_id,
314 ep.letterer_setup_time,
315 ep.proofreader_update_time,
316 |r, t| r.letterer_task_list.push(t),
317 );
318 }
319 continue;
320 }
321 if ep.translator_update_time.is_some() {
322 push_task(
323 &mut resp,
324 ep,
325 "PROOFREADER",
326 ep.proofreader_id,
327 ep.proofreader_setup_time,
328 ep.translator_update_time,
329 |r, t| r.proofreader_task_list.push(t),
330 );
331 continue;
332 }
333 push_task(
334 &mut resp,
335 ep,
336 "TRANSLATOR",
337 ep.translator_id,
338 ep.translator_setup_time,
339 ep.episode_update_time,
340 |r, t| r.translator_task_list.push(t),
341 );
342 }
343 resp
344 }
345
346 #[tracing::instrument(skip_all, level = "debug")]
348 pub async fn count_pending_publish_episodes(
349 &self,
350 manga_tran_name: Option<&str>,
351 ) -> crate::error::ApiResult<i64> {
352 let (from_where, _) = pending_publish_from_where(manga_tran_name.is_some());
353 let sql = format!("SELECT COUNT(*) AS cnt {from_where}");
354 let mut q = sqlx::query(&sql);
355 if let Some(name) = manga_tran_name {
356 q = q.bind(name);
357 }
358 Ok(q.fetch_one(&self.pool).await?.get("cnt"))
359 }
360
361 #[tracing::instrument(skip_all, level = "debug")]
363 pub async fn list_pending_publish_episodes(
364 &self,
365 manga_tran_name: Option<&str>,
366 page: i32,
367 page_size: i32,
368 ) -> crate::error::ApiResult<Vec<PendingEpisodeRow>> {
369 let (from_where, select_cols) = pending_publish_from_where(manga_tran_name.is_some());
370 let page = page.max(1);
371 let page_size = page_size.max(1);
372 let offset = (page - 1) * page_size;
373 let sql = format!(
374 "SELECT {select_cols} {from_where} \
375 ORDER BY m.mangaTranName ASC, me.mangaId ASC, \
376 CAST(SUBSTRING_INDEX(me.mangaEpisode, '+', 1) AS DECIMAL(10,2)) ASC \
377 LIMIT ? OFFSET ?"
378 );
379 let mut q = sqlx::query(&sql);
380 if let Some(name) = manga_tran_name {
381 q = q.bind(name);
382 }
383 q = q.bind(page_size).bind(offset);
384 let rows = q.fetch_all(&self.pool).await?;
385 Ok(rows.into_iter().map(|r| row_to_pending_episode(&r)).collect())
386 }
387
388 #[tracing::instrument(skip_all, level = "debug")]
390 pub async fn map_publish_episode_context(
391 &self,
392 manga_ids: &[i32],
393 ) -> crate::error::ApiResult<(HashMap<i32, String>, HashMap<i32, i32>)> {
394 if manga_ids.is_empty() {
395 return Ok((HashMap::new(), HashMap::new()));
396 }
397 let placeholders = manga_ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
398 let sql = format!(
399 "SELECT me.mangaId, me.Id AS episodeId, me.mangaEpisode, me.publishLink \
400 FROM mangaepisodetb me \
401 WHERE me.mangaId IN ({placeholders})"
402 );
403 let mut q = sqlx::query(&sql);
404 for id in manga_ids {
405 q = q.bind(id);
406 }
407 let rows = q.fetch_all(&self.pool).await?;
408
409 let mut max_published_num: HashMap<i32, f64> = HashMap::new();
410 let mut latest_label: HashMap<i32, String> = HashMap::new();
411 let mut unpublished: Vec<(i32, i32, f64)> = Vec::new();
412
413 for r in &rows {
414 let manga_id: i32 = r.get("mangaId");
415 let episode_id: i32 = r.get("episodeId");
416 let episode_label: String = r.try_get("mangaEpisode").unwrap_or_default();
417 let publish_link: Option<String> = r.try_get("publishLink").ok();
418 let ep_num = episode_sort_key(&episode_label);
419 let published = publish_link.as_deref().is_some_and(|s| !s.is_empty());
420
421 if published {
422 let replace = max_published_num
423 .get(&manga_id)
424 .is_none_or(|&n| ep_num > n);
425 if replace {
426 max_published_num.insert(manga_id, ep_num);
427 latest_label.insert(manga_id, episode_label);
428 }
429 } else {
430 unpublished.push((manga_id, episode_id, ep_num));
431 }
432 }
433
434 let mut next_min: HashMap<i32, (f64, i32)> = HashMap::new();
435 for (manga_id, episode_id, ep_num) in unpublished {
436 let threshold = max_published_num.get(&manga_id).copied().unwrap_or(-1.0);
437 if ep_num <= threshold {
438 continue;
439 }
440 match next_min.get(&manga_id) {
441 None => {
442 next_min.insert(manga_id, (ep_num, episode_id));
443 }
444 Some((min_num, _)) if ep_num < *min_num => {
445 next_min.insert(manga_id, (ep_num, episode_id));
446 }
447 _ => {}
448 }
449 }
450 let next_publish = next_min
451 .into_iter()
452 .map(|(manga_id, (_, episode_id))| (manga_id, episode_id))
453 .collect();
454
455 Ok((latest_label, next_publish))
456 }
457
458 pub fn row_mangatb(row: &PendingEpisodeRow) -> Mangatb {
460 Mangatb {
461 id: Some(row.manga_id),
462 manga_tran_name: row.manga_tran_name.clone(),
463 manga_ori_name: row.manga_ori_name.clone(),
464 image: row.image.clone(),
465 category: row.category.map(|v| v as i32),
466 manga_status: row.manga_status.map(|v| v as i32),
467 setup_time: row.manga_setup_time,
468 update_time: row.manga_update_time,
469 link: row.link.clone(),
470 introduction: row.introduction.clone(),
471 }
472 }
473}
474
475fn pending_publish_from_where(name_filter: bool) -> (String, String) {
477 let select_cols = "me.Id AS episodeId, me.mangaId, me.mangaEpisode, me.mangaEpisodeName, \
478 me.providerId, me.translatorId, me.proofreaderId, me.lettererId, me.timerId, me.reviewerId, \
479 me.setupTime, me.updateTime, me.translatorFile, me.proofreaderFile, me.timerFile, me.publishLink, \
480 me.provider_file_oss_id AS providerFileOssId, me.translator_file_oss_id AS translatorFileOssId, \
481 me.proofreader_file_oss_id AS proofreaderFileOssId, me.letterer_file_oss_id AS lettererFileOssId, \
482 me.timer_file_oss_id AS timerFileOssId, \
483 med.reviewerUpdateTime, \
484 m.Id AS mangaDbId, m.mangaTranName, m.mangaOriName, m.category, m.mangaStatus, m.image, \
485 m.setupTime AS mangaSetupTime, m.updateTime AS mangaUpdateTime, m.link, m.introduction"
486 .to_string();
487 let mut from_where = String::from(
488 "FROM mangaepisodetb me \
489 LEFT JOIN mangaepisodedetail med ON med.episodeId = me.Id \
490 INNER JOIN mangatb m ON m.Id = me.mangaId \
491 WHERE me.letterer_file_oss_id IS NOT NULL \
492 AND (me.publishLink IS NULL OR me.publishLink = '')",
493 );
494 if name_filter {
495 from_where.push_str(" AND m.mangaTranName LIKE CONCAT('%', ?, '%')");
496 }
497 (from_where, select_cols)
498}
499
500fn episode_sort_key(episode: &str) -> f64 {
502 episode
503 .split('+')
504 .next()
505 .unwrap_or(episode)
506 .parse()
507 .unwrap_or(0.0)
508}
509
510fn row_to_member_task_count(row: &sqlx::mysql::MySqlRow) -> MemberTaskCountRow {
512 MemberTaskCountRow {
513 user_id: row.get("userId"),
514 user_name: row.try_get("userName").ok(),
515 intern: row.try_get("intern").ok(),
516 email: row.try_get("email").ok(),
517 last_update_time: row
518 .try_get::<DateTime<Utc>, _>("lastUpdateTime")
519 .ok()
520 .map(|t| t.to_rfc3339()),
521 translator_count: row.try_get("translatorCount").unwrap_or(0),
522 proofreader_count: row.try_get("proofreaderCount").unwrap_or(0),
523 letterer_count: row.try_get("lettererCount").unwrap_or(0),
524 timer_count: row.try_get("timerCount").unwrap_or(0),
525 reviewer_count: row.try_get("reviewerCount").unwrap_or(0),
526 stationed_count: row.try_get("stationedCount").unwrap_or(0),
527 }
528}
529
530fn row_to_unpublished(row: &sqlx::mysql::MySqlRow) -> UnpublishedEpisodeRow {
532 UnpublishedEpisodeRow {
533 episode_id: row.get("episodeId"),
534 manga_id: row.get("mangaId"),
535 manga_episode: row.try_get("mangaEpisode").ok(),
536 manga_episode_name: row.try_get("mangaEpisodeName").ok(),
537 manga_tran_name: row.try_get("mangaTranName").ok(),
538 category: row.try_get("category").ok(),
539 translator_id: row.try_get("translatorId").ok(),
540 proofreader_id: row.try_get("proofreaderId").ok(),
541 letterer_id: row.try_get("lettererId").ok(),
542 timer_id: row.try_get("timerId").ok(),
543 reviewer_id: row.try_get("reviewerId").ok(),
544 translator_setup_time: row.try_get("translatorSetupTime").ok(),
545 translator_update_time: row.try_get("translatorUpdateTime").ok(),
546 proofreader_setup_time: row.try_get("proofreaderSetupTime").ok(),
547 proofreader_update_time: row.try_get("proofreaderUpdateTime").ok(),
548 letterer_setup_time: row.try_get("lettererSetupTime").ok(),
549 letterer_update_time: row.try_get("lettererUpdateTime").ok(),
550 timer_setup_time: row.try_get("timerSetupTime").ok(),
551 timer_update_time: row.try_get("timerUpdateTime").ok(),
552 reviewer_setup_time: row.try_get("reviewerSetupTime").ok(),
553 reviewer_update_time: row.try_get("reviewerUpdateTime").ok(),
554 episode_update_time: row.try_get("episodeUpdateTime").ok(),
555 }
556}
557
558fn row_to_pending_episode(row: &sqlx::mysql::MySqlRow) -> PendingEpisodeRow {
560 PendingEpisodeRow {
561 episode_id: row.get("episodeId"),
562 manga_id: row.get("mangaId"),
563 manga_episode: row.try_get("mangaEpisode").ok(),
564 manga_episode_name: row.try_get("mangaEpisodeName").ok(),
565 provider_id: row.try_get("providerId").ok(),
566 translator_id: row.try_get("translatorId").ok(),
567 proofreader_id: row.try_get("proofreaderId").ok(),
568 letterer_id: row.try_get("lettererId").ok(),
569 timer_id: row.try_get("timerId").ok(),
570 reviewer_id: row.try_get("reviewerId").ok(),
571 setup_time: row.try_get("setupTime").ok(),
572 update_time: row.try_get("updateTime").ok(),
573 translator_file: row.try_get("translatorFile").ok(),
574 proofreader_file: row.try_get("proofreaderFile").ok(),
575 timer_file: row.try_get("timerFile").ok(),
576 publish_link: row.try_get("publishLink").ok(),
577 provider_file_oss_id: row.try_get("providerFileOssId").ok(),
578 translator_file_oss_id: row.try_get("translatorFileOssId").ok(),
579 proofreader_file_oss_id: row.try_get("proofreaderFileOssId").ok(),
580 letterer_file_oss_id: row.try_get("lettererFileOssId").ok(),
581 timer_file_oss_id: row.try_get("timerFileOssId").ok(),
582 reviewer_update_time: row.try_get("reviewerUpdateTime").ok(),
583 manga_tran_name: row.try_get("mangaTranName").ok(),
584 manga_ori_name: row.try_get("mangaOriName").ok(),
585 category: row.try_get("category").ok(),
586 manga_status: row.try_get("mangaStatus").ok(),
587 image: row.try_get("image").ok(),
588 manga_setup_time: row.try_get("mangaSetupTime").ok(),
589 manga_update_time: row.try_get("mangaUpdateTime").ok(),
590 link: row.try_get("link").ok(),
591 introduction: row.try_get("introduction").ok(),
592 }
593}
594
595fn make_episode_task(
596 ep: &UnpublishedEpisodeRow,
597 post: &str,
598 member_id: Option<i32>,
599 setup_time: Option<DateTime<Utc>>,
600 previous_update_time: Option<DateTime<Utc>>,
601) -> EpisodeTask {
602 EpisodeTask {
603 manga_id: Some(ep.manga_id),
604 episode_id: Some(ep.episode_id),
605 manga_episode: ep.manga_episode.clone(),
606 episode_name: ep.manga_episode_name.clone(),
607 member_id,
608 setup_time,
609 previous_update_time,
610 post: Some(post.to_string()),
611 }
612}
613
614fn push_task<F>(
615 resp: &mut TaskTrackingResponse,
616 ep: &UnpublishedEpisodeRow,
617 post: &str,
618 member_id: Option<i32>,
619 setup_time: Option<DateTime<Utc>>,
620 previous_update_time: Option<DateTime<Utc>>,
621 push_list: F,
622) where
623 F: FnOnce(&mut TaskTrackingResponse, EpisodeTask),
624{
625 let task = make_episode_task(ep, post, member_id, setup_time, previous_update_time);
626 resp.all_task_list.push(task.clone());
627 push_list(resp, task);
628}