feat(train): the marked playlists downloaded by a WorkManager job, the app open or not (ADR 0030, D52)

The app reads the playlists as before and hands the missing tracks to
MusicWorker.kt, which runs every 6 hours and once whenever the list
changes, on unmetered networks with the battery and storage not low.
MusicFetcher.kt downloads each with the token from JellyfinTokens, works
out its tempo and appends a line to fetched.txt, which the app takes in
as it opens and as each track comes. The app's own download is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-29 18:57:19 +01:00
commit 409a2519f9
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:halkyon_train_flutter/src/feature/music/data/music_platform.dart';
import 'package:halkyon_train_flutter/src/feature/music/data/music_store.dart';
@ -124,20 +126,67 @@ class FakeJellyfin({
});
}
/// A [MusicPlatform] answering [tempos] by the file's name.
class FakeMusicPlatform() implements MusicPlatform {
/// A [MusicPlatform] whose music job runs when a test says ([run]), as
/// `MusicFetcher.kt` does: each track from [jellyfin] into the folder, a
/// line each in `fetched.txt`, its tempo from [tempos] by its ID.
class FakeMusicPlatform([final FakeJellyfin? jellyfin])
implements MusicPlatform {
bool unmeteredNow = true;
final tempos = <String, double>{'t1': 128, 't2': 172.5};
final analysed = <String>[];
/// The job as last scheduled; null where none is.
MusicJob? job;
final _fetched = StreamController<void>.broadcast();
@override
Future<bool> unmetered() async => unmeteredNow;
@override
Future<double?> tempo(String path) async {
final name = path.split('/').last;
analysed.add(name);
return tempos[name];
Future<void> schedule(MusicJob? job) async => this.job = job;
@override
Stream<void> get fetched => _fetched.stream;
/// Runs the job as scheduled, with the token [token].
Future<void> run(String token) async {
final job = this.job;
if (job == null || !unmeteredNow) {
return;
}
final lines = File('${job.folder}/fetched.txt');
final done = {
if (lines.existsSync())
for (final line in lines.readAsLinesSync())
(jsonDecode(line) as Map<String, Object?>)['id'],
};
for (final track in job.tracks) {
if (done.contains(track.id)) {
continue;
}
final response = await jellyfin!.client.get(
Uri.parse('${job.address}${track.path}'),
headers: {'authorization': '${job.client}, Token="$token"'},
);
if (response.statusCode >= 300) {
continue;
}
final file = File('${job.folder}/${track.id}')
..parent.createSync(recursive: true)
..writeAsBytesSync(response.bodyBytes);
if (track.analyse) {
analysed.add(track.id);
}
lines.writeAsStringSync(
'${jsonEncode({
'id': track.id,
'bytes': file.lengthSync(),
if (track.analyse) ...{'tempo': tempos[track.id], 'analysed': true},
})}\n',
mode: FileMode.append,
);
_fetched.add(null);
}
}
}