Introduces shared seams for paginated views, D-pad reorder, media control routing, async singletons and the device method channel, then points the open-coded copies at them. Also removes unused models and duplicated provider/server plumbing, folds the twice-implemented artifact store in the server, and factors the repeated Flutter toolchain prologue in CI into a composite action.
78 lines
3.2 KiB
Dart
78 lines
3.2 KiB
Dart
import 'dart:io';
|
|
|
|
import '../database/app_database.dart';
|
|
import '../models/download_models.dart';
|
|
import '../utils/app_logger.dart';
|
|
import '../utils/downloaded_version_match.dart';
|
|
import 'download_storage_service.dart';
|
|
|
|
/// A downloaded copy resolved to a playable location, plus the version that is
|
|
/// actually on disk — which can differ from the requested one when
|
|
/// [resolveDownloadedVideoSource] was allowed to fall back.
|
|
typedef DownloadedVideoSource = ({String path, int mediaIndex, String? mediaSourceId});
|
|
|
|
/// Single source of truth for "where is the playable copy of this downloaded
|
|
/// row, and is it the version that was asked for".
|
|
///
|
|
/// Returns null when the row cannot back playback: the download is not
|
|
/// complete, it holds a different version than requested (unless
|
|
/// [allowAnyDownloadedVersion]), it has no stored video path, or the stored
|
|
/// file is gone from disk.
|
|
///
|
|
/// Version matching is strict by default so online flows keep streaming an
|
|
/// explicitly requested non-downloaded version (issue #1440). With
|
|
/// [allowAnyDownloadedVersion] the downloaded version is returned on mismatch
|
|
/// instead — for offline flows where the alternative is failing outright.
|
|
///
|
|
/// Callers own their own preconditions (profile ownership, how the row was
|
|
/// looked up); this only judges the row itself.
|
|
Future<DownloadedVideoSource?> resolveDownloadedVideoSource(
|
|
DownloadedMediaItem row, {
|
|
int? requestedMediaIndex,
|
|
String? requestedMediaSourceId,
|
|
bool allowAnyDownloadedVersion = false,
|
|
}) async {
|
|
if (row.status != DownloadStatus.completed.index) {
|
|
appLogger.d('Download not complete for ${row.globalKey}. Status: ${row.status}');
|
|
return null;
|
|
}
|
|
|
|
if (!downloadedVersionMatches(
|
|
row,
|
|
requestedMediaIndex: requestedMediaIndex,
|
|
requestedMediaSourceId: requestedMediaSourceId,
|
|
)) {
|
|
if (!allowAnyDownloadedVersion) {
|
|
appLogger.d(
|
|
'[VersionTrace] Downloaded copy of ${row.globalKey} is version ${row.mediaIndex} '
|
|
'(source ${row.mediaSourceId}), but requested version $requestedMediaIndex '
|
|
'(source ${requestedMediaSourceId?.trim()}) — skipping offline',
|
|
);
|
|
return null;
|
|
}
|
|
appLogger.d(
|
|
'[VersionTrace] Requested version $requestedMediaIndex (source ${requestedMediaSourceId?.trim()}) '
|
|
'is not downloaded — falling back to downloaded version ${row.mediaIndex} '
|
|
'(source ${row.mediaSourceId})',
|
|
);
|
|
}
|
|
|
|
final storedPath = row.videoFilePath;
|
|
if (storedPath == null) {
|
|
appLogger.d('Video file path is null for ${row.globalKey}');
|
|
return null;
|
|
}
|
|
|
|
final storageService = DownloadStorageService.instance;
|
|
// SAF URIs (content://) are already playable and come back untouched; file
|
|
// paths may be stored relative, so resolve them and confirm they still exist.
|
|
final readablePath = await storageService.getReadablePath(storedPath);
|
|
if (!storageService.isSafUri(storedPath) && !await File(readablePath).exists()) {
|
|
appLogger.w('Offline video file not found: $readablePath (stored as: $storedPath)');
|
|
return null;
|
|
}
|
|
|
|
appLogger.d('Found offline video: $readablePath');
|
|
return (path: readablePath, mediaIndex: row.mediaIndex, mediaSourceId: row.mediaSourceId);
|
|
}
|