mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
fix(sourced-track): retry stale cache/auth, fall back on missing presets
Three related resilience gaps in SourcedTrack.fetchFromTrack and URL resolution: - A cached SpotubeAudioSourceMatchObject whose stream resolution now fails (source removed/changed) was never retried; it now deletes the stale sourceMatchTable row, fetches fresh siblings, caches the new match, and retries once. - fetchSiblings returning empty was treated as a hard TrackNotFoundError; it now attempts audioSource.auth.authenticate() once and retries before giving up, via a shared _fetchSiblingsWithRetry helper. - `url` getter and getUrlOfQuality threw/returned null when the user has no configured quality presets, or no stream matches the selected container; both now fall back to sources.firstOrNull?.url instead of crashing. Fixes #3091
This commit is contained in:
parent
69a310c78f
commit
ccf82de13d
@ -13,6 +13,7 @@ import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
|||||||
import 'package:spotube/services/dio/dio.dart';
|
import 'package:spotube/services/dio/dio.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||||
|
import 'package:spotube/services/metadata/metadata.dart';
|
||||||
|
|
||||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
@ -59,7 +60,8 @@ class SourcedTrack extends BasicSourcedTrack {
|
|||||||
.then((s) => s.firstOrNull);
|
.then((s) => s.firstOrNull);
|
||||||
|
|
||||||
if (cachedSource == null) {
|
if (cachedSource == null) {
|
||||||
final siblings = await fetchSiblings(ref: ref, query: query);
|
final siblings =
|
||||||
|
await _fetchSiblingsWithRetry(audioSource, ref, query);
|
||||||
if (siblings.isEmpty) {
|
if (siblings.isEmpty) {
|
||||||
throw TrackNotFoundError(query);
|
throw TrackNotFoundError(query);
|
||||||
}
|
}
|
||||||
@ -86,7 +88,40 @@ class SourcedTrack extends BasicSourcedTrack {
|
|||||||
final item = SpotubeAudioSourceMatchObject.fromJson(
|
final item = SpotubeAudioSourceMatchObject.fromJson(
|
||||||
jsonDecode(cachedSource.sourceInfo),
|
jsonDecode(cachedSource.sourceInfo),
|
||||||
);
|
);
|
||||||
final manifest = await audioSource.audioSource.streams(item);
|
|
||||||
|
List<SpotubeAudioSourceStreamObject> manifest;
|
||||||
|
try {
|
||||||
|
manifest = await audioSource.audioSource.streams(item);
|
||||||
|
} catch (e) {
|
||||||
|
AppLogger.log.w(
|
||||||
|
"Cached source streams failed for ${query.name}, retrying fresh: $e");
|
||||||
|
await (database.sourceMatchTable.delete()
|
||||||
|
..where((s) =>
|
||||||
|
s.trackId.equals(query.id) &
|
||||||
|
s.sourceType.equals(audioSourceConfig.slug)))
|
||||||
|
.go();
|
||||||
|
final freshSiblings =
|
||||||
|
await _fetchSiblingsWithRetry(audioSource, ref, query);
|
||||||
|
if (freshSiblings.isEmpty) {
|
||||||
|
throw TrackNotFoundError(query);
|
||||||
|
}
|
||||||
|
await database.into(database.sourceMatchTable).insert(
|
||||||
|
SourceMatchTableCompanion.insert(
|
||||||
|
trackId: query.id,
|
||||||
|
sourceInfo: Value(jsonEncode(freshSiblings.first)),
|
||||||
|
sourceType: audioSourceConfig.slug,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
manifest = await audioSource.audioSource.streams(freshSiblings.first);
|
||||||
|
return SourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: freshSiblings.skip(1).toList(),
|
||||||
|
info: freshSiblings.first,
|
||||||
|
source: audioSourceConfig.slug,
|
||||||
|
sources: manifest,
|
||||||
|
query: query,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final sourcedTrack = SourcedTrack(
|
final sourcedTrack = SourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
@ -174,6 +209,32 @@ class SourcedTrack extends BasicSourcedTrack {
|
|||||||
return videoResults.toSet().toList();
|
return videoResults.toSet().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<List<SpotubeAudioSourceMatchObject>> _fetchSiblingsWithRetry(
|
||||||
|
MetadataPlugin audioSource,
|
||||||
|
Ref ref,
|
||||||
|
SpotubeFullTrackObject query,
|
||||||
|
) async {
|
||||||
|
List<SpotubeAudioSourceMatchObject> siblings;
|
||||||
|
try {
|
||||||
|
siblings = await fetchSiblings(ref: ref, query: query);
|
||||||
|
} catch (_) {
|
||||||
|
siblings = const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siblings.isEmpty) {
|
||||||
|
AppLogger.log.w(
|
||||||
|
"fetchSiblings returned empty for ${query.name}, attempting re-auth");
|
||||||
|
try {
|
||||||
|
await audioSource.auth.authenticate();
|
||||||
|
siblings = await fetchSiblings(ref: ref, query: query);
|
||||||
|
} catch (_) {
|
||||||
|
siblings = const [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return siblings;
|
||||||
|
}
|
||||||
|
|
||||||
Future<SourcedTrack> copyWithSibling() async {
|
Future<SourcedTrack> copyWithSibling() async {
|
||||||
if (siblings.isNotEmpty) {
|
if (siblings.isNotEmpty) {
|
||||||
return this;
|
return this;
|
||||||
@ -301,10 +362,15 @@ class SourcedTrack extends BasicSourcedTrack {
|
|||||||
String? get url {
|
String? get url {
|
||||||
final preferences = ref.read(audioSourcePresetsProvider);
|
final preferences = ref.read(audioSourcePresetsProvider);
|
||||||
|
|
||||||
|
if (preferences.presets.isEmpty) {
|
||||||
|
return sources.firstOrNull?.url;
|
||||||
|
}
|
||||||
|
|
||||||
return getUrlOfQuality(
|
return getUrlOfQuality(
|
||||||
preferences.presets[preferences.selectedStreamingContainerIndex],
|
preferences.presets[preferences.selectedStreamingContainerIndex],
|
||||||
preferences.selectedStreamingQualityIndex,
|
preferences.selectedStreamingQualityIndex,
|
||||||
);
|
) ??
|
||||||
|
sources.firstOrNull?.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the URL of the track based on the codec and quality preferences.
|
/// Returns the URL of the track based on the codec and quality preferences.
|
||||||
@ -340,9 +406,13 @@ class SourcedTrack extends BasicSourcedTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the preset with closest quality to the supplied quality
|
// Find the preset with closest quality to the supplied quality
|
||||||
return sources.where((source) {
|
final containerMatches =
|
||||||
return source.container == preset.name;
|
sources.where((source) => source.container == preset.name).toList();
|
||||||
}).reduce((prev, curr) {
|
if (containerMatches.isEmpty) {
|
||||||
|
// No stream matches selected container — fall back to any available stream
|
||||||
|
return sources.firstOrNull;
|
||||||
|
}
|
||||||
|
return containerMatches.reduce((prev, curr) {
|
||||||
if (quality is SpotubeAudioLosslessContainerQuality) {
|
if (quality is SpotubeAudioLosslessContainerQuality) {
|
||||||
final prevDiff = ((prev.sampleRate ?? 0) - quality.sampleRate).abs() +
|
final prevDiff = ((prev.sampleRate ?? 0) - quality.sampleRate).abs() +
|
||||||
((prev.bitDepth ?? 0) - quality.bitDepth).abs();
|
((prev.bitDepth ?? 0) - quality.bitDepth).abs();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user