Compare commits

...

10 Commits

8 changed files with 483 additions and 256 deletions

View File

@ -1,8 +1,8 @@
import 'dart:core';
import 'package:funkblubber/funkblubber.dart' as funkblubber;
import 'package:funkblubber/funkwhale.dart' as funkblubber;
import 'package:funkblubber/console.dart' as console;
import 'package:funkblubber/parsing/parser.dart' as parser;
import 'package:funkblubber/parser.dart' as parser;
void main(final List<String> arguments) async {
final result = parser.extract(arguments);
@ -11,5 +11,12 @@ void main(final List<String> arguments) async {
return;
}
await funkblubber.download(object: result.object!);
final bool success = await funkblubber.download(
object: result.object!,
path: result.localPath ?? '.',
);
if (!success) {
console.error("couldn't execute request successfully.");
}
}

View File

@ -1,97 +0,0 @@
import 'dart:core';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:funkblubber/funkentity.dart';
import 'package:funkblubber/console.dart' as console;
Future<void> download({
required final FunkObject object,
final String path = '.',
}) async {
switch (object.kind) {
case FunkEntity.album:
await _downloadAlbum(object, path);
return;
case FunkEntity.artist:
await _downloadArtist(object, path);
return;
default:
return;
}
}
Future<void> _downloadArtist(
final FunkObject object,
final String path,
) async {
Response response = await Dio().get(
'https://${object.domain}/api/v1/artists/${object.id}/?'
'refresh=false',
);
final String pathAppend = response.data['name'];
await Directory('$path/$pathAppend').create();
response = await Dio().get(
'https://${object.domain}/api/v1/albums/?'
'artist=${object.id}&ordering=creation_date&'
'page=1&page_size=16&scope=all',
);
for (final albumResponse in response.data['results']) {
_downloadAlbum(
FunkObject(
domain: object.domain,
id: albumResponse['id'].toString(),
kind: FunkEntity.album,
),
'$path/$pathAppend',
);
}
}
Future<void> _downloadAlbum(
final FunkObject object,
final String path,
) async {
final response = await Dio().get(
'https://${object.domain}/api/v1/tracks/?'
'album=${object.id}&ordering=creation_date&'
'page=1&page_size=16&scope=all',
);
final String pathAppend = response.data['results'][0]['album']['title'];
await Directory('$path/$pathAppend').create();
for (final songResponse in response.data['results']) {
final String songTitle = songResponse['title'];
final String ext = songResponse['uploads'][0]['extension'];
_downloadSongObject(
FunkObject(
domain: object.domain,
id: songResponse['listen_url'],
kind: FunkEntity.song,
),
'$path/$pathAppend/$songTitle.$ext',
);
}
}
Future<void> _downloadSongObject(
final FunkObject object,
final String path,
) async {
final Response response = await Dio().get(
'https://${object.domain}${object.id}',
options: Options(
responseType: ResponseType.bytes,
followRedirects: false,
),
);
final File file = File(path);
final accessFile = file.openSync(mode: FileMode.write);
accessFile.writeFromSync(response.data);
await accessFile.close();
}

View File

@ -1,7 +1,5 @@
enum FunkEntity {
error,
song,
track,
album,
artist,
profile,

170
lib/funkwhale.dart Normal file
View File

@ -0,0 +1,170 @@
import 'dart:core';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:funkblubber/funkentity.dart';
import 'package:funkblubber/console.dart' as console;
import 'package:funkblubber/string_utils.dart' as utils;
Future<bool> download({
required final FunkObject object,
required final String path,
}) async {
bool success = false;
console.info('Downloading');
switch (object.kind) {
case FunkEntity.album:
success = await _downloadAlbum(object, path);
break;
case FunkEntity.artist:
success = await _downloadArtist(object, path);
break;
case FunkEntity.track:
success = await _downloadTrack(object, path);
break;
default:
console.info(' nothing...');
break;
}
return success;
}
Future<bool> _downloadArtist(
final FunkObject object,
final String path,
) async {
Response response = await Dio().get(
'https://${object.domain}/api/v1/artists/${object.id}/?'
'refresh=false',
);
final String pathAppend = utils.sanitizePath(response.data['name']);
bool folderCreated = true;
try {
await Directory('$path/$pathAppend').create();
} catch (e) {
console.error(e.toString());
folderCreated = false;
}
if (!folderCreated) {
return false;
}
console.info('Artist: $pathAppend');
response = await Dio().get(
'https://${object.domain}/api/v1/albums/?'
'artist=${object.id}&ordering=creation_date&'
'page=1&page_size=16&scope=all',
);
final List<Future<bool>> results = [];
for (final albumResponse in response.data['results']) {
final result = _downloadAlbum(
FunkObject(
domain: object.domain,
id: albumResponse['id'].toString(),
kind: FunkEntity.album,
),
'$path/$pathAppend',
);
results.add(result);
}
final List<bool> successes = await Future.wait(results);
return successes.every((final success) => success);
}
Future<bool> _downloadAlbum(
final FunkObject object,
final String path,
) async {
final response = await Dio().get(
'https://${object.domain}/api/v1/tracks/?'
'album=${object.id}&ordering=creation_date&'
'page=1&page_size=16&scope=all',
);
final String pathAppend = utils.sanitizePath(
response.data['results'][0]['album']['title'],
);
bool folderCreated = true;
try {
await Directory('$path/$pathAppend').create();
} catch (e) {
console.error(e.toString());
folderCreated = false;
}
if (!folderCreated) {
return false;
}
console.info('Album: $pathAppend');
final List<Future<bool>> results = [];
for (final songResponse in response.data['results']) {
final String songTitle = utils.sanitizePath(songResponse['title']);
final String ext = songResponse['uploads'][0]['extension'];
final result = _downloadTrackObject(
FunkObject(
domain: object.domain,
id: songResponse['listen_url'],
kind: FunkEntity.track,
),
'$path/$pathAppend/$songTitle.$ext',
);
results.add(result);
}
final List<bool> successes = await Future.wait(results);
return successes.every((final success) => success);
}
Future<bool> _downloadTrack(
final FunkObject object,
final String path,
) async {
final response = await Dio().get(
'https://${object.domain}/api/v1/tracks/${object.id}/?refresh=false',
);
final String songTitle = utils.sanitizePath(response.data['title']);
final String ext = response.data['uploads'][0]['extension'];
return _downloadTrackObject(
FunkObject(
domain: object.domain,
id: response.data['uploads'][0]['listen_url'],
kind: FunkEntity.track,
),
'$path/$songTitle.$ext',
);
}
Future<bool> _downloadTrackObject(
final FunkObject object,
final String path,
) async {
bool success = true;
try {
final Response response = await Dio().get(
'https://${object.domain}${object.id}',
options: Options(
responseType: ResponseType.bytes,
followRedirects: false,
),
);
console.info('- $path');
final File file = File(path);
final accessFile = file.openSync(mode: FileMode.write);
accessFile.writeFromSync(response.data);
await accessFile.close();
} catch (e) {
console.error(e.toString());
success = false;
}
return success;
}

View File

@ -1,11 +1,46 @@
import 'package:funkblubber/funkentity.dart';
import 'package:funkblubber/console.dart' as console;
import 'package:funkblubber/string_utils.dart' as utils;
FunkObject extract(final List<String> args) {
FunkObject result = FunkObject(
kind: FunkEntity.error,
id: '',
domain: '',
enum Action {
download,
upload,
}
enum ParsingStage {
nothing,
album,
artist,
track,
path,
upload,
domain,
}
class StageResult {
StageResult({required this.result, required this.stage});
final ParseResult result;
final ParsingStage stage;
}
class ParseResult {
ParseResult({
required this.success,
required this.action,
this.object,
this.localPath,
});
final FunkObject? object;
final String? localPath;
final Action action;
final bool success;
}
ParseResult extract(final List<String> args) {
ParseResult result = ParseResult(
success: false,
action: Action.download,
);
if (args.isEmpty) {
@ -13,26 +48,227 @@ FunkObject extract(final List<String> args) {
return result;
}
ParsingStage currentStage = ParsingStage.nothing;
for (final String arg in args) {
switch (currentStage) {
case ParsingStage.nothing:
final stageResult = _onNothingStage(arg, result, currentStage);
currentStage = stageResult.stage;
result = stageResult.result;
break;
case ParsingStage.album:
final stageResult = _onEntityStage(
arg,
result,
currentStage,
FunkEntity.album,
);
currentStage = stageResult.stage;
result = stageResult.result;
break;
case ParsingStage.artist:
final stageResult = _onEntityStage(
arg,
result,
currentStage,
FunkEntity.artist,
);
currentStage = stageResult.stage;
result = stageResult.result;
break;
case ParsingStage.track:
final stageResult = _onEntityStage(
arg,
result,
currentStage,
FunkEntity.track,
);
currentStage = stageResult.stage;
result = stageResult.result;
break;
case ParsingStage.domain:
final stageResult = _onDomainStage(arg, result, currentStage);
currentStage = stageResult.stage;
result = stageResult.result;
break;
case ParsingStage.path:
final stageResult = _onPathStage(arg, result, currentStage);
currentStage = stageResult.stage;
result = stageResult.result;
break;
default:
console.error('not implemented yet');
break;
}
}
return result;
}
void _onHelpStage() {
final String help = '''
usage: funkblubber [[-OPTIONS|URL]...]
A simple CLI for interaction with your Funkwhale account.
Options:
[-a|--album ID] Provide with album ID to download it.
[-A|--artist ID] Provide with artist ID to download their albums.
[-t|--track ID] Provide with track ID to download a single track entity.
[-p|--path PATH] Provide to explicitly define download directory.
Assumed '.'.
[-d|--domain DOMAIN] Provide to explicitly define the host where your
Funkwhale instance is. Required with -a, -A, and -u.
Is not required if you download by a full URL.
Examples:
funkblubber https://my.funkwhale/library/tracks/10244/
Download album by URL to current folder.
funkblubber https://my.funkwhale/library/tracks/10244/ -p music
Download album by URL to local ./music folder.
funkblubber -A 152 -d my.funkwhale -p media/music/
Download everything by artist with ID 152 from a funkwhale instance
with my.funkwhale domain and save it to local ./media/music folder.
funkblubber --album 1423 -d my.funkwhale
Download album with ID 1423 from a funkwhale instance
with my.funkwhale domain and save it to current folder.
''';
console.info(help);
}
StageResult _onEntityStage(
final String arg,
final ParseResult previousResult,
final ParsingStage previousStage,
final FunkEntity kind,
) {
ParsingStage currentStage = previousStage;
ParseResult result = previousResult;
if (int.tryParse(arg) != null) {
currentStage = ParsingStage.nothing;
result = ParseResult(
action: previousResult.action,
success: true,
object: FunkObject(
domain: previousResult.object?.domain ?? '',
id: arg,
kind: kind,
),
);
}
return StageResult(result: result, stage: currentStage);
}
StageResult _onDomainStage(
final String arg,
final ParseResult previousResult,
final ParsingStage previousStage,
) {
ParsingStage currentStage = previousStage;
ParseResult result = previousResult;
try {
final Uri uri = Uri.parse(args[0]);
final Uri uri = Uri.parse(arg);
currentStage = ParsingStage.nothing;
result = ParseResult(
action: previousResult.action,
success: true,
object: FunkObject(
domain: uri.toString(),
id: previousResult.object?.id ?? '',
kind: previousResult.object?.kind ?? FunkEntity.album,
),
);
} catch (e) {
console.error(e.toString());
}
return StageResult(result: result, stage: currentStage);
}
StageResult _onPathStage(
final String arg,
final ParseResult previousResult,
final ParsingStage previousStage,
) {
ParsingStage currentStage = previousStage;
ParseResult result = previousResult;
currentStage = ParsingStage.nothing;
result = ParseResult(
localPath: utils.cutTrailingDash(arg),
action: previousResult.action,
success: true,
object: previousResult.object,
);
return StageResult(result: result, stage: currentStage);
}
ParseResult _makeParseResultFromEntityInfo(
final FunkEntity kind,
final String host,
final String id,
) =>
ParseResult(
action: Action.download,
success: true,
object: FunkObject(
kind: kind,
id: id,
domain: host,
),
);
ParseResult _parseUrl(final String url, final ParseResult previousResult) {
ParseResult result = previousResult;
try {
final Uri uri = Uri.parse(url);
final segments = uri.pathSegments;
for (int i = 0; i < segments.length; ++i) {
switch (segments[i]) {
case 'artists':
result = FunkObject(
kind: FunkEntity.artist,
id: segments[i + 1],
domain: uri.host,
result = _makeParseResultFromEntityInfo(
FunkEntity.artist,
uri.host,
segments[i + 1],
);
++i;
break;
case 'tracks':
result = _makeParseResultFromEntityInfo(
FunkEntity.track,
uri.host,
segments[i + 1],
);
++i;
break;
case 'albums':
result = FunkObject(
kind: FunkEntity.album,
id: segments[i + 1],
domain: uri.host,
result = _makeParseResultFromEntityInfo(
FunkEntity.album,
uri.host,
segments[i + 1],
);
++i;
break;
@ -40,12 +276,52 @@ FunkObject extract(final List<String> args) {
}
} catch (e) {
console.error(e.toString());
result = FunkObject(
kind: FunkEntity.error,
id: '',
domain: '',
);
}
return result;
}
StageResult _onNothingStage(
final String arg,
final ParseResult previousResult,
final ParsingStage previousStage,
) {
ParsingStage currentStage = previousStage;
ParseResult result = previousResult;
switch (arg) {
case '-A':
case '--artist':
currentStage = ParsingStage.artist;
break;
case '-a':
case '--album':
currentStage = ParsingStage.album;
break;
case '-t':
case '--track':
currentStage = ParsingStage.track;
break;
case '-u':
case '--upload':
currentStage = ParsingStage.upload;
break;
case '-p':
case '--path':
currentStage = ParsingStage.path;
break;
case '-d':
case '--domain':
currentStage = ParsingStage.domain;
break;
case '-h':
case '--help':
_onHelpStage();
currentStage = ParsingStage.nothing;
break;
default:
result = _parseUrl(arg, result);
break;
}
return StageResult(result: result, stage: currentStage);
}

View File

@ -1,125 +0,0 @@
import 'package:funkblubber/funkentity.dart';
import 'package:funkblubber/console.dart' as console;
import 'package:funkblubber/parsing/parsing_stage.dart';
enum Action {
download,
upload,
}
class StageResult {
StageResult({required this.result, required this.stage});
final ParseResult result;
final ParsingStage stage;
}
class ParseResult {
ParseResult({
required this.success,
required this.action,
this.object,
this.localPath,
});
final FunkObject? object;
final String? localPath;
final Action action;
final bool success;
}
ParseResult extract(final List<String> args) {
ParseResult result = ParseResult(
success: false,
action: Action.download,
);
if (args.isEmpty) {
console.error('no arguments provided');
return result;
}
ParsingStage currentStage = ParsingStage.nothing;
for (final String arg in args) {
switch (currentStage) {
case ParsingStage.nothing:
final stageResult = _onNothingStage(arg, result, currentStage);
currentStage = stageResult.stage;
result = stageResult.result;
break;
default:
console.error('not implemented yet');
break;
}
}
return result;
}
StageResult _onNothingStage(
final String arg,
final ParseResult previousResult,
final ParsingStage previousStage,
) {
ParsingStage currentStage = previousStage;
ParseResult result = previousResult;
switch (arg) {
case '-A':
case '--artist':
currentStage = ParsingStage.artist;
break;
case '-a':
case '--album':
currentStage = ParsingStage.album;
break;
case '-u':
case '--upload':
currentStage = ParsingStage.upload;
break;
case '-p':
case '--path':
currentStage = ParsingStage.path;
break;
default:
try {
final Uri uri = Uri.parse(arg);
final segments = uri.pathSegments;
for (int i = 0; i < segments.length; ++i) {
switch (segments[i]) {
case 'artists':
result = ParseResult(
action: Action.download,
success: true,
object: FunkObject(
kind: FunkEntity.artist,
id: segments[i + 1],
domain: uri.host,
),
);
++i;
break;
case 'albums':
result = ParseResult(
action: Action.download,
success: true,
object: FunkObject(
kind: FunkEntity.album,
id: segments[i + 1],
domain: uri.host,
),
);
++i;
break;
}
}
} catch (e) {
console.error(e.toString());
currentStage = ParsingStage.nothing;
}
break;
}
return StageResult(result: result, stage: currentStage);
}

View File

@ -1,8 +0,0 @@
enum ParsingStage {
nothing,
album,
artist,
song,
path,
upload,
}

6
lib/string_utils.dart Normal file
View File

@ -0,0 +1,6 @@
String cutTrailingDash(final String path) =>
path.endsWith('/') && path.length > 1
? path.substring(0, path.length - 1)
: path;
String sanitizePath(final String rawPath) => rawPath.replaceAll('/', '\\');