@@ -67,17 +67,7 @@ public async Task FinalizeAsync(SeriesContext seriesContext, ActionsContext acti
6767 List < Chapter > seriesChapters = await seriesContext . Chapters
6868 . Where ( c => c . ParentMangaId == chapter . ParentMangaId && ! c . Downloaded )
6969 . ToListAsync ( ct ) ;
70- placed = 0 ;
71- foreach ( string archive in archives )
72- {
73- ParsedRelease parsed = ReleaseTitleParser . Parse ( Path . GetFileNameWithoutExtension ( archive ) ) ;
74- // Same-series check keeps a pack's extras/specials from claiming a main-run issue number.
75- if ( parsed . IssueNumber is null ) continue ;
76- if ( ! string . Equals ( parsed . SeriesTitle , chapter . ParentManga . Name , StringComparison . OrdinalIgnoreCase ) ) continue ;
77- Chapter ? target = seriesChapters . FirstOrDefault ( c => c . ChapterNumber == parsed . IssueNumber && ! c . Downloaded ) ;
78- if ( target is null ) continue ;
79- if ( Place ( archive , target , settings , actionsContext ) ) placed ++ ;
80- }
70+ placed = FanOut ( archives , chapter . ParentManga . Name , seriesChapters , settings , actionsContext ) ;
8171 }
8272
8373 if ( placed == 0 )
@@ -100,6 +90,74 @@ public async Task FinalizeAsync(SeriesContext seriesContext, ActionsContext acti
10090 Log . InfoFormat ( "Finalised torrent for {0}: placed {1} chapter file(s)." , chapter . ParentManga . Name , placed ) ;
10191 }
10292
93+ /// <summary>
94+ /// Finalises a completed pack torrent (tag <c>pack:{seriesKey}:{hash}</c>): fans its archives out
95+ /// to every undownloaded chapter of the series they parse to, then removes the torrent. The pack
96+ /// is removed even when nothing could be placed — its contents won't change, so leaving it would
97+ /// only make the completion reconciler re-enqueue a hopeless finalise forever.
98+ /// </summary>
99+ public async Task FinalizePackAsync ( SeriesContext seriesContext , ActionsContext actionsContext ,
100+ IDownloadClient downloadClient , KenkuSettings settings , string tag , string seriesKey , string savePath ,
101+ CancellationToken ct )
102+ {
103+ Series ? series = await seriesContext . Series
104+ . Include ( s => s . Library )
105+ . FirstOrDefaultAsync ( s => s . Key == seriesKey , ct ) ;
106+ if ( series is null )
107+ {
108+ Log . ErrorFormat ( "Could not finalise pack {0}: series {1} not found." , tag , seriesKey ) ;
109+ return ;
110+ }
111+
112+ if ( ! Directory . Exists ( savePath ) )
113+ {
114+ Log . ErrorFormat ( "Pack {0} reports completion at {1} but the directory does not exist." , tag , savePath ) ;
115+ return ;
116+ }
117+
118+ List < Chapter > chapters = await seriesContext . Chapters
119+ . Where ( c => c . ParentMangaId == seriesKey && ! c . Downloaded )
120+ . ToListAsync ( ct ) ;
121+
122+ string [ ] archives = Directory . EnumerateFiles ( savePath , "*.cbz" , SearchOption . AllDirectories ) . ToArray ( ) ;
123+ int placed = FanOut ( archives , series . Name , chapters , settings , actionsContext ) ;
124+
125+ if ( placed > 0 )
126+ {
127+ var syncs = await Task . WhenAll (
128+ seriesContext . Sync ( ct , typeof ( TorrentFinalizationService ) , nameof ( FinalizePackAsync ) ) ,
129+ actionsContext . Sync ( ct , typeof ( TorrentFinalizationService ) , nameof ( FinalizePackAsync ) ) ) ;
130+ foreach ( var s in syncs )
131+ if ( ! s . success ) Log . ErrorFormat ( "Sync failed during pack finalise: {0}" , s . exceptionMessage ) ;
132+ }
133+ else
134+ {
135+ Log . ErrorFormat ( "Pack {0} at {1} contained {2} archive(s) but none matched an undownloaded chapter of {3}." ,
136+ tag , savePath , archives . Length , series . Name ) ;
137+ }
138+
139+ await downloadClient . Remove ( tag , deleteData : false , ct ) ;
140+ Log . InfoFormat ( "Finalised pack {0} for {1}: placed {2} chapter file(s)." , tag , series . Name , placed ) ;
141+ }
142+
143+ /// <summary>Fans pack archives out to the chapters their filenames parse to. Returns how many were placed.</summary>
144+ private static int FanOut ( string [ ] archives , string seriesName , List < Chapter > chapters ,
145+ KenkuSettings settings , ActionsContext actionsContext )
146+ {
147+ int placed = 0 ;
148+ foreach ( string archive in archives )
149+ {
150+ ParsedRelease parsed = ReleaseTitleParser . Parse ( Path . GetFileNameWithoutExtension ( archive ) ) ;
151+ // Same-series check keeps a pack's extras/specials from claiming a main-run issue number.
152+ if ( parsed . IssueNumber is null ) continue ;
153+ if ( ! string . Equals ( parsed . SeriesTitle , seriesName , StringComparison . OrdinalIgnoreCase ) ) continue ;
154+ Chapter ? target = chapters . FirstOrDefault ( c => c . ChapterNumber == parsed . IssueNumber && ! c . Downloaded ) ;
155+ if ( target is null ) continue ;
156+ if ( Place ( archive , target , settings , actionsContext ) ) placed ++ ;
157+ }
158+ return placed ;
159+ }
160+
103161 /// <summary>Moves one archive into <paramref name="chapter"/>'s publication path and marks it downloaded.</summary>
104162 private static bool Place ( string archive , Chapter chapter , KenkuSettings settings , ActionsContext actionsContext )
105163 {
0 commit comments