Skip to content

Commit

Permalink
feature: download podcasts
Browse files Browse the repository at this point in the history
Fixes #240
  • Loading branch information
Feichtmeier committed Dec 8, 2023
1 parent 85e2ae7 commit c11d986
Show file tree
Hide file tree
Showing 14 changed files with 291 additions and 71 deletions.
1 change: 1 addition & 0 deletions lib/constants.dart
Expand Up @@ -93,6 +93,7 @@ const kStarredStationsFileName = 'starredStations.json';
const kSettingsFileName = 'settings.json';
const kLastPositionsFileName = 'lastPositions.json';
const kLocalAudioCacheFileName = 'localaudiocache.json';
const kDownloads = 'downloads.json';
const kLocalAudioCache = 'localAudioCache';
const kUseLocalAudioCache = 'cacheSuggestionDisposed';
const kCreateCacheLimit = 1000;
Expand Down
26 changes: 12 additions & 14 deletions lib/src/app/master_items.dart
Expand Up @@ -112,20 +112,18 @@ List<MasterItem> createMasterItems({
podcast.value.firstOrNull?.title ??
podcast.value.firstOrNull.toString(),
),
pageBuilder: (context) => isOnline
? PodcastPage(
pageId: podcast.key,
title: podcast.value.firstOrNull?.album ??
podcast.value.firstOrNull?.title ??
podcast.value.firstOrNull.toString(),
audios: podcast.value,
onTextTap: onTextTap,
addPodcast: addPodcast,
removePodcast: removePodcast,
imageUrl: podcast.value.firstOrNull?.albumArtUrl ??
podcast.value.firstOrNull?.imageUrl,
)
: const OfflinePage(),
pageBuilder: (context) => PodcastPage(
pageId: podcast.key,
title: podcast.value.firstOrNull?.album ??
podcast.value.firstOrNull?.title ??
podcast.value.firstOrNull.toString(),
audios: podcast.value,
onTextTap: onTextTap,
addPodcast: addPodcast,
removePodcast: removePodcast,
imageUrl: podcast.value.firstOrNull?.albumArtUrl ??
podcast.value.firstOrNull?.imageUrl,
),
iconBuilder: (context, selected) => PodcastPage.createIcon(
context: context,
imageUrl: podcast.value.firstOrNull?.albumArtUrl ??
Expand Down
8 changes: 7 additions & 1 deletion lib/src/common/audio_page_body.dart
Expand Up @@ -7,6 +7,7 @@ import '../../common.dart';
import '../../data.dart';
import '../../player.dart';
import '../../podcasts.dart';
import '../app/connectivity_notifier.dart';
import '../library/library_model.dart';

class AudioPageBody extends StatefulWidget {
Expand Down Expand Up @@ -90,6 +91,7 @@ class _AudioPageBodyState extends State<AudioPageBody> {

@override
Widget build(BuildContext context) {
final isOnline = context.select((ConnectivityNotifier c) => c.isOnline);
final isPlaying = context.select((PlayerModel m) => m.isPlaying);

final playerModel = context.read<PlayerModel>();
Expand Down Expand Up @@ -209,13 +211,16 @@ class _AudioPageBodyState extends State<AudioPageBody> {
List.generate(sortedAudios.take(_amount).length, (index) {
final audio = sortedAudios.elementAt(index);
final audioSelected = currentAudio == audio;
final download = libraryModel.getDownload(audio.url);

if (audio.audioType == AudioType.podcast) {
return PodcastAudioTile(
removeUpdate: () =>
libraryModel.removePodcastUpdate(widget.pageId),
isExpanded: audioSelected,
audio: audio,
audio: download != null
? audio.copyWith(path: download)
: audio,
isPlayerPlaying: isPlaying,
selected: audioSelected,
pause: pause,
Expand All @@ -226,6 +231,7 @@ class _AudioPageBodyState extends State<AudioPageBody> {
play: play,
lastPosition: libraryModel.getLastPosition.call(audio.url),
safeLastPosition: playerModel.safeLastPosition,
isOnline: isOnline,
);
}

Expand Down
5 changes: 3 additions & 2 deletions lib/src/data/audio.dart
Expand Up @@ -252,8 +252,9 @@ class Audio {
if (identical(this, other)) return true;

return other is Audio &&
other.path == path &&
other.url == url &&
(other.path == path ||
(other.url == url && other.path != null) ||
other.url == url) &&
other.audioType == audioType &&
other.imageUrl == imageUrl &&
other.description == description &&
Expand Down
8 changes: 8 additions & 0 deletions lib/src/library/library_model.dart
Expand Up @@ -22,6 +22,7 @@ class LibraryModel extends SafeChangeNotifier {
StreamSubscription<bool>? _neverShowFailedImportsSub;
StreamSubscription<bool>? _favTagsSub;
StreamSubscription<bool>? _lastFavSub;
StreamSubscription<bool>? _downloadsSub;

bool get neverShowFailedImports => _service.neverShowFailedImports;
Future<void> setNeverShowLocalImports() async =>
Expand Down Expand Up @@ -56,6 +57,7 @@ class LibraryModel extends SafeChangeNotifier {
_service.neverShowFailedImportsChanged.listen((_) => notifyListeners());
_favTagsSub = _service.favTagsChanged.listen((_) => notifyListeners());
_lastFavSub = _service.lastFavChanged.listen((_) => notifyListeners());
_downloadsSub = _service.downloadsChanged.listen((_) => notifyListeners());

notifyListeners();
}
Expand All @@ -79,6 +81,7 @@ class LibraryModel extends SafeChangeNotifier {
_neverShowFailedImportsSub?.cancel();
_favTagsSub?.cancel();
_lastFavSub?.cancel();
_downloadsSub?.cancel();

super.dispose();
}
Expand Down Expand Up @@ -184,6 +187,11 @@ class LibraryModel extends SafeChangeNotifier {
void removePodcastUpdate(String feedUrl) =>
_service.removePodcastUpdate(feedUrl);

int get downloadsLength => _service.downloads.length;

String? getDownload(String? url) =>
url == null ? null : _service.downloads[url];

//
// Albums
//
Expand Down
18 changes: 18 additions & 0 deletions lib/src/library/library_service.dart
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';

import '../../constants.dart';
Expand Down Expand Up @@ -227,7 +228,21 @@ class LibraryService {
}

// Podcasts
final dio = Dio();
Map<String, String> _downloads = {};
Map<String, String> get downloads => _downloads;
String? getDownload(String? url) => downloads[url];

final _downloadsController = StreamController<bool>.broadcast();
Stream<bool> get downloadsChanged => _downloadsController.stream;
void addDownload(String url, String path) {
_downloads.putIfAbsent(url, () => path);
writeStringMap(_downloads, kDownloads)
.then((_) => _downloadsController.add(true));
}

String? _downloadsDir;
String? get downloadsDir => _downloadsDir;
Map<String, Set<Audio>> _podcasts = {};
Map<String, Set<Audio>> get podcasts => _podcasts;
int get podcastsLength => _podcasts.length;
Expand Down Expand Up @@ -342,6 +357,8 @@ class LibraryService {
(await readAudioMap(kLikedAudios)).entries.firstOrNull?.value ??
<Audio>{};
_favTags = (await readStringSet(filename: kTagFavsFileName));
_downloadsDir = await getDownloadsDir();
_downloads = await readStringMap(kDownloads);
_libraryInitialized = true;
}

Expand Down Expand Up @@ -399,6 +416,7 @@ class LibraryService {
}

Future<void> dispose() async {
dio.close();
await safeStates();
await _useLocalAudioCacheController.close();
await _albumsController.close();
Expand Down
56 changes: 56 additions & 0 deletions lib/src/podcasts/download_button.dart
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:ubuntu_service/ubuntu_service.dart';

import '../../common.dart';
import '../../data.dart';
import '../../library.dart';
import 'download_model.dart';

class DownloadButton extends StatelessWidget {
const DownloadButton({
super.key,
this.iconSize,
required this.audio,
});

static Widget create({
required BuildContext context,
double? iconSize,
required Audio? audio,
}) {
return ChangeNotifierProvider(
create: (_) => DownloadModel(getService<LibraryService>()),
child: DownloadButton(
iconSize: iconSize,
audio: audio,
),
);
}

final double? iconSize;
final Audio? audio;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final model = context.read<DownloadModel>();
final value = context.select((DownloadModel m) => m.value);
return Stack(
alignment: Alignment.center,
children: [
Progress(
value: value ?? 0,
backgroundColor: Colors.transparent,
),
IconButton(
icon: Icon(Iconz().download),
// TODO: add remove download
onPressed: () => model.startDownload(audio),
iconSize: iconSize,
color: audio?.path != null ? theme.colorScheme.primary : null,
),
],
);
}
}
66 changes: 66 additions & 0 deletions lib/src/podcasts/download_model.dart
@@ -0,0 +1,66 @@
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:safe_change_notifier/safe_change_notifier.dart';
import 'package:path/path.dart' as p;

import '../../data.dart';
import '../../library.dart';

class DownloadModel extends SafeChangeNotifier {
DownloadModel(this._service);

final LibraryService _service;

double? _value;
double? get value => _value;
void setValue(int received, int total) {
if (total <= 0) return;
_value = received / total;
notifyListeners();
}

Future<void> startDownload(
Audio? audio,
) async {
_service.dio.interceptors.add(LogInterceptor());
// Assure the value of total argument of onReceiveProgress is not -1.
_service.dio.options.headers = {HttpHeaders.acceptEncodingHeader: '*'};
final downloadsDir = _service.downloadsDir;

if (audio?.url == null || downloadsDir == null) return;

final downloadDir = p.join(downloadsDir, 'musicpod');

if (!Directory(downloadDir).existsSync()) {
Directory(downloadDir).createSync();
}
final fileName =
'${audio?.artist}${audio?.title}${audio?.year}'.replaceAll(' ', '_');
final file = File(
p.join(
downloadDir,
fileName,
),
);
file.createSync();

download1(_service.dio, audio!.url!, file.path);
}

Future download1(Dio dio, String url, String path) async {
final cancelToken = CancelToken();
try {
await dio
.download(
url,
path,
onReceiveProgress: setValue,
cancelToken: cancelToken,
)
.then((_) => _service.addDownload(url, path));
} catch (e) {
//TODO: manage download exception
}
}
}

0 comments on commit c11d986

Please sign in to comment.