feat: add extension system with skipBuiltInFallback support

- Add extension manager, manifest, runtime, providers, settings
- Add extension provider and UI pages (extensions, detail, priority)
- Add download service picker widget
- Add metadata provider priority page
- Add source field to Track model for extension tracking
- Add skipBuiltInFallback manifest option to skip built-in providers
- Update download queue to use source extension first
- Add extension upgrade support without data loss
This commit is contained in:
zarzet
2026-01-12 00:17:52 +07:00
parent 24ef66be4c
commit 57cb575483
45 changed files with 10491 additions and 1018 deletions
+8
View File
@@ -24,6 +24,8 @@ class AppSettings {
final bool useCustomSpotifyCredentials; // Whether to use custom credentials (if set)
final String metadataSource; // spotify, deezer - source for search and metadata
final bool enableLogging; // Enable detailed logging for debugging
final bool useExtensionProviders; // Use extension providers for downloads when available
final String? searchProvider; // null/empty = default (Deezer/Spotify), otherwise extension ID
const AppSettings({
this.defaultService = 'tidal',
@@ -46,6 +48,8 @@ class AppSettings {
this.useCustomSpotifyCredentials = true, // Default: use custom if set
this.metadataSource = 'deezer', // Default: Deezer (no rate limit)
this.enableLogging = false, // Default: disabled for performance
this.useExtensionProviders = true, // Default: use extensions when available
this.searchProvider, // Default: null (use Deezer/Spotify)
});
AppSettings copyWith({
@@ -69,6 +73,8 @@ class AppSettings {
bool? useCustomSpotifyCredentials,
String? metadataSource,
bool? enableLogging,
bool? useExtensionProviders,
String? searchProvider,
}) {
return AppSettings(
defaultService: defaultService ?? this.defaultService,
@@ -91,6 +97,8 @@ class AppSettings {
useCustomSpotifyCredentials: useCustomSpotifyCredentials ?? this.useCustomSpotifyCredentials,
metadataSource: metadataSource ?? this.metadataSource,
enableLogging: enableLogging ?? this.enableLogging,
useExtensionProviders: useExtensionProviders ?? this.useExtensionProviders,
searchProvider: searchProvider ?? this.searchProvider,
);
}
+4
View File
@@ -28,6 +28,8 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
json['useCustomSpotifyCredentials'] as bool? ?? true,
metadataSource: json['metadataSource'] as String? ?? 'deezer',
enableLogging: json['enableLogging'] as bool? ?? false,
useExtensionProviders: json['useExtensionProviders'] as bool? ?? true,
searchProvider: json['searchProvider'] as String?,
);
Map<String, dynamic> _$AppSettingsToJson(AppSettings instance) =>
@@ -52,4 +54,6 @@ Map<String, dynamic> _$AppSettingsToJson(AppSettings instance) =>
'useCustomSpotifyCredentials': instance.useCustomSpotifyCredentials,
'metadataSource': instance.metadataSource,
'enableLogging': instance.enableLogging,
'useExtensionProviders': instance.useExtensionProviders,
'searchProvider': instance.searchProvider,
};
+5
View File
@@ -18,6 +18,7 @@ class Track {
final String? releaseDate;
final String? deezerId;
final ServiceAvailability? availability;
final String? source; // Extension ID that provided this track (null for built-in sources)
const Track({
required this.id,
@@ -33,10 +34,14 @@ class Track {
this.releaseDate,
this.deezerId,
this.availability,
this.source,
});
factory Track.fromJson(Map<String, dynamic> json) => _$TrackFromJson(json);
Map<String, dynamic> toJson() => _$TrackToJson(this);
/// Check if this track is from an extension
bool get isFromExtension => source != null && source!.isNotEmpty;
}
@JsonSerializable()
+2
View File
@@ -24,6 +24,7 @@ Track _$TrackFromJson(Map<String, dynamic> json) => Track(
: ServiceAvailability.fromJson(
json['availability'] as Map<String, dynamic>,
),
source: json['source'] as String?,
);
Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
@@ -40,6 +41,7 @@ Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
'releaseDate': instance.releaseDate,
'deezerId': instance.deezerId,
'availability': instance.availability,
'source': instance.source,
};
ServiceAvailability _$ServiceAvailabilityFromJson(Map<String, dynamic> json) =>