feat(train): marked playlists kept on the phone, their tempo worked out (ADR 0030, D47)
Music's second part: the marked playlists' tracks are downloaded as Jellyfin keeps them, on unmetered networks only, while the app is open or resumes, and deleted once no marked playlist holds them. Each track's tempo is worked out once on the phone: a BPM tag where the file has one, else a minute decoded with MediaCodec and its spectral flux autocorrelated (Tempo.kt, tested on the JVM). The music screen shows what is kept and deletes it after asking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
3336fe4eca
commit
7f18f001f4
1 changed files with 1676 additions and 24 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:halkyon_train_flutter/src/feature/music/data/music_platform.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/music/data/music_store.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
|
|
@ -21,6 +22,28 @@ class FakeJellyfin({
|
|||
{'Id': 'p2', 'Name': 'Easy', 'MediaType': 'Audio', 'ChildCount': 1},
|
||||
];
|
||||
|
||||
/// The tracks of each playlist, by its ID; one missing is a 404.
|
||||
Map<String, List<Map<String, Object?>>> tracks = {
|
||||
'p1': [track('t1', 'One'), track('t2', 'Two')],
|
||||
'p2': [track('t2', 'Two'), track('t3', 'Three')],
|
||||
};
|
||||
|
||||
/// The tracks whose download fails with a server error.
|
||||
final broken = <String>{};
|
||||
|
||||
/// The tracks downloaded, in order.
|
||||
final downloads = <String>[];
|
||||
|
||||
/// A track as Jellyfin lists it.
|
||||
static Map<String, Object?> track(String id, String name) => {
|
||||
'Id': id,
|
||||
'Name': name,
|
||||
'Type': 'Audio',
|
||||
'AlbumArtist': 'Band',
|
||||
'Album': 'Album',
|
||||
'RunTimeTicks': 1800000000,
|
||||
};
|
||||
|
||||
void approve() => _approved = true;
|
||||
|
||||
void revoke() => _valid.clear();
|
||||
|
|
@ -75,6 +98,24 @@ class FakeJellyfin({
|
|||
return _valid.contains(token)
|
||||
? json({'Items': playlists})
|
||||
: json(null, 401);
|
||||
case _ when request.url.path.startsWith('/Playlists/'):
|
||||
final id = request.url.pathSegments[1];
|
||||
if (!_valid.contains(token)) {
|
||||
return json(null, 401);
|
||||
}
|
||||
return tracks[id] == null
|
||||
? json(null, 404)
|
||||
: json({'Items': tracks[id]});
|
||||
case _ when request.url.path.startsWith('/Audio/'):
|
||||
final id = request.url.pathSegments[1];
|
||||
if (!_valid.contains(token)) {
|
||||
return json(null, 401);
|
||||
}
|
||||
if (broken.contains(id)) {
|
||||
return http.Response('', 500);
|
||||
}
|
||||
downloads.add(id);
|
||||
return http.Response('audio of $id', 200);
|
||||
case 'POST /Sessions/Logout':
|
||||
_valid.remove(token);
|
||||
return http.Response('', 204);
|
||||
|
|
@ -83,6 +124,23 @@ class FakeJellyfin({
|
|||
});
|
||||
}
|
||||
|
||||
/// A [MusicPlatform] answering [tempos] by the file's name.
|
||||
class FakeMusicPlatform() implements MusicPlatform {
|
||||
bool unmeteredNow = true;
|
||||
final tempos = <String, double>{'t1': 128, 't2': 172.5};
|
||||
final analysed = <String>[];
|
||||
|
||||
@override
|
||||
Future<bool> unmetered() async => unmeteredNow;
|
||||
|
||||
@override
|
||||
Future<double?> tempo(String path) async {
|
||||
final name = path.split('/').last;
|
||||
analysed.add(name);
|
||||
return tempos[name];
|
||||
}
|
||||
}
|
||||
|
||||
/// A [JellyfinTokenStore] in memory, with others' tokens in [others].
|
||||
class MemoryJellyfinTokenStore() implements JellyfinTokenStore {
|
||||
String? token;
|
||||
|
|
|
|||
Loading…
Reference in a new issue