1use crate::{
11 app::AppState,
12 cache::{
13 collect_list_cache_key, get_collect_list_json_cached, get_list_page_json_cached,
14 get_manga_ori_names_cached, get_manga_tran_names_cached, invalidate_collect_list,
15 invalidate_stationed_mangas, manga_page_cache_key, on_manga_mutated, on_station_mutated,
16 set_collect_list_json_cached, set_list_page_json_cached, set_manga_ori_names_cached,
17 set_manga_tran_names_cached,
18 },
19 common::PageBean,
20 entity::{
21 manga::{
22 AddStationRequest, CollectedMembersVo, GlossaryRequest, GlossaryVo, Manga,
23 MangaCollect, MangaDetailVo, MangaListVo, MangaSimpleVo, MangaUpdateRequest,
24 },
25 member::Member,
26 },
27 error::{ApiResult, AppError},
28 repository::{
29 author_repo::AuthorRepository, manga_list_assemble::MangaListSort,
30 manga_repo::MangaRepository, member_repo::MemberRepository,
31 },
32 service::rss_service::{RssRefreshScope, RssService},
33 utils::page::{paginate, slice_rows},
34};
35use axum::body::Bytes;
36use sea_orm::TransactionTrait;
37use serde::Deserialize;
38use std::path::PathBuf;
39use std::sync::Arc;
40
41#[derive(Debug, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct StationMemberBody {
45 #[serde(default, alias = "Id")]
47 pub id: i32,
48 pub station_id: Option<i32>,
50 pub status: Option<i16>,
52 pub manga_id: Option<i32>,
54 pub post: Option<i32>,
56}
57
58pub struct MangaService;
62
63impl MangaService {
64 #[tracing::instrument(skip_all, level = "debug")]
72 pub async fn page(
73 state: &AppState,
74 page: i32,
75 page_size: i32,
76 manga_tran_name: Option<String>,
77 manga_ori_name: Option<String>,
78 category: Option<i16>,
79 manga_status: Option<i16>,
80 _author_id: Option<i16>,
81 author_name: Option<String>,
82 magazine_name: Option<String>,
83 sort_prop: Option<String>,
84 sort_order: Option<String>,
85 ) -> ApiResult<PageBean<MangaListVo>> {
86 let page = page.max(1);
87 let page_size = page_size.max(1);
88 let offset = (page - 1) * page_size;
89 let pool_count = state.db.clone();
90 let pool_page = state.db.clone();
91 let repo_count = MangaRepository::new(pool_count);
92 let repo_page = MangaRepository::new(pool_page);
93 let tran = manga_tran_name.as_deref();
94 let ori = manga_ori_name.as_deref();
95 let author = author_name.as_deref();
96 let magazine = magazine_name.as_deref();
97 let sort = MangaListSort::from_query(sort_prop.as_deref(), sort_order.as_deref());
98 let (total, rows) = tokio::try_join!(
99 repo_count.count_list(tran, ori, category, manga_status, author, magazine),
100 repo_page.list_page_sorted(
101 tran,
102 ori,
103 category,
104 manga_status,
105 author,
106 magazine,
107 sort,
108 page_size,
109 offset,
110 ),
111 )?;
112 Ok(slice_rows(rows, total))
113 }
114
115 #[tracing::instrument(skip_all, level = "debug")]
117 pub async fn page_json(
118 state: &AppState,
119 page: i32,
120 page_size: i32,
121 manga_tran_name: Option<String>,
122 manga_ori_name: Option<String>,
123 category: Option<i16>,
124 manga_status: Option<i16>,
125 author_id: Option<i16>,
126 author_name: Option<String>,
127 magazine_name: Option<String>,
128 sort_prop: Option<String>,
129 sort_order: Option<String>,
130 ) -> ApiResult<Arc<Vec<u8>>> {
131 let sort = MangaListSort::from_query(sort_prop.as_deref(), sort_order.as_deref());
132 let key = manga_page_cache_key(
133 state,
134 page,
135 page_size,
136 manga_tran_name.as_deref(),
137 manga_ori_name.as_deref(),
138 category,
139 manga_status,
140 author_name.as_deref(),
141 magazine_name.as_deref(),
142 sort.cache_field(),
143 sort.cache_order(),
144 );
145 if let Some(json) = get_list_page_json_cached(state, &key).await {
146 return Ok(json);
147 }
148 let page_data = Self::page(
149 state,
150 page,
151 page_size,
152 manga_tran_name,
153 manga_ori_name,
154 category,
155 manga_status,
156 author_id,
157 author_name,
158 magazine_name,
159 sort_prop,
160 sort_order,
161 )
162 .await?;
163 set_list_page_json_cached(state, key.clone(), page_data).await;
164 get_list_page_json_cached(state, &key)
165 .await
166 .ok_or_else(|| AppError::business("漫画列表缓存写入失败"))
167 }
168
169 #[tracing::instrument(skip_all, level = "debug")]
171 pub async fn get_manga_tran_name(state: &AppState) -> ApiResult<Vec<MangaSimpleVo>> {
172 if let Some(cached) = get_manga_tran_names_cached(state).await {
173 return Ok(cached);
174 }
175 let data = MangaRepository::new(state.db.clone())
176 .get_manga_tran_names()
177 .await?;
178 set_manga_tran_names_cached(state, &data).await;
179 Ok(data)
180 }
181
182 #[tracing::instrument(skip_all, level = "debug")]
184 pub async fn get_manga_ori_name(state: &AppState) -> ApiResult<Vec<MangaSimpleVo>> {
185 if let Some(cached) = get_manga_ori_names_cached(state).await {
186 return Ok(cached);
187 }
188 let data = MangaRepository::new(state.db.clone())
189 .list_ori_names()
190 .await?;
191 set_manga_ori_names_cached(state, &data).await;
192 Ok(data)
193 }
194
195 #[tracing::instrument(skip_all, level = "debug")]
197 pub async fn delete_manga(state: &AppState, id: i32) -> ApiResult<()> {
198 let repo = MangaRepository::new(state.db.clone());
199 repo.delete_manga_author(id).await?;
200 repo.delete_manga_author2(id).await?;
201 repo.delete_manga_magazine(id).await?;
202 repo.delete_manga_episode(id).await?;
203 repo.delete_by_id(id).await?;
204 on_manga_mutated(state, Some(id)).await;
205 Ok(())
206 }
207
208 #[tracing::instrument(skip_all, level = "debug")]
210 pub async fn add_new_manga(
211 state: &AppState,
212 req: MangaUpdateRequest,
213 image: Option<Bytes>,
214 member_id: i32,
215 ) -> ApiResult<()> {
216 let repo = MangaRepository::new(state.db.clone());
217 let author_repo = AuthorRepository::new(state.db.clone());
218 validate_manga_name_unique(&repo, None, &req).await?;
219
220 let mut manga = request_to_manga(&req);
221 apply_manga_fields(&mut manga, &req);
222 if let Some(img) = resolve_image_path(state, &req, image, None, member_id).await? {
223 manga.img_url = Some(img);
224 }
225
226 let author1_id = resolve_author_for_add(&author_repo, req.author_name.as_deref()).await?;
227 let author2_id = resolve_author2_for_add(&author_repo, req.author_name2.as_deref()).await?;
228
229 let manga_id = repo.insert(&manga).await?;
230 repo.insert_manga_author(manga_id, author1_id).await?;
231 if let Some(aid2) = author2_id {
232 repo.insert_manga_author2(manga_id, aid2).await?;
233 }
234 if let Some(mid) = normalize_magazine_id(req.magazine_id) {
235 repo.insert_manga_magazine(manga_id, mid).await?;
236 }
237 on_manga_mutated(state, None).await;
238 RssService::refresh(state, RssRefreshScope::NewManga);
239 Ok(())
240 }
241
242 #[tracing::instrument(skip_all, level = "debug")]
244 pub async fn get_manga_by_id(state: &AppState, id: i32) -> ApiResult<MangaDetailVo> {
245 let repo = MangaRepository::new(state.db.clone());
246 repo.get_manga_detail_by_id(id)
247 .await?
248 .ok_or_else(|| AppError::business("漫画不存在喵"))
249 }
250
251 #[tracing::instrument(skip_all, level = "debug")]
253 pub async fn update_manga(
254 state: &AppState,
255 req: MangaUpdateRequest,
256 image: Option<Bytes>,
257 member_id: i32,
258 ) -> ApiResult<()> {
259 let repo = MangaRepository::new(state.db.clone());
260 let author_repo = AuthorRepository::new(state.db.clone());
261 let id = req.id.ok_or_else(|| AppError::business("缺少漫画 ID"))?;
262 validate_manga_name_unique(&repo, Some(id), &req).await?;
263
264 let old = repo
265 .get_manga_by_id(id)
266 .await?
267 .ok_or_else(|| AppError::business("漫画不存在喵"))?;
268
269 let mut manga = old;
270 apply_manga_fields(&mut manga, &req);
271 if let Some(img) =
272 resolve_image_path(state, &req, image, manga.img_url.as_deref(), member_id).await?
273 {
274 manga.img_url = Some(img);
275 }
276
277 let author1_id = resolve_author_for_update(
278 &author_repo,
279 req.author_name.as_deref(),
280 "原作作者不在档案里喵!先添加作者吧",
281 )
282 .await?;
283 let author2_id = resolve_author_for_update(
284 &author_repo,
285 req.author_name2.as_deref(),
286 "作画作者不在档案里喵!先添加作者吧",
287 )
288 .await?;
289
290 repo.update_manga(&manga).await?;
291 if let Some(aid) = author1_id {
292 if repo.test_manga_author1(id).await? {
293 repo.update_manga_author(id, aid).await?;
294 } else {
295 repo.insert_manga_author(id, aid).await?;
296 }
297 }
298 sync_manga_author2(&repo, id, req.author_name2.as_deref(), author2_id).await?;
299 sync_manga_magazine(&repo, id, normalize_magazine_id(req.magazine_id)).await?;
300 on_manga_mutated(state, Some(id)).await;
301 RssService::refresh(state, RssRefreshScope::MangaUpdated);
302 Ok(())
303 }
304
305 #[tracing::instrument(skip_all, level = "debug")]
307 pub async fn get_collect_detail(
308 state: &AppState,
309 collect: MangaCollect,
310 ) -> ApiResult<MangaCollect> {
311 let detail = MangaRepository::new(state.db.clone())
312 .get_collect_detail(collect.manga_id, collect.member_id)
313 .await?;
314 Ok(detail.unwrap_or(collect))
315 }
316
317 #[tracing::instrument(skip_all, level = "debug")]
319 pub async fn get_collect_list(
320 state: &AppState,
321 page: i32,
322 page_size: i32,
323 member_id: i32,
324 ) -> ApiResult<PageBean<MangaListVo>> {
325 let (total, rows) = MangaRepository::new(state.db.clone())
326 .get_collect_list_page(member_id, page, page_size)
327 .await?;
328 Ok(slice_rows(rows, total))
329 }
330
331 #[tracing::instrument(skip_all, level = "debug")]
333 pub async fn get_collect_list_json(
334 state: &AppState,
335 page: i32,
336 page_size: i32,
337 member_id: i32,
338 ) -> ApiResult<Arc<Vec<u8>>> {
339 let key = collect_list_cache_key(member_id, page, page_size);
340 if let Some(json) = get_collect_list_json_cached(state, &key).await {
341 return Ok(json);
342 }
343 let page_data = Self::get_collect_list(state, page, page_size, member_id).await?;
344 set_collect_list_json_cached(state, key.clone(), page_data).await;
345 get_collect_list_json_cached(state, &key)
346 .await
347 .ok_or_else(|| AppError::business("collectList 缓存写入失败"))
348 }
349
350 #[tracing::instrument(skip_all, level = "debug")]
352 pub async fn get_collected_members(
353 state: &AppState,
354 page: i32,
355 page_size: i32,
356 manga_id: i32,
357 ) -> ApiResult<PageBean<CollectedMembersVo>> {
358 let manga_repo = MangaRepository::new(state.db.clone());
359 let member_repo = MemberRepository::new(state.db.clone());
360 let mut all = manga_repo.get_collected_members(manga_id).await?;
361 let ids: Vec<i32> = all.iter().map(|m| m.id).collect();
362 let posts_map = member_repo.get_posts_map(&ids).await?;
363 for m in &mut all {
364 let post_ids = posts_map.get(&m.id).cloned().unwrap_or_default();
365 m.posts = post_ids
366 .iter()
367 .map(|p| crate::entity::member::Post { post: *p })
368 .collect();
369 }
370 Ok(paginate(all, page, page_size))
371 }
372
373 #[tracing::instrument(skip_all, level = "debug")]
375 pub async fn del_collect(state: &AppState, collect: MangaCollect) -> ApiResult<()> {
376 MangaRepository::new(state.db.clone())
377 .del_collect(collect.manga_id, collect.member_id)
378 .await?;
379 invalidate_collect_list(state, collect.member_id).await;
380 Ok(())
381 }
382
383 #[tracing::instrument(skip_all, level = "debug")]
385 pub async fn add_collect(state: &AppState, collect: MangaCollect) -> ApiResult<()> {
386 MangaRepository::new(state.db.clone())
387 .add_collect(collect.manga_id, collect.member_id)
388 .await?;
389 invalidate_collect_list(state, collect.member_id).await;
390 Ok(())
391 }
392
393 #[tracing::instrument(skip_all, level = "debug")]
395 pub async fn page_glossary(
396 state: &AppState,
397 page: i32,
398 page_size: i32,
399 r#type: Option<i16>,
400 manga_id: i16,
401 ) -> ApiResult<PageBean<GlossaryVo>> {
402 let repo = MangaRepository::new(state.db.clone());
403 let all = repo.list_glossary(manga_id as i32, r#type).await?;
404 Ok(paginate(all, page, page_size))
405 }
406
407 #[tracing::instrument(skip_all, level = "debug")]
409 pub async fn delete_glossary(state: &AppState, id: i32) -> ApiResult<()> {
410 MangaRepository::new(state.db.clone())
411 .delete_glossary_by_id(id)
412 .await
413 }
414
415 #[tracing::instrument(skip_all, level = "debug")]
417 pub async fn add_glossary(
418 state: &AppState,
419 req: GlossaryRequest,
420 member_id: i32,
421 ) -> ApiResult<()> {
422 MangaRepository::new(state.db.clone())
423 .insert_glossary(&req, member_id)
424 .await?;
425 Ok(())
426 }
427
428 #[tracing::instrument(skip_all, level = "debug")]
430 pub async fn get_glossary_by_id(state: &AppState, id: i32) -> ApiResult<GlossaryVo> {
431 MangaRepository::new(state.db.clone())
432 .get_glossary_by_id(id)
433 .await?
434 .ok_or_else(|| AppError::business("术语不存在喵"))
435 }
436
437 #[tracing::instrument(skip_all, level = "debug")]
439 pub async fn update_glossary(
440 state: &AppState,
441 req: GlossaryRequest,
442 member_id: i32,
443 ) -> ApiResult<()> {
444 MangaRepository::new(state.db.clone())
445 .update_glossary(&req, member_id)
446 .await
447 }
448
449 #[tracing::instrument(skip_all, level = "debug")]
451 pub async fn get_manga_rss(
452 state: &AppState,
453 ) -> ApiResult<Vec<crate::entity::rss::RssMangaRow>> {
454 MangaRepository::new(state.db.clone()).get_manga_rss().await
455 }
456
457 #[tracing::instrument(skip_all, level = "debug")]
459 pub async fn get_episode_rss(
460 state: &AppState,
461 ) -> ApiResult<Vec<crate::entity::rss::EpisodeRssRow>> {
462 MangaRepository::new(state.db.clone())
463 .get_episode_rss()
464 .await
465 }
466
467 #[tracing::instrument(skip_all, level = "debug")]
469 pub async fn rss_output(state: &AppState, xml: String) -> ApiResult<()> {
470 RssService::refresh(state, RssRefreshScope::ExternalOutput { xml });
471 Ok(())
472 }
473
474 #[tracing::instrument(skip_all, level = "debug")]
476 pub async fn get_stationed_members(
477 state: &AppState,
478 page: i32,
479 page_size: i32,
480 manga_id: i32,
481 ) -> ApiResult<PageBean<Member>> {
482 let manga_repo = MangaRepository::new(state.db.clone());
483 let member_repo = MemberRepository::new(state.db.clone());
484 let mut all = manga_repo.get_stationed_members(manga_id).await?;
485 let ids: Vec<i32> = all.iter().map(|m| m.id).collect();
486 let posts_map = member_repo.get_posts_map(&ids).await?;
487 for m in &mut all {
488 let post_ids = posts_map.get(&m.id).cloned().unwrap_or_default();
489 m.post_ids = post_ids.clone();
490 m.posts = post_ids
491 .iter()
492 .map(|p| crate::entity::member::Post { post: *p })
493 .collect();
494 }
495 Ok(paginate(all, page, page_size))
496 }
497
498 #[tracing::instrument(skip_all, level = "debug")]
500 pub async fn del_station(state: &AppState, station_id: i32) -> ApiResult<()> {
501 let db = crate::db::from_sqlx_pool(state.db.clone());
502 let txn = db.begin().await?;
503 let station = MangaRepository::get_station_by_id_with(&txn, station_id)
504 .await?
505 .ok_or_else(|| AppError::business("常驻记录不存在喵"))?;
506 MangaRepository::clear_member_unsubmitted_episodes_with(
507 &txn,
508 station.manga_id,
509 station.member_id,
510 station.post,
511 )
512 .await?;
513 MangaRepository::del_station_with(&txn, station_id).await?;
514 txn.commit().await?;
515 on_station_mutated(state, station.manga_id, station.member_id).await;
516 Ok(())
517 }
518
519 #[tracing::instrument(skip_all, level = "debug")]
521 pub async fn add_station(state: &AppState, body: StationMemberBody) -> ApiResult<()> {
522 let manga_id = body
523 .manga_id
524 .ok_or_else(|| AppError::business("缺少漫画 ID"))?;
525 let post = body.post.unwrap_or(5);
526 MangaRepository::new(state.db.clone())
527 .add_station(manga_id, body.id, post)
528 .await?;
529 invalidate_stationed_mangas(state, body.id).await;
530 Ok(())
531 }
532
533 #[tracing::instrument(skip_all, level = "debug")]
535 pub async fn add_station_by_admin(state: &AppState, req: AddStationRequest) -> ApiResult<()> {
536 let db = crate::db::from_sqlx_pool(state.db.clone());
537 let txn = db.begin().await?;
538 MangaRepository::add_station_by_admin_with(&txn, &req).await?;
539 if req.fill_episodes == Some(true) {
540 MangaRepository::fill_empty_episodes_with(&txn, &req).await?;
541 MangaRepository::fill_episode_detail_with(&txn, &req).await?;
542 }
543 txn.commit().await?;
544 on_station_mutated(state, req.manga_id, req.member_id).await;
545 Ok(())
546 }
547
548 #[tracing::instrument(skip_all, level = "debug")]
550 pub async fn update_station(state: &AppState, body: StationMemberBody) -> ApiResult<()> {
551 let station_id = body
552 .station_id
553 .ok_or_else(|| AppError::business("缺少常驻记录 ID(stationId)"))?;
554 let repo = MangaRepository::new(state.db.clone());
555 let station = repo
556 .get_station_by_id(station_id)
557 .await?
558 .ok_or_else(|| AppError::business("常驻记录不存在喵"))?;
559 let status = body.status.unwrap_or(0);
560 let new_status = match status {
561 1 => 2,
562 0 => 1,
563 2 => 1,
564 other => other,
565 };
566 repo.update_station_status(station_id, new_status).await?;
567 invalidate_stationed_mangas(state, station.member_id).await;
568 Ok(())
569 }
570}
571
572fn request_to_manga(req: &MangaUpdateRequest) -> Manga {
573 Manga {
574 id: req.id,
575 manga_tran_name: req.manga_tran_name.clone(),
576 manga_ori_name: req.manga_ori_name.clone(),
577 category: req.category,
578 manga_status: req.manga_status,
579 img_url: req.image.clone(),
580 link: req.link.clone(),
581 introduction: req.introduction.clone(),
582 update_time: None,
583 }
584}
585
586fn apply_manga_fields(manga: &mut Manga, req: &MangaUpdateRequest) {
588 if let Some(v) = req.manga_tran_name.clone() {
589 manga.manga_tran_name = Some(v);
590 }
591 if let Some(v) = req.manga_ori_name.clone() {
592 manga.manga_ori_name = Some(v);
593 }
594 if let Some(v) = req.category {
595 manga.category = Some(v);
596 }
597 if let Some(v) = req.manga_status {
598 manga.manga_status = Some(v);
599 }
600 if let Some(v) = req.link.clone() {
601 manga.link = Some(v);
602 }
603 if let Some(v) = req.introduction.clone() {
604 manga.introduction = Some(v);
605 }
606}
607
608#[tracing::instrument(skip_all, level = "debug")]
610async fn validate_manga_name_unique(
611 repo: &MangaRepository,
612 exclude_id: Option<i32>,
613 req: &MangaUpdateRequest,
614) -> ApiResult<()> {
615 if let Some(name) = req.manga_ori_name.as_deref().filter(|s| !s.is_empty()) {
616 if repo.exists_ori_name_for_other(name, exclude_id).await? {
617 return Err(AppError::unique("漫画原名"));
618 }
619 }
620 if let Some(name) = req.manga_tran_name.as_deref().filter(|s| !s.is_empty()) {
621 if repo.exists_tran_name_for_other(name, exclude_id).await? {
622 return Err(AppError::unique("漫画译名"));
623 }
624 }
625 Ok(())
626}
627
628#[tracing::instrument(skip_all, level = "debug")]
630async fn resolve_image_path(
631 state: &AppState,
632 req: &MangaUpdateRequest,
633 image: Option<Bytes>,
634 _old_image: Option<&str>,
635 member_id: i32,
636) -> ApiResult<Option<String>> {
637 if let Some(bytes) = image {
638 return Ok(Some(save_image(state, &bytes, member_id).await?));
639 }
640 if let Some(ref path) = req.image {
641 if !path.trim().is_empty() {
642 return Ok(Some(path.clone()));
643 }
644 }
645 Ok(None)
646}
647
648#[tracing::instrument(skip_all, level = "debug")]
650async fn resolve_author_for_add(
651 author_repo: &AuthorRepository,
652 name: Option<&str>,
653) -> ApiResult<i32> {
654 let name = name
655 .filter(|s| !s.trim().is_empty())
656 .ok_or_else(|| AppError::business("还没输入作者呢喵"))?;
657 if let Some(author) = author_repo.test_author_name(name).await? {
658 return author.id.ok_or_else(|| AppError::business("作者 ID 无效"));
659 }
660 Ok(author_repo
661 .insert(&crate::entity::author::Author {
662 id: None,
663 author_name: Some(name.to_string()),
664 })
665 .await?)
666}
667
668#[tracing::instrument(skip_all, level = "debug")]
670async fn resolve_author2_for_add(
671 author_repo: &AuthorRepository,
672 name: Option<&str>,
673) -> ApiResult<Option<i32>> {
674 let Some(name) = name.filter(|s| !s.trim().is_empty()) else {
675 return Ok(None);
676 };
677 if let Some(author) = author_repo.test_author_name(name).await? {
678 return Ok(Some(
679 author
680 .id
681 .ok_or_else(|| AppError::business("作者 ID 无效"))?,
682 ));
683 }
684 Ok(Some(
685 author_repo
686 .insert(&crate::entity::author::Author {
687 id: None,
688 author_name: Some(name.to_string()),
689 })
690 .await?,
691 ))
692}
693
694#[tracing::instrument(skip_all, level = "debug")]
696async fn resolve_author_for_update(
697 author_repo: &AuthorRepository,
698 name: Option<&str>,
699 err_msg: &str,
700) -> ApiResult<Option<i32>> {
701 let Some(name) = name.filter(|s| !s.trim().is_empty()) else {
702 return Ok(None);
703 };
704 match author_repo.test_author_name(name).await? {
705 Some(a) => a.id.ok_or_else(|| AppError::business(err_msg)).map(Some),
706 None => Err(AppError::business(err_msg)),
707 }
708}
709
710#[tracing::instrument(skip_all, level = "debug")]
712async fn sync_manga_author2(
713 repo: &MangaRepository,
714 manga_id: i32,
715 author2_name: Option<&str>,
716 author2_id: Option<i32>,
717) -> ApiResult<()> {
718 let has_name = author2_name.is_some_and(|s| !s.trim().is_empty());
719 let has_row = repo.test_author2(manga_id).await?;
720 if has_name {
721 if let Some(aid) = author2_id {
722 if has_row {
723 repo.update_manga_author2(manga_id, aid).await?;
724 } else {
725 repo.insert_manga_author2(manga_id, aid).await?;
726 }
727 }
728 } else if has_row {
729 repo.delete_manga_author2(manga_id).await?;
730 }
731 Ok(())
732}
733
734#[tracing::instrument(skip_all, level = "debug")]
736async fn sync_manga_magazine(
737 repo: &MangaRepository,
738 manga_id: i32,
739 magazine_id: Option<i32>,
740) -> ApiResult<()> {
741 match magazine_id {
742 None => repo.delete_manga_magazine(manga_id).await?,
743 Some(mid) => {
744 if repo.test_manga_magazine(manga_id).await? {
745 repo.update_manga_magazine(manga_id, mid).await?;
746 } else {
747 repo.insert_manga_magazine(manga_id, mid).await?;
748 }
749 }
750 }
751 Ok(())
752}
753
754fn normalize_magazine_id(magazine_id: Option<i32>) -> Option<i32> {
756 magazine_id.filter(|id| *id > 0)
757}
758
759#[cfg(test)]
760mod tests {
761 use super::normalize_magazine_id;
762
763 #[test]
764 fn normalize_magazine_id_filters_invalid() {
765 assert_eq!(normalize_magazine_id(None), None);
766 assert_eq!(normalize_magazine_id(Some(0)), None);
767 assert_eq!(normalize_magazine_id(Some(3)), Some(3));
768 }
769}
770
771#[tracing::instrument(skip_all, level = "debug")]
772async fn save_image(state: &AppState, bytes: &Bytes, member_id: i32) -> ApiResult<String> {
773 let name = format!("manga_{member_id}_{}.jpg", uuid::Uuid::new_v4());
774 let path = PathBuf::from(&state.config.folder.base2)
775 .join("images")
776 .join(&name);
777 if let Some(parent) = path.parent() {
778 tokio::fs::create_dir_all(parent)
779 .await
780 .map_err(|e| AppError::Internal(e.to_string()))?;
781 }
782 tokio::fs::write(&path, bytes)
783 .await
784 .map_err(|e| AppError::Internal(e.to_string()))?;
785 Ok(format!("images/{name}"))
786}