@@ -47,6 +47,13 @@ public class ProgressiveDownloadInfo
4747 public bool IsDownloading { get ; set ; }
4848 public DateTime StartTime { get ; set ; }
4949 public CancellationTokenSource ? CancellationTokenSource { get ; set ; }
50+ /// <summary>
51+ /// Set when the user cancelled the background download from the downloads list.
52+ /// While set (and not expired), new range requests won't restart the cache download;
53+ /// playback keeps working through direct Telegram fetches.
54+ /// </summary>
55+ public bool CancelledByUser { get ; set ; }
56+ public DateTime CancelledAtUtc { get ; set ; }
5057 }
5158
5259 public class ProgressiveDownloadService : IProgressiveDownloadService
@@ -109,14 +116,23 @@ public async Task<ProgressiveDownloadInfo> StartOrGetDownloadAsync(
109116 string filePath )
110117 {
111118 // Quick check if already complete
112- if ( _activeDownloads . TryGetValue ( cacheKey , out var existingInfo ) && existingInfo . IsComplete )
119+ if ( _activeDownloads . TryGetValue ( cacheKey , out var existingInfo ) )
113120 {
114- if ( File . Exists ( existingInfo . FilePath ) )
121+ if ( existingInfo . IsComplete )
115122 {
123+ if ( File . Exists ( existingInfo . FilePath ) )
124+ {
125+ return existingInfo ;
126+ }
127+ // Cache file was evicted: forget the stale entry and re-download
128+ _activeDownloads . TryRemove ( cacheKey , out _ ) ;
129+ }
130+ else if ( IsUserCancelActive ( existingInfo ) )
131+ {
132+ // User stopped this cache download: don't restart it on every range
133+ // request; playback is served through direct Telegram fetches instead
116134 return existingInfo ;
117135 }
118- // Cache file was evicted: forget the stale entry and re-download
119- _activeDownloads . TryRemove ( cacheKey , out _ ) ;
120136 }
121137
122138 // Check if file already exists and is complete
@@ -149,6 +165,7 @@ public async Task<ProgressiveDownloadInfo> StartOrGetDownloadAsync(
149165 if ( _activeDownloads . TryGetValue ( cacheKey , out existingInfo ) )
150166 {
151167 if ( existingInfo . IsDownloading ||
168+ IsUserCancelActive ( existingInfo ) ||
152169 ( existingInfo . IsComplete && File . Exists ( existingInfo . FilePath ) ) )
153170 {
154171 return existingInfo ;
@@ -229,6 +246,9 @@ private async Task DownloadInBackgroundAsync(
229246 path = info . FilePath ,
230247 name = Path . GetFileName ( info . FilePath ) ,
231248 _size = info . TotalSize ,
249+ // Start the counter at the resume offset so progress and global
250+ // speed stats only account for newly downloaded bytes
251+ _transmitted = startOffset ,
232252 channelName = _ts . getChatName ( Convert . ToInt64 ( channelId ) )
233253 } ;
234254 _tis . addToDownloadList ( dm ) ;
@@ -266,6 +286,16 @@ private async Task DownloadInBackgroundAsync(
266286
267287 while ( localOffset < partSize && ! ct . IsCancellationRequested )
268288 {
289+ // React to Cancel/Pause pressed in the downloads list
290+ if ( dm . state == StateTask . Canceled || dm . state == StateTask . Paused )
291+ {
292+ _logger . LogInformation ( "Background download stopped by user: {CacheKey}" , info . CacheKey ) ;
293+ info . CancelledByUser = true ;
294+ info . CancelledAtUtc = DateTime . UtcNow ;
295+ aborted = true ;
296+ break ;
297+ }
298+
269299 var toDownload = ( int ) Math . Min ( chunkSize , partSize - localOffset ) ;
270300
271301 try
@@ -283,15 +313,22 @@ private async Task DownloadInBackgroundAsync(
283313
284314 localOffset += chunk . Length ;
285315 info . DownloadedBytes = partStartGlobal + localOffset ;
286- dm . _transmitted = info . DownloadedBytes ;
287316 consecutiveErrors = 0 ;
288317
289- // Log progress every 10%
290- var progress = ( double ) info . DownloadedBytes / info . TotalSize * 100 ;
291- if ( ( int ) progress % 10 == 0 )
318+ try
292319 {
293- _logger . LogDebug ( "Download progress: {FileName} - {Progress:F1}%" ,
294- Path . GetFileName ( info . FilePath ) , progress ) ;
320+ // Updates progress/speed and notifies the downloads UI;
321+ // marks the task completed when the last byte is written
322+ dm . ProgressCallback ( info . DownloadedBytes , info . TotalSize ) ;
323+ }
324+ catch
325+ {
326+ // ProgressCallback throws when Cancel/Pause was pressed
327+ _logger . LogInformation ( "Background download stopped by user: {CacheKey}" , info . CacheKey ) ;
328+ info . CancelledByUser = true ;
329+ info . CancelledAtUtc = DateTime . UtcNow ;
330+ aborted = true ;
331+ break ;
295332 }
296333 }
297334 catch ( Exception ex ) when ( ex is not OperationCanceledException )
@@ -319,6 +356,12 @@ private async Task DownloadInBackgroundAsync(
319356 info . IsComplete = info . DownloadedBytes >= info . TotalSize ;
320357 info . IsDownloading = false ;
321358
359+ // Don't leave an incomplete task hanging as "Working" in the downloads list
360+ if ( ! info . IsComplete && dm . state == StateTask . Working )
361+ {
362+ dm . Cancel ( ) ;
363+ }
364+
322365 _logger . LogInformation ( "Background download {Status}: {FileName} ({Downloaded}/{Total} bytes)" ,
323366 info . IsComplete ? "complete" : "incomplete" ,
324367 Path . GetFileName ( info . FilePath ) ,
@@ -342,6 +385,15 @@ private async Task DownloadInBackgroundAsync(
342385 }
343386 }
344387
388+ // A user cancel blocks automatic restarts for this long; afterwards a new
389+ // playback of the file starts caching again
390+ private static readonly TimeSpan USER_CANCEL_TTL = TimeSpan . FromHours ( 1 ) ;
391+
392+ private static bool IsUserCancelActive ( ProgressiveDownloadInfo info )
393+ {
394+ return info . CancelledByUser && DateTime . UtcNow - info . CancelledAtUtc < USER_CANCEL_TTL ;
395+ }
396+
345397 /// <summary>
346398 /// Ordered list of Telegram message IDs that make up a file: a single message for
347399 /// regular files, several (one per part) for split files.
0 commit comments