Skip to content

Commit

Permalink
feat: use documents folder for downloads
Browse files Browse the repository at this point in the history
  • Loading branch information
phanan committed Nov 22, 2023
1 parent c9a4383 commit 0e94663
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 43 deletions.
2 changes: 1 addition & 1 deletion lib/audio_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class KoelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
if (download == null) {
await _player.setUrl(mediaItem.extras?['sourceUrl'] as String);
} else {
await _player.setFilePath(download.file.path);
await _player.setFilePath(download.path);
}
}

Expand Down
117 changes: 79 additions & 38 deletions lib/providers/download_provider.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import 'dart:async';
import 'dart:io';

import 'package:app/mixins/stream_subscriber.dart';
import 'package:app/models/models.dart';
import 'package:app/providers/providers.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:app/utils/crypto.dart';
import 'package:get_storage/get_storage.dart';
import 'package:collection/collection.dart';
import 'package:app/utils/preferences.dart' as preferences;
import 'package:path_provider/path_provider.dart';

class Download {
final Song song;
final FileInfo file;
final String path;

Download({required this.song, required this.file});
Download({required this.song, required this.path});
}

class DownloadProvider with StreamSubscriber {
Expand All @@ -32,16 +34,8 @@ class DownloadProvider with StreamSubscriber {
Stream<Download> get songDownloadedStream => _songDownloaded.stream;

static const serializedSongContainer = 'Downloads';
static const downloadCacheKey = 'koel.downloaded.songs';
static final _songStorage = GetStorage(serializedSongContainer);

static final _downloadManager = CacheManager(
Config(
downloadCacheKey,
stalePeriod: Duration(days: 365 * 10),
),
);

DownloadProvider({required SongProvider songProvider})
: _songProvider = songProvider {
subscribe(AuthProvider.userLoggedOutStream.listen((_) {
Expand All @@ -57,55 +51,90 @@ class DownloadProvider with StreamSubscriber {
_collectDownloads();
}

Future<String> get downloadsDir async {
final documentsDir = await getApplicationDocumentsDirectory();
final hash =
sha256('${preferences.host}${preferences.userEmail}'.toLowerCase());
return '${documentsDir.path}/${hash}';
}

Future<void> _collectDownloads() async {
_downloads.clear();
final serializedSongs =
_songStorage.read<List<dynamic>>(serializedSongKey) ?? [];

await Future.forEach<dynamic>(serializedSongs, (json) async {
var downloadsDir = await this.downloadsDir;

serializedSongs.forEach((json) {
var song = Song.fromJson(json);
final file = await _downloadManager.getFileFromCache(song.cacheKey);
final file = _localFile(downloadsDir, song);

// a download is only valid if the file is still found in the cache
// (i.e. it hasn't been deleted by the OS)
if (file != null) {
_downloads.add(Download(song: song, file: file));
if (file.existsSync() && !_downloads.any((d) => d.song == song)) {
_downloads.add(Download(song: song, path: file.path));
}
});

_songProvider.syncWithVault(this.songs);
}

File _localFile(String downloadsDir, Song song) {
// just_audio requires a valid extension on iOS
// see https://github.com/ryanheise/just_audio/issues/289
return File('${downloadsDir}/${song.cacheKey}.mp3');
}

get serializedSongKey => '${preferences.host}.${preferences.userEmail}.songs';

Future<void> download({required Song song}) async {
final file = await _downloadManager.downloadFile(
song.sourceUrl,
key: song.cacheKey,
force: true,
);

final download = Download(song: song, file: file);
_songStorage.write(
serializedSongKey,
songs
..add(song)
..toSet()
..toList(),
);

_songDownloaded.add(download);
_downloads.add(download);
var client = HttpClient();
var targetDir = Directory(await downloadsDir);

if (!targetDir.existsSync()) {
targetDir.createSync();
}

var file = _localFile(targetDir.path, song);

if (file.existsSync()) {
try {
file.deleteSync();
} catch (e) {
print(e);
}
}

var request = await client.getUrl(Uri.parse(song.sourceUrl));
var response = await request.close();
List<int> downloadData = [];

response.listen((data) async {
downloadData.addAll(data);
}, onDone: () {
file.writeAsBytesSync(downloadData);
final download = Download(song: song, path: file.path);
_songStorage.write(
serializedSongKey,
songs
..add(song)
..toSet()
..toList(),
);

_songDownloaded.add(download);
_downloads.add(download);
}, onError: (error) {
print(error);
});
}

FileInfo? getForSong(Song song) {
return _downloads.firstWhereOrNull((d) => d.song == song)?.file;
Download? getForSong(Song song) {
return _downloads.firstWhereOrNull((d) => d.song == song);
}

bool has({required Song song}) => getForSong(song) != null;

Future<void> removeForSong(Song song) async {
await _downloadManager.removeFile(song.cacheKey);
_removeSong(song);
_downloadRemoved.add(song);

_downloads.removeWhere((element) => element.song.id == song.id);
Expand All @@ -114,11 +143,23 @@ class DownloadProvider with StreamSubscriber {

Future<void> clear() async {
await Future.forEach<Song>(songs, (song) async {
await _downloadManager.removeFile(song.cacheKey);
await _removeSong(song);
});

_downloadsCleared.add(true);
_downloads.clear();
_songStorage.remove(serializedSongKey);
}

Future<void> _removeSong(Song song) async {
var download = getForSong(song);

if (download == null) return;

try {
await File(download.path).delete();
} catch (e) {
print(e);
}
}
}
5 changes: 4 additions & 1 deletion lib/utils/crypto.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'dart:convert';

import 'package:crypto/crypto.dart' as crypto;

String md5(String input) {
return crypto.md5.convert(utf8.encode(input)).toString();
}

String sha256(String input) {
return crypto.sha256.convert(utf8.encode(input)).toString();
}
2 changes: 1 addition & 1 deletion pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ packages:
source: hosted
version: "0.7.0"
flutter_cache_manager:
dependency: "direct main"
dependency: transitive
description:
name: flutter_cache_manager
sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3"
Expand Down
1 change: 0 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ dependencies:
easy_debounce: ^2.0.0
flutter_html: ^2.1.0
flutter_spinkit: ^5.0.0
flutter_cache_manager: ^3.1.2
crypto: ^3.0.1
intl: ^0.17.0
get_storage: ^2.0.2
Expand Down
2 changes: 1 addition & 1 deletion test/ui/widgets/song_cache_icon_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ void main() {
await _mount(tester);
_assertCacheStatus(hasCache: false);

songCached.add(Download(song: song, file: MockFileInfo()));
songCached.add(Download(song: song, path: MockFileInfo()));
await tester.pumpAndSettle();
_assertCacheStatus(hasCache: true);
});
Expand Down

0 comments on commit 0e94663

Please sign in to comment.