fixes to scrubbing and offline issues
This commit is contained in:
parent
5bf85a5f44
commit
d558aba246
31 changed files with 296 additions and 33 deletions
|
|
@ -67,19 +67,25 @@ class DownloadInfo {
|
|||
error: error,
|
||||
);
|
||||
|
||||
/// Serialize a completed record. The path is written as *relative* to the
|
||||
/// app-support directory by [DownloadController._persist] (key `relPath`) —
|
||||
/// absolute paths embed the iOS app-container UUID, which changes across app
|
||||
/// updates and would orphan every download. See [DownloadController].
|
||||
Map<String, dynamic> toJson() => {
|
||||
'song': song.toJson(),
|
||||
'path': path,
|
||||
if (bitRate != null) 'bitRate': bitRate,
|
||||
if (format != null) 'format': format,
|
||||
if (sizeBytes != null) 'sizeBytes': sizeBytes,
|
||||
};
|
||||
|
||||
/// Rebuild a completed record from the manifest.
|
||||
factory DownloadInfo.fromJson(Map<String, dynamic> j) => DownloadInfo(
|
||||
/// Rebuild a completed record from the manifest. [path] is the absolute path
|
||||
/// resolved by the controller from the stored relative (or legacy absolute)
|
||||
/// path against the *current* app-support directory.
|
||||
factory DownloadInfo.fromJson(Map<String, dynamic> j, {String? path}) =>
|
||||
DownloadInfo(
|
||||
song: Song.fromJson((j['song'] as Map).cast<String, dynamic>()),
|
||||
status: DownloadStatus.done,
|
||||
path: j['path'] as String?,
|
||||
path: path,
|
||||
bitRate: (j['bitRate'] as num?)?.toInt(),
|
||||
format: j['format'] as String?,
|
||||
sizeBytes: (j['sizeBytes'] as num?)?.toInt(),
|
||||
|
|
@ -139,6 +145,42 @@ class DownloadController extends StateNotifier<DownloadState> {
|
|||
final List<String> _queue = [];
|
||||
int _active = 0;
|
||||
|
||||
/// Cached app-support directory path. Manifests store paths *relative* to
|
||||
/// this so downloads survive the app-container path changing across updates;
|
||||
/// we re-root them against the current directory at load time.
|
||||
String? _supportDirPath;
|
||||
|
||||
Future<String> _supportPath() async =>
|
||||
_supportDirPath ??= (await getApplicationSupportDirectory()).path;
|
||||
|
||||
/// Strip the app-support prefix so the manifest stores a stable relative path
|
||||
/// (`downloads/<key>/<file>`). Falls back to the `downloads/` segment if the
|
||||
/// absolute path doesn't sit under the cached base.
|
||||
String? _relativize(String? absPath) {
|
||||
if (absPath == null) return null;
|
||||
final base = _supportDirPath;
|
||||
if (base != null && absPath.startsWith('$base/')) {
|
||||
return absPath.substring(base.length + 1);
|
||||
}
|
||||
final i = absPath.indexOf('downloads/');
|
||||
return i >= 0 ? absPath.substring(i) : absPath;
|
||||
}
|
||||
|
||||
/// Resolve a manifest entry's stored path to an absolute path under the
|
||||
/// *current* app-support directory. Prefers the new relative `relPath`; for a
|
||||
/// legacy absolute `path` it re-roots the trailing `downloads/...` segment so
|
||||
/// downloads made before this change (or before an app update) still resolve.
|
||||
String? _resolveStoredPath(Map<String, dynamic> j) {
|
||||
final base = _supportDirPath;
|
||||
final rel = j['relPath'] as String?;
|
||||
if (rel != null) return base != null ? '$base/$rel' : rel;
|
||||
final legacy = j['path'] as String?;
|
||||
if (legacy == null) return null;
|
||||
final i = legacy.indexOf('downloads/');
|
||||
if (i >= 0 && base != null) return '$base/${legacy.substring(i)}';
|
||||
return legacy;
|
||||
}
|
||||
|
||||
/// Path to a downloaded file if (and only if) it is fully downloaded — read
|
||||
/// synchronously by the playback stream-URI resolver.
|
||||
String? localPathFor(String id) {
|
||||
|
|
@ -177,6 +219,7 @@ class DownloadController extends StateNotifier<DownloadState> {
|
|||
return;
|
||||
}
|
||||
|
||||
await _supportPath();
|
||||
try {
|
||||
final file = await _manifestFile(key);
|
||||
if (!await file.exists()) {
|
||||
|
|
@ -185,16 +228,23 @@ class DownloadController extends StateNotifier<DownloadState> {
|
|||
}
|
||||
final raw = jsonDecode(await file.readAsString());
|
||||
final byId = <String, DownloadInfo>{};
|
||||
var migrated = false;
|
||||
if (raw is List) {
|
||||
for (final e in raw.whereType<Map>()) {
|
||||
final info = DownloadInfo.fromJson(e.cast<String, dynamic>());
|
||||
final path = info.path;
|
||||
if (path != null && await File(path).exists()) {
|
||||
byId[info.song.id] = info;
|
||||
}
|
||||
final map = e.cast<String, dynamic>();
|
||||
final path = _resolveStoredPath(map);
|
||||
if (path == null || !await File(path).exists()) continue;
|
||||
final info = DownloadInfo.fromJson(map, path: path);
|
||||
byId[info.song.id] = info;
|
||||
// A legacy absolute-path entry re-persists as relative on next save.
|
||||
if (map['relPath'] == null) migrated = true;
|
||||
}
|
||||
}
|
||||
if (gen == _generation) state = DownloadState(byId: byId);
|
||||
if (gen == _generation) {
|
||||
state = DownloadState(byId: byId);
|
||||
// Self-migrate the manifest to relative paths.
|
||||
if (migrated) await _persist();
|
||||
}
|
||||
} catch (_) {
|
||||
if (gen == _generation) state = const DownloadState();
|
||||
}
|
||||
|
|
@ -360,10 +410,16 @@ class DownloadController extends StateNotifier<DownloadState> {
|
|||
final key = _serverKeyGetter();
|
||||
if (key == null) return;
|
||||
try {
|
||||
await _supportPath();
|
||||
final file = await _manifestFile(key);
|
||||
final tmp = File('${file.path}.tmp');
|
||||
await tmp.writeAsString(
|
||||
jsonEncode(state.completed.map((d) => d.toJson()).toList()),
|
||||
jsonEncode(state.completed.map((d) {
|
||||
final j = d.toJson();
|
||||
final rel = _relativize(d.path);
|
||||
if (rel != null) j['relPath'] = rel;
|
||||
return j;
|
||||
}).toList()),
|
||||
);
|
||||
await tmp.rename(file.path);
|
||||
} catch (_) {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue