Overview
Aradia supports downloading audiobooks from Archive.org and YouTube for offline playback. The download system handles large files efficiently with progress tracking, pause/resume capabilities, and automatic error recovery.
Download Features
Background Downloads Downloads continue even when the app is closed
Progress Tracking Real-time progress updates for each file
Pause/Resume Pause and resume downloads at any time
Smart Chunking Handles large files with intelligent chunking
Download Manager
The DownloadManager class handles all download operations:
Download Manager Setup (download_manager.dart:10-19)
class DownloadManager {
static final DownloadManager _instance = DownloadManager . _internal ();
factory DownloadManager () => _instance;
DownloadManager . _internal ();
final FileDownloader _downloader = FileDownloader ();
final Box < dynamic > downloadStatusBox = Hive . box ( 'download_status_box' );
final Map < String , bool > _activeDownloads = {};
static const int _veryLargeFileThresholdBytes = 50 * 1024 * 1024 ;
}
Permission Handling
Check Permissions
Request storage and notification permissions before downloading: Permission Check (download_manager.dart:21-23)
Future < bool > checkAndRequestPermissions () async {
return await PermissionHelper . requestDownloadPermissions ();
}
Configure Notifications
Set up download notifications (if permissions granted): Notification Config (download_manager.dart:42-53)
if (hasNotificationPermission) {
_downloader. configureNotification (
running : TaskNotification (
'Downloading $ audiobookTitle ' ,
'File: {filename}'
),
progressBar : true ,
complete : TaskNotification (
'Download complete: $ audiobookTitle ' ,
'File: {filename}'
),
error : TaskNotification (
'Download error: $ audiobookTitle ' ,
'File: {filename}'
),
);
}
Download Process
Archive.org Downloads
Standard HTTP downloads with progress tracking:
Direct Download (download_manager.dart:188-226)
DownloadTask task = DownloadTask (
taskId : uniqueFileTaskId,
url : url,
filename : fileName,
directory : currentFileDirectoryPath,
baseDirectory : BaseDirectory .applicationDocuments,
updates : Updates .statusAndProgress,
allowPause : true ,
);
await _downloader. download (task, onProgress : (progress) {
if (_activeDownloads[audiobookId] != true ) {
_downloader. cancelTaskWithId (task.taskId);
throw Exception ( 'Download cancelled' );
}
totalProgress = (completedFiles + progress) / totalFiles;
onProgressUpdate (totalProgress);
downloadStatusBox. put ( 'status_ $ audiobookId ' , {
'isDownloading' : true ,
'progress' : totalProgress,
'isCompleted' : false ,
'audiobookTitle' : audiobookTitle,
'audiobookId' : audiobookId,
'isYouTube' : false ,
});
});
YouTube Downloads
YouTube downloads use the youtube_explode_dart package with adaptive streaming:
YouTube Download (download_manager.dart:92-163)
if (isYouTubeUrl) {
// Parse video ID from URL
String ? parsedVideoId = Uri . parse (url).queryParameters[ 'v' ] ??
(url. contains ( 'youtu.be/' )
? url. split ( 'youtu.be/' ).last. split ( '?' ).first
: null );
// Get audio stream manifest
final manifest = await yt.videos.streams. getManifest (
parsedVideoId,
requireWatchPage : true ,
ytClients : [ YoutubeApiClient .androidVr]
);
// Select best quality MP4 audio stream
List < AudioOnlyStreamInfo > mp4AudioStreams = manifest.audioOnly
. where ((s) => s.container == StreamContainer .mp4)
. sortByBitrate ()
. toList ();
AudioOnlyStreamInfo ? audioStreamInfo = mp4AudioStreams.isNotEmpty
? mp4AudioStreams.last
: manifest.audioOnly. withHighestBitrate ();
final int totalBytesForFile = audioStreamInfo.size.totalBytes;
int receivedBytesForFile = 0 ;
// Use chunking for throttled or very large files
final bool useChunking = audioStreamInfo.isThrottled ||
totalBytesForFile > _veryLargeFileThresholdBytes;
final stream = audioStreamClient. getAudioStream (
audioStreamInfo,
start : 0 ,
end : totalBytesForFile,
isThrottledOrVeryLarge : useChunking,
);
// Download with progress tracking
await for ( final data in stream) {
if (_activeDownloads[audiobookId] != true ) {
await fileStream. close ();
if ( await outputFile. exists ()) await outputFile. delete ();
throw Exception ( 'Download cancelled' );
}
fileStream. add (data);
receivedBytesForFile += data.length;
double fileProgress = totalBytesForFile > 0
? (receivedBytesForFile / totalBytesForFile)
: 0.0 ;
totalProgress = (completedFiles + fileProgress) / totalFiles;
onProgressUpdate (totalProgress);
}
}
Smart Chunking : Files larger than 50MB or throttled streams use chunked downloading for reliability.
Progress Tracking
Download status is persisted to Hive for tracking across app restarts:
Status Tracking (download_manager.dart:62-71)
await downloadStatusBox. put ( 'status_ $ audiobookId ' , {
'isDownloading' : true ,
'progress' : 0.0 ,
'isCompleted' : false ,
'audiobookTitle' : audiobookTitle,
'audiobookId' : audiobookId,
'isYouTube' : files. any ((f) =>
(f[ 'url' ] as String ). contains ( 'youtube.com' ) ||
(f[ 'url' ] as String ). contains ( 'youtu.be' )),
});
Checking Download Status
Is Downloading
Is Downloaded
Get Progress
bool isDownloading ( String audiobookId) {
final status = downloadStatusBox. get ( 'status_ $ audiobookId ' );
return _activeDownloads[audiobookId] == true ||
(status != null && status[ 'isDownloading' ] == true );
}
bool isDownloaded ( String audiobookId) {
final status = downloadStatusBox. get ( 'status_ $ audiobookId ' );
return status != null && status[ 'isCompleted' ] == true ;
}
double getProgress ( String audiobookId) {
final status = downloadStatusBox. get ( 'status_ $ audiobookId ' );
return status != null
? (status[ 'progress' ] as num ? ) ? . toDouble () ?? 0.0
: 0.0 ;
}
Pause & Resume
Downloads can be paused and resumed:
Pause/Resume (download_manager.dart:364-386)
Future < void > pauseDownload ( String uniqueFileTaskId) async {
try {
final taskJson = downloadStatusBox. get ( 'task_ $ uniqueFileTaskId ' );
if (taskJson != null ) {
final task = DownloadTask . fromJson (taskJson as Map < String , dynamic >);
await _downloader. pause (task);
}
} catch (e) {
AppLogger . debug ( 'Pause Error: $ e ' );
}
}
Future < void > resumeDownload ( String uniqueFileTaskId) async {
try {
final taskJson = downloadStatusBox. get ( 'task_ $ uniqueFileTaskId ' );
if (taskJson != null ) {
final task = DownloadTask . fromJson (taskJson as Map < String , dynamic >);
await _downloader. resume (task);
}
} catch (e) {
AppLogger . debug ( 'Resume Error: $ e ' );
}
}
YouTube Downloads : Pause/resume is not available for YouTube downloads due to stream-based downloading.
Cancel Downloads
Cancel ongoing downloads and clean up partial files:
Cancel Download (download_manager.dart:315-334)
void cancelDownload ( String audiobookId) async {
_activeDownloads. remove (audiobookId);
// Cancel all associated tasks
for ( var key in downloadStatusBox.keys. toList ()) {
if (key. toString (). startsWith ( 'task_ $ audiobookId -' )) {
final taskJson = downloadStatusBox. get (key);
if (taskJson != null ) {
try {
final task = DownloadTask . fromJson (
taskJson as Map < String , dynamic >
);
await _downloader. cancelTaskWithId (task.taskId);
} catch (e) {
AppLogger . debug ( 'Cancel Error: $ e ' );
}
}
await downloadStatusBox. delete (key);
}
}
// Clean up partial files
await _cleanupPartialDownload (audiobookId);
await downloadStatusBox. delete ( 'status_ $ audiobookId ' );
}
Automatic Cleanup
Cleanup Partial Downloads (download_manager.dart:303-313)
Future < void > _cleanupPartialDownload ( String audiobookId) async {
try {
final baseDir = await getExternalStorageDirectory ();
final downloadDir = Directory ( ' ${ baseDir ?. path } /downloads/ $ audiobookId ' );
if ( await downloadDir. exists ()) {
await downloadDir. delete (recursive : true );
}
} catch (e) {
AppLogger . debug ( 'Cleanup Error: $ e ' );
}
}
Error Handling
Download errors are captured and stored:
Error Tracking (download_manager.dart:226-241)
catch (e) {
_activeDownloads. remove (audiobookId);
await downloadStatusBox. put ( 'status_ $ audiobookId ' , {
'isDownloading' : false ,
'progress' : totalProgress,
'isCompleted' : false ,
'error' : 'File $ fileName : ${ e . toString ()} ' ,
'audiobookTitle' : audiobookTitle,
'audiobookId' : audiobookId,
'isYouTube' : false ,
});
AppLogger . debug ( 'Direct Download Error: $ e ' );
await _cleanupPartialDownload (audiobookId);
onCompleted ( false );
return ;
}
Download errors are automatically cleaned up, and you can retry by initiating a new download.
Storage Location
/storage/emulated/0/Android/data/com.example.aradia/files/downloads/{audiobookId}/
/storage/emulated/0/Android/data/com.example.aradia/files/youtube/{videoId}/
All downloads are stored in the app’s external storage directory and persist until manually deleted.
Next Steps Learn how to import audiobooks from YouTube