Compare commits

..

2 commits

Author SHA1 Message Date
Pato05 3f9c9d5027
merge 2024-03-31 19:29:50 +02:00
Pato05 1257a81e90
work on download backend + try fix ios 2024-03-31 19:29:06 +02:00
9 changed files with 124 additions and 49 deletions

View file

@ -538,9 +538,8 @@ class DownloadManager {
results.playlists!.add((await getPlaylist(playlist['id']))); results.playlists!.add((await getPlaylist(playlist['id'])));
} }
return results; return results;
} } //Sanitize filename
//Sanitize filename
String sanitize(String input) { String sanitize(String input) {
RegExp sanitize = RegExp(r'[\/\\\?\%\*\:\|\"\<\>]'); RegExp sanitize = RegExp(r'[\/\\\?\%\*\:\|\"\<\>]');
return input.replaceAll(sanitize, ''); return input.replaceAll(sanitize, '');
@ -575,8 +574,10 @@ class DownloadManager {
} }
} }
//Final path //Final path
path = p.join(path!, path = p.join(
isSingleton ? settings.singletonFilename : settings.downloadFilename); path!,
isSingleton ? settings.singletonFilename : settings.downloadFilename,
);
//Playlist track number variable (not accessible in service) //Playlist track number variable (not accessible in service)
if (playlistTrackNumber != null) { if (playlistTrackNumber != null) {
path = path.replaceAll( path = path.replaceAll(

View file

@ -1,9 +1,27 @@
import 'package:freezer/api/definitions.dart' as d; import 'package:freezer/api/definitions.dart' as d;
import 'package:freezer/api/definitions.dart' show AlbumType; import 'package:freezer/api/definitions.dart' show AlbumType;
import 'package:freezer/api/paths.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
part 'database.g.dart'; part 'database.g.dart';
class Database {
static Future<Isar> open() async => Isar.open(
[
// collections
TrackSchema,
AlbumSchema,
ArtistSchema,
PlaylistSchema,
],
directory: await Paths.dataDirectory(),
name: 'offline',
);
static String sanitize(String input) =>
input.replaceAll(RegExp(r'[\/\\\?\%\*\:\|\"\<\>]'), '');
}
@collection @collection
class Track { class Track {
Id get isarId => int.parse(id); Id get isarId => int.parse(id);

View file

@ -6,15 +6,16 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart' as d; import 'package:freezer/api/definitions.dart' as d;
import 'package:freezer/api/download_manager/database.dart'; import 'package:freezer/api/download/database.dart';
import 'package:freezer/api/download_manager/download_service.dart'; import 'package:freezer/api/download/service.dart';
import 'package:freezer/api/download_manager/service_interface.dart'; import 'package:freezer/api/download/service_interface.dart';
import 'package:freezer/api/paths.dart'; import 'package:freezer/api/paths.dart';
import 'package:freezer/main.dart'; import 'package:freezer/main.dart';
import 'package:freezer/settings.dart'; import 'package:freezer/settings.dart';
import 'package:freezer/translations.i18n.dart'; import 'package:freezer/translations.i18n.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import '../download.dart' as dl; import '../download.dart' as dl;
import 'package:path/path.dart' as p;
class DownloadManager { class DownloadManager {
//implements dl.DownloadManager { //implements dl.DownloadManager {
@ -36,17 +37,7 @@ class DownloadManager {
} }
Future<bool> configure() async { Future<bool> configure() async {
_isar = await Isar.open( _isar = await Database.open();
[
// collections
TrackSchema,
AlbumSchema,
ArtistSchema,
PlaylistSchema,
],
directory: await Paths.dataDirectory(),
name: 'offline',
);
if (Platform.isAndroid || Platform.isIOS) { if (Platform.isAndroid || Platform.isIOS) {
return FlutterBackgroundService().configure( return FlutterBackgroundService().configure(
iosConfiguration: IosConfiguration(), // fuck ios iosConfiguration: IosConfiguration(), // fuck ios
@ -123,16 +114,16 @@ class DownloadManager {
} }
Future<bool> addOfflineTrack(d.Track track, AudioQuality downloadQuality, Future<bool> addOfflineTrack(d.Track track, AudioQuality downloadQuality,
{bool private = true}) async { {bool offline = true, bool isSingleton = false}) async {
//Permission //Permission
if (!private && !(await dl.DownloadManager.checkCanDownload())) { if (!offline && !(await dl.DownloadManager.checkCanDownload())) {
return false; return false;
} }
if (downloadQuality == AudioQuality.ASK) { if (downloadQuality == AudioQuality.ASK) {
throw Exception('Invalid quality.'); throw Exception('Invalid quality.');
} }
if (private) { if (offline) {
if (track.artists == null || if (track.artists == null ||
track.artists!.isEmpty || track.artists!.isEmpty ||
track.album == null) { track.album == null) {
@ -142,18 +133,6 @@ class DownloadManager {
// cache album art // cache album art
cacheManager.getSingleFile(track.albumArt!.thumb); cacheManager.getSingleFile(track.albumArt!.thumb);
cacheManager.getSingleFile(track.albumArt!.full); cacheManager.getSingleFile(track.albumArt!.full);
await _isar.writeTxn(() async {
await _isar.tracks.put(Track.from(track));
if (track.album != null) {
await _isar.albums.put(Album.from(track.album!));
}
if (track.artists != null) {
await _isar.artists
.putAll(track.artists!.map(Artist.from).toList(growable: false));
}
});
} }
// logic for downloading the track // logic for downloading the track
@ -161,12 +140,52 @@ class DownloadManager {
'addDownloads', 'addDownloads',
DownloadInfo( DownloadInfo(
trackId: track.id, trackId: track.id,
path: private ? null : settings.downloadPath, path: offline ? null : settings.downloadPath,
).toJson()); ).toJson());
return true; return true;
} }
//Generate track download path
String? _generatePath(Track track, bool offline,
{String? playlistName,
int? playlistTrackNumber,
bool isSingleton = false}) {
var path = settings.downloadPath!;
if (settings.playlistFolder && playlistName != null) {
path = p.join(path, Database.sanitize(playlistName));
}
if (settings.artistFolder) path = p.join(path, '%albumArtist%');
//Album folder / with disk number
if (settings.albumFolder) {
if (settings.albumDiscFolder) {
path = p.join(path, '%album% - Disk ${track.diskNumber ?? 1}');
} else {
path = p.join(path, '%album%');
}
}
//Final path
path = p.join(
path,
isSingleton ? settings.singletonFilename : settings.downloadFilename,
);
//Playlist track number variable (not accessible in service)
if (playlistTrackNumber != null) {
path = path.replaceAll(
'%playlistTrackNumber%', playlistTrackNumber.toString());
path = path.replaceAll('%0playlistTrackNumber%',
playlistTrackNumber.toString().padLeft(2, '0'));
} else {
path = path.replaceAll('%playlistTrackNumber%', '');
path = path.replaceAll('%0playlistTrackNumber%', '');
}
return path;
}
static void _startNative(ServiceInstance service) => static void _startNative(ServiceInstance service) =>
_startService(ServiceInterface(service: service)); _startService(ServiceInterface(service: service));

View file

@ -3,15 +3,18 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/deezer_audio.dart'; import 'package:freezer/api/deezer_audio.dart';
import 'package:freezer/api/definitions.dart' as d;
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/api/download_manager/service_interface.dart'; import 'package:freezer/api/download/database.dart';
import 'package:freezer/api/download/service_interface.dart';
import 'package:freezer/main.dart'; import 'package:freezer/main.dart';
import 'package:freezer/settings.dart'; import 'package:freezer/settings.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:isar/isar.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
part 'download_service.freezed.dart'; part 'service.freezed.dart';
part 'download_service.g.dart'; part 'service.g.dart';
class DownloadService { class DownloadService {
static const NOTIFICATION_ID = 6969; static const NOTIFICATION_ID = 6969;
@ -28,6 +31,8 @@ class DownloadService {
late DeezerAPI _deezerAPI; late DeezerAPI _deezerAPI;
late final Isar _isar;
void run() async { void run() async {
service.on('addDownloads', (event) { service.on('addDownloads', (event) {
downloadTrack(DownloadInfo.fromJson(event!)); downloadTrack(DownloadInfo.fromJson(event!));
@ -57,25 +62,24 @@ class DownloadService {
} }
}); });
_isar = await Database.open();
_deezerAPI = DeezerAPI(await getCookieJar()); _deezerAPI = DeezerAPI(await getCookieJar());
await _deezerAPI.authorize();
service.send('ready'); service.send('ready');
} }
void downloadTrack(DownloadInfo info) async { void downloadTrack(DownloadInfo info) async {
await _deezerAPI.authorize();
final trackId = info.trackId; final trackId = info.trackId;
final deezerAudio = DeezerAudio( final deezerAudio = DeezerAudio(
deezerAPI: _deezerAPI, deezerAPI: _deezerAPI,
quality: _downloadQuality ?? AudioQuality.MP3_128, quality: _downloadQuality ?? AudioQuality.MP3_128,
trackId: trackId); trackId: trackId);
bool offline = info.path == null;
final track = await _deezerAPI.track(trackId); final track = await _deezerAPI.track(trackId);
final file = File(join( final file = File(fillVariables(track, info.path!));
info.path!,
downloadFilename
.replaceAll('%artist%', track.artists?.join(',') ?? '')
.replaceAll('%title%', track.title!)));
print('downloading to ${file.path}'); print('downloading to ${file.path}');
final Uri uri; final Uri uri;
if (useGetURL) { if (useGetURL) {
@ -90,6 +94,18 @@ class DownloadService {
uri = res.uri; uri = res.uri;
} }
await _isar.writeTxn(() async {
await _isar.tracks.put(Track.from(track));
if (track.album != null) {
await _isar.albums.put(Album.from(track.album!));
}
if (track.artists != null) {
await _isar.artists
.putAll(track.artists!.map(Artist.from).toList(growable: false));
}
});
// download url and decrypt // download url and decrypt
final req = await _deezerAPI.dio.get(uri.toString(), final req = await _deezerAPI.dio.get(uri.toString(),
options: Options(responseType: ResponseType.bytes)); options: Options(responseType: ResponseType.bytes));
@ -99,6 +115,26 @@ class DownloadService {
await stream.pipe(fWrite); await stream.pipe(fWrite);
print('download complete!'); print('download complete!');
} }
String pathForOffline(d.Track t) {
// TODO: implement
throw Exception();
}
String fillVariables(d.Track t, String path) {
return path
.replaceAll('%title%', Database.sanitize(t.title!))
.replaceAll('%album%', t.album!.title!)
.replaceAll('%artist%', t.artists?[0].name ?? '')
.replaceAll('%albumArtist%',
Database.sanitize(t.album?.artistString ?? t.artistString))
.replaceAll('%artists%', t.artistString)
.replaceAll('%feats%',
t.artists?.sublist(1).map((e) => e.name!).join(', ') ?? '')
.replaceAll('%trackNumber%', t.trackNumber!.toString())
.replaceAll('%0trackNumber%', t.trackNumber.toString().padLeft(2, '0'))
.replaceAll('%year%', '0'); // TODO: find year
}
} }
@freezed @freezed

View file

@ -47,6 +47,8 @@ class Paths {
.path; .path;
} }
return target.path; return target.path;
case TargetPlatform.iOS:
return (await getApplicationSupportDirectory()).path;
default: default:
return (await getApplicationDocumentsDirectory()).path; return (await getApplicationDocumentsDirectory()).path;
} }

View file

@ -131,6 +131,7 @@ void main() async {
GetIt.instance.registerSingleton<CookieJar>(await getCookieJar()); GetIt.instance.registerSingleton<CookieJar>(await getCookieJar());
final deezerAPI = final deezerAPI =
GetIt.instance.registerSingleton<DeezerAPI>(DeezerAPI(cookieJar)); GetIt.instance.registerSingleton<DeezerAPI>(DeezerAPI(cookieJar));
deezerAPI.deezerCountry = settings.deezerCountry; deezerAPI.deezerCountry = settings.deezerCountry;
deezerAPI.deezerLanguage = settings.deezerLanguage; deezerAPI.deezerLanguage = settings.deezerLanguage;
deezerAPI.favoritesPlaylistId = cache.favoritesPlaylistId; deezerAPI.favoritesPlaylistId = cache.favoritesPlaylistId;
@ -141,7 +142,7 @@ void main() async {
'${record.level.name}: ${record.time}: [${record.loggerName}] ${record.message}'); '${record.level.name}: ${record.time}: [${record.loggerName}] ${record.message}');
}); });
if (kDebugMode || true) { if (kDebugMode) {
Logger.root.level = Level.ALL; Logger.root.level = Level.ALL;
} }

View file

@ -17,7 +17,6 @@ import 'package:freezer/ui/cached_image.dart';
import 'package:numberpicker/numberpicker.dart'; import 'package:numberpicker/numberpicker.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:freezer/api/download_manager/download_manager.dart' as newDl;
class SliverTrackPersistentHeader extends SliverPersistentHeaderDelegate { class SliverTrackPersistentHeader extends SliverPersistentHeaderDelegate {
final Track track; final Track track;
@ -297,7 +296,7 @@ class MenuSheet {
Text('Download'.i18n), Text('Download'.i18n),
icon: const Icon(Icons.file_download), icon: const Icon(Icons.file_download),
onTap: () async { onTap: () async {
final dl = newDl.DownloadManager(); // final dl = newDl.DownloadManager();
// await dl.startDebug(); // await dl.startDebug();
// dl.addOfflineTrack(t, settings.downloadQuality, private: false); // dl.addOfflineTrack(t, settings.downloadQuality, private: false);
if (await downloadManager.addOfflineTrack(t, if (await downloadManager.addOfflineTrack(t,

View file

@ -1028,8 +1028,7 @@ class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
TextButton( TextButton(
child: Text('Reset'.i18n), child: Text('Reset'.i18n),
onPressed: () { onPressed: () {
_controller!.value = _controller!.text = '%artist% - %title%';
_controller!.value.copyWith(text: '%artist% - %title%');
_new = '%artist% - %title%'; _new = '%artist% - %title%';
}, },
), ),