Overview
Aradia can import audio from YouTube videos and playlists, making them available as audiobooks. This feature is perfect for lectures, podcasts, or any long-form audio content on YouTube.
Import Features
Single Videos Import individual YouTube videos as standalone audiobooks
Playlists Import entire playlists with all videos as chapters
WebView Browser Built-in YouTube browser for easy navigation
Metadata Extraction Automatic extraction of title, author, and description
YouTube WebView
The app includes a built-in YouTube browser powered by flutter_inappwebview:
WebView Setup (youtube_webview.dart:293-313)
InAppWebView (
keepAlive : Provider . of < WebViewKeepAliveProvider >(context, listen : false )
.keepAlive,
initialUrlRequest : URLRequest (
url : WebUri ( 'https://www.youtube.com' )
),
onWebViewCreated : (controller) async {
_webViewController = controller;
final url = await controller. getUrl ();
if (url != null && url. toString () != 'about:blank' ) {
if (mounted) {
setState (() {
_currentUrl = url. toString ();
_isWebViewLoading = false ;
_checkIfCurrentContentIsImported ();
});
}
}
},
// ... URL tracking handlers
)
URL Tracking
The WebView tracks navigation to detect when a user is on a video or playlist page:
URL Detection (youtube_webview.dart:314-338)
onLoadStop : (controller, url) {
if (mounted) {
setState (() {
_isWebViewLoading = false ;
_currentUrl = url. toString ();
_checkIfCurrentContentIsImported ();
});
}
},
onUpdateVisitedHistory : (controller, url, androidIsReload) {
if (mounted && url != null ) {
setState (() {
_currentUrl = url. toString ();
_checkIfCurrentContentIsImported ();
});
}
}
The import button only appears when viewing a YouTube video or playlist page.
Import Process
Navigate to Content
Browse YouTube and navigate to a video or playlist: URL Validation (youtube_webview.dart:96-101)
if ( ! _currentUrl ! . contains ( 'youtube.com/watch' ) &&
! _currentUrl ! . contains ( 'youtube.com/playlist' )) {
setState (() => _errorMessageYT =
'Please navigate to a YouTube video or playlist page' );
return ;
}
Extract Metadata
The app extracts video/playlist information using youtube_explode_dart: Video Import (youtube_webview.dart:152-175)
final video = await yt.videos. get (_currentUrl ! );
entityId = video.id.value;
entityTitle = video.title;
entityAuthor = video.author;
entityDescription = video.description;
tags = _extractTags (video.description);
coverImage = video.thumbnails.highResUrl;
files. add ( AudiobookFile . fromMap ({
"identifier" : video.id.value,
"title" : video.title,
"name" : " ${ video . id . value } .mp3" ,
"track" : 1 ,
"size" : 0 ,
"length" : video.duration ? .inSeconds. toDouble () ?? 0.0 ,
"url" : video.url,
"highQCoverImage" : video.thumbnails.highResUrl,
}));
Playlist Import (youtube_webview.dart:123-151)
final playlist = await yt.playlists. get (_currentUrl ! );
entityId = playlist.id.value;
entityTitle = playlist.title;
entityAuthor = playlist.author;
entityDescription = playlist.description;
tags = _extractTags (playlist.description);
final videos = await yt.playlists
. getVideos (playlist.id)
. toList ();
if (videos.isEmpty) {
throw Exception ( "Playlist contains no videos." );
}
coverImage = videos.first.thumbnails.highResUrl;
for ( var video in videos) {
files. add ( AudiobookFile . fromMap ({
"identifier" : video.id.value,
"title" : video.title,
"name" : " ${ video . id . value } .mp3" ,
"track" : files.length + 1 ,
"url" : video.url,
"highQCoverImage" : video.thumbnails.highResUrl,
}));
}
Extract Tags
Hashtags are automatically extracted from descriptions: Tag Extraction (youtube_webview.dart:204-214)
List < String > _extractTags ( String description) {
final tags = < String > {};
final words = description. split ( RegExp ( r'\s+' ));
for ( final word in words) {
if (word. startsWith ( '#' ) && word.length > 1 ) {
final cleaned = word. substring ( 1 )
. replaceAll ( RegExp ( r'[^\w-]' ), '' );
if (cleaned.isNotEmpty) tags. add (cleaned);
}
}
return tags. toList ();
}
Create Audiobook
Build the audiobook object with extracted metadata: Audiobook Creation (youtube_webview.dart:177-191)
final audiobook = Audiobook . fromMap ({
"title" : entityTitle,
"id" : entityId,
"description" : entityDescription,
"author" : entityAuthor,
"date" : DateTime . now (). toIso8601String (),
"downloads" : 0 ,
"subject" : tags,
"size" : 0 ,
"rating" : 0.0 ,
"reviews" : 0 ,
"lowQCoverImage" : coverImage,
"language" : "en" ,
"origin" : AppConstants .youtubeDirName,
});
Save Metadata
Store audiobook and file metadata locally: Metadata Storage (youtube_webview.dart:216-230)
Future < void > _saveYouTubeAudiobookMetadata (
Audiobook audiobook, List < AudiobookFile > files) async {
final appDir = await getExternalStorageDirectory ();
if (appDir == null ) {
throw Exception ( 'Could not access storage directory' );
}
final audiobookDir = Directory (
p. join (appDir.path, AppConstants .youtubeDirName, audiobook.id)
);
await audiobookDir. create (recursive : true );
final metadataFile = File (
p. join (audiobookDir.path, 'audiobook.txt' )
);
await metadataFile. writeAsString (
jsonEncode (audiobook. toMap ())
);
final filesFile = File (
p. join (audiobookDir.path, 'files.txt' )
);
await filesFile. writeAsString (
jsonEncode (files. map ((f) => f. toJson ()). toList ())
);
}
Duplicate Detection
The app prevents re-importing already imported content:
Duplicate Check (youtube_webview.dart:53-84)
void _checkIfCurrentContentIsImported () {
if (_currentUrl == null ) {
_isCurrentContentImported = false ;
return ;
}
try {
String ? entityId;
if (_currentUrl ! . contains ( 'playlist?list=' )) {
final uri = Uri . parse (_currentUrl ! );
entityId = uri.queryParameters[ 'list' ];
} else if (_currentUrl ! . contains ( 'youtube.com/watch' )) {
final uri = Uri . parse (_currentUrl ! );
entityId = uri.queryParameters[ 'v' ];
}
if (entityId != null ) {
final wasImported = _isCurrentContentImported;
_isCurrentContentImported =
_audioBookNotifier. isAudiobookAlreadyImported (entityId);
if (wasImported != _isCurrentContentImported) {
setState (() {});
}
} else {
_isCurrentContentImported = false ;
}
} catch (e) {
_isCurrentContentImported = false ;
}
}
The floating action button adapts based on import status:
Import FAB (youtube_webview.dart:364-384)
floatingActionButton : _currentUrl != null &&
(_currentUrl ! . contains ( 'youtube.com/watch' ) ||
_currentUrl ! . contains ( 'youtube.com/playlist' ))
? FloatingActionButton (
heroTag : 'import' ,
backgroundColor : AppColors .primaryColor,
onPressed : _isImporting
? null
: _isCurrentContentImported
? () => context. go ( '/home' )
: _importFromYouTube,
child : _isImporting
? const CircularProgressIndicator (
strokeWidth : 2 ,
color : Colors .white,
)
: _isCurrentContentImported
? const Icon ( Icons .check_circle, color : Colors .white)
: const Icon ( Icons .add_to_queue, color : Colors .white),
)
: null ,
Not Imported Shows add icon when content hasn’t been imported
Importing Shows loading spinner during import
Imported Shows checkmark if already imported
WebView Keep-Alive
The WebView state persists across navigation:
Keep-Alive Provider (youtube_webview.dart:293-296)
InAppWebView (
keepAlive : Provider . of < WebViewKeepAliveProvider >(context, listen : false )
.keepAlive,
// ...
)
The WebView maintains your browsing session even when navigating away from the import screen.
Error Handling
if ( ! _currentUrl ! . contains ( 'youtube.com/watch' ) &&
! _currentUrl ! . contains ( 'youtube.com/playlist' )) {
setState (() => _errorMessageYT =
'Please navigate to a YouTube video or playlist page' );
return ;
}
if (videos.isEmpty) {
throw Exception ( "Playlist contains no videos." );
}
catch (e) {
if (mounted) {
setState (() => _errorMessageYT =
'Error importing from YouTube: $ e ' );
}
} finally {
if (mounted) setState (() => _isImporting = false );
}
Playback
Imported YouTube audiobooks are streamed on-demand:
No Pre-Download Required : YouTube imports stream audio directly without downloading. Use the download feature if you need offline access.
YouTube imports support all standard player features including speed control, sleep timer, and Chromecast!
Next Steps Learn how to import audiobooks from local storage