From 439de5d7f7b61492e5cb6dfa4e8f3439993f4a85 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 19 Oct 2025 13:48:53 +0600 Subject: [PATCH 01/14] feat: add plugin audio source models and api service --- lib/models/metadata/audio_source.dart | 84 + lib/models/metadata/metadata.dart | 1 + lib/models/metadata/metadata.freezed.dart | 1527 ++++++++++++++++- lib/models/metadata/metadata.g.dart | 125 +- lib/models/metadata/plugin.dart | 11 +- .../metadata_plugin_provider.dart | 9 +- .../metadata/endpoints/audio_source.dart | 38 + lib/services/metadata/metadata.dart | 63 +- pubspec.lock | 13 +- pubspec.yaml | 3 +- 10 files changed, 1827 insertions(+), 47 deletions(-) create mode 100644 lib/models/metadata/audio_source.dart create mode 100644 lib/services/metadata/endpoints/audio_source.dart diff --git a/lib/models/metadata/audio_source.dart b/lib/models/metadata/audio_source.dart new file mode 100644 index 00000000..c429ec74 --- /dev/null +++ b/lib/models/metadata/audio_source.dart @@ -0,0 +1,84 @@ +part of 'metadata.dart'; + +enum SpotubeMediaCompressionType { + lossy, + lossless, +} + +@Freezed(unionKey: 'type') +class SpotubeAudioSourceContainerPreset + with _$SpotubeAudioSourceContainerPreset { + @FreezedUnionValue("lossy") + factory SpotubeAudioSourceContainerPreset.lossy({ + required SpotubeMediaCompressionType type, + required String name, + required List qualities, + }) = SpotubeAudioSourceContainerPresetLossy; + + @FreezedUnionValue("lossless") + factory SpotubeAudioSourceContainerPreset.lossless({ + required SpotubeMediaCompressionType type, + required String name, + required List qualities, + }) = SpotubeAudioSourceContainerPresetLossless; + + factory SpotubeAudioSourceContainerPreset.fromJson( + Map json) => + _$SpotubeAudioSourceContainerPresetFromJson(json); +} + +@freezed +class SpotubeAudioLossyContainerQuality + with _$SpotubeAudioLossyContainerQuality { + factory SpotubeAudioLossyContainerQuality({ + required double bitrate, + }) = _SpotubeAudioLossyContainerQuality; + + factory SpotubeAudioLossyContainerQuality.fromJson( + Map json) => + _$SpotubeAudioLossyContainerQualityFromJson(json); +} + +@freezed +class SpotubeAudioLosslessContainerQuality + with _$SpotubeAudioLosslessContainerQuality { + factory SpotubeAudioLosslessContainerQuality({ + required int bitDepth, + required double sampleRate, + }) = _SpotubeAudioLosslessContainerQuality; + + factory SpotubeAudioLosslessContainerQuality.fromJson( + Map json) => + _$SpotubeAudioLosslessContainerQualityFromJson(json); +} + +@freezed +class SpotubeAudioSourceMatchObject with _$SpotubeAudioSourceMatchObject { + factory SpotubeAudioSourceMatchObject({ + required String id, + required String title, + required List artists, + required Duration duration, + String? thumbnail, + required String externalUri, + }) = _SpotubeAudioSourceMatchObject; + + factory SpotubeAudioSourceMatchObject.fromJson(Map json) => + _$SpotubeAudioSourceMatchObjectFromJson(json); +} + +@freezed +class SpotubeAudioSourceStreamObject with _$SpotubeAudioSourceStreamObject { + factory SpotubeAudioSourceStreamObject({ + required String url, + required String container, + required SpotubeMediaCompressionType type, + String? codec, + double? bitrate, + int? bitDepth, + double? sampleRate, + }) = _SpotubeAudioSourceStreamObject; + + factory SpotubeAudioSourceStreamObject.fromJson(Map json) => + _$SpotubeAudioSourceStreamObjectFromJson(json); +} diff --git a/lib/models/metadata/metadata.dart b/lib/models/metadata/metadata.dart index 97da704c..4c6eb2ac 100644 --- a/lib/models/metadata/metadata.dart +++ b/lib/models/metadata/metadata.dart @@ -15,6 +15,7 @@ import 'package:spotube/utils/primitive_utils.dart'; part 'metadata.g.dart'; part 'metadata.freezed.dart'; +part 'audio_source.dart'; part 'album.dart'; part 'artist.dart'; part 'browse.dart'; diff --git a/lib/models/metadata/metadata.freezed.dart b/lib/models/metadata/metadata.freezed.dart index bb4cf3f8..5d4bc695 100644 --- a/lib/models/metadata/metadata.freezed.dart +++ b/lib/models/metadata/metadata.freezed.dart @@ -14,6 +14,1502 @@ T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); +SpotubeAudioSourceContainerPreset _$SpotubeAudioSourceContainerPresetFromJson( + Map json) { + switch (json['type']) { + case 'lossy': + return SpotubeAudioSourceContainerPresetLossy.fromJson(json); + case 'lossless': + return SpotubeAudioSourceContainerPresetLossless.fromJson(json); + + default: + throw CheckedFromJsonException( + json, + 'type', + 'SpotubeAudioSourceContainerPreset', + 'Invalid union type "${json['type']}"!'); + } +} + +/// @nodoc +mixin _$SpotubeAudioSourceContainerPreset { + SpotubeMediaCompressionType get type => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + List get qualities => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossy, + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossless, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(SpotubeAudioSourceContainerPresetLossy value) + lossy, + required TResult Function(SpotubeAudioSourceContainerPresetLossless value) + lossless, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult? Function(SpotubeAudioSourceContainerPresetLossless value)? + lossless, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult Function(SpotubeAudioSourceContainerPresetLossless value)? lossless, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + /// Serializes this SpotubeAudioSourceContainerPreset to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SpotubeAudioSourceContainerPresetCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpotubeAudioSourceContainerPresetCopyWith<$Res> { + factory $SpotubeAudioSourceContainerPresetCopyWith( + SpotubeAudioSourceContainerPreset value, + $Res Function(SpotubeAudioSourceContainerPreset) then) = + _$SpotubeAudioSourceContainerPresetCopyWithImpl<$Res, + SpotubeAudioSourceContainerPreset>; + @useResult + $Res call({SpotubeMediaCompressionType type, String name}); +} + +/// @nodoc +class _$SpotubeAudioSourceContainerPresetCopyWithImpl<$Res, + $Val extends SpotubeAudioSourceContainerPreset> + implements $SpotubeAudioSourceContainerPresetCopyWith<$Res> { + _$SpotubeAudioSourceContainerPresetCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? name = null, + }) { + return _then(_value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as SpotubeMediaCompressionType, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SpotubeAudioSourceContainerPresetLossyImplCopyWith<$Res> + implements $SpotubeAudioSourceContainerPresetCopyWith<$Res> { + factory _$$SpotubeAudioSourceContainerPresetLossyImplCopyWith( + _$SpotubeAudioSourceContainerPresetLossyImpl value, + $Res Function(_$SpotubeAudioSourceContainerPresetLossyImpl) then) = + __$$SpotubeAudioSourceContainerPresetLossyImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {SpotubeMediaCompressionType type, + String name, + List qualities}); +} + +/// @nodoc +class __$$SpotubeAudioSourceContainerPresetLossyImplCopyWithImpl<$Res> + extends _$SpotubeAudioSourceContainerPresetCopyWithImpl<$Res, + _$SpotubeAudioSourceContainerPresetLossyImpl> + implements _$$SpotubeAudioSourceContainerPresetLossyImplCopyWith<$Res> { + __$$SpotubeAudioSourceContainerPresetLossyImplCopyWithImpl( + _$SpotubeAudioSourceContainerPresetLossyImpl _value, + $Res Function(_$SpotubeAudioSourceContainerPresetLossyImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? name = null, + Object? qualities = null, + }) { + return _then(_$SpotubeAudioSourceContainerPresetLossyImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as SpotubeMediaCompressionType, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + qualities: null == qualities + ? _value._qualities + : qualities // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioSourceContainerPresetLossyImpl + implements SpotubeAudioSourceContainerPresetLossy { + _$SpotubeAudioSourceContainerPresetLossyImpl( + {required this.type, + required this.name, + required final List qualities}) + : _qualities = qualities; + + factory _$SpotubeAudioSourceContainerPresetLossyImpl.fromJson( + Map json) => + _$$SpotubeAudioSourceContainerPresetLossyImplFromJson(json); + + @override + final SpotubeMediaCompressionType type; + @override + final String name; + final List _qualities; + @override + List get qualities { + if (_qualities is EqualUnmodifiableListView) return _qualities; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_qualities); + } + + @override + String toString() { + return 'SpotubeAudioSourceContainerPreset.lossy(type: $type, name: $name, qualities: $qualities)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioSourceContainerPresetLossyImpl && + (identical(other.type, type) || other.type == type) && + (identical(other.name, name) || other.name == name) && + const DeepCollectionEquality() + .equals(other._qualities, _qualities)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, type, name, const DeepCollectionEquality().hash(_qualities)); + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioSourceContainerPresetLossyImplCopyWith< + _$SpotubeAudioSourceContainerPresetLossyImpl> + get copyWith => + __$$SpotubeAudioSourceContainerPresetLossyImplCopyWithImpl< + _$SpotubeAudioSourceContainerPresetLossyImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossy, + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossless, + }) { + return lossy(type, name, qualities); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + }) { + return lossy?.call(type, name, qualities); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + required TResult orElse(), + }) { + if (lossy != null) { + return lossy(type, name, qualities); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(SpotubeAudioSourceContainerPresetLossy value) + lossy, + required TResult Function(SpotubeAudioSourceContainerPresetLossless value) + lossless, + }) { + return lossy(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult? Function(SpotubeAudioSourceContainerPresetLossless value)? + lossless, + }) { + return lossy?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult Function(SpotubeAudioSourceContainerPresetLossless value)? lossless, + required TResult orElse(), + }) { + if (lossy != null) { + return lossy(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$SpotubeAudioSourceContainerPresetLossyImplToJson( + this, + ); + } +} + +abstract class SpotubeAudioSourceContainerPresetLossy + implements SpotubeAudioSourceContainerPreset { + factory SpotubeAudioSourceContainerPresetLossy( + {required final SpotubeMediaCompressionType type, + required final String name, + required final List qualities}) = + _$SpotubeAudioSourceContainerPresetLossyImpl; + + factory SpotubeAudioSourceContainerPresetLossy.fromJson( + Map json) = + _$SpotubeAudioSourceContainerPresetLossyImpl.fromJson; + + @override + SpotubeMediaCompressionType get type; + @override + String get name; + @override + List get qualities; + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioSourceContainerPresetLossyImplCopyWith< + _$SpotubeAudioSourceContainerPresetLossyImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$SpotubeAudioSourceContainerPresetLosslessImplCopyWith<$Res> + implements $SpotubeAudioSourceContainerPresetCopyWith<$Res> { + factory _$$SpotubeAudioSourceContainerPresetLosslessImplCopyWith( + _$SpotubeAudioSourceContainerPresetLosslessImpl value, + $Res Function(_$SpotubeAudioSourceContainerPresetLosslessImpl) then) = + __$$SpotubeAudioSourceContainerPresetLosslessImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {SpotubeMediaCompressionType type, + String name, + List qualities}); +} + +/// @nodoc +class __$$SpotubeAudioSourceContainerPresetLosslessImplCopyWithImpl<$Res> + extends _$SpotubeAudioSourceContainerPresetCopyWithImpl<$Res, + _$SpotubeAudioSourceContainerPresetLosslessImpl> + implements _$$SpotubeAudioSourceContainerPresetLosslessImplCopyWith<$Res> { + __$$SpotubeAudioSourceContainerPresetLosslessImplCopyWithImpl( + _$SpotubeAudioSourceContainerPresetLosslessImpl _value, + $Res Function(_$SpotubeAudioSourceContainerPresetLosslessImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? name = null, + Object? qualities = null, + }) { + return _then(_$SpotubeAudioSourceContainerPresetLosslessImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as SpotubeMediaCompressionType, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + qualities: null == qualities + ? _value._qualities + : qualities // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioSourceContainerPresetLosslessImpl + implements SpotubeAudioSourceContainerPresetLossless { + _$SpotubeAudioSourceContainerPresetLosslessImpl( + {required this.type, + required this.name, + required final List qualities}) + : _qualities = qualities; + + factory _$SpotubeAudioSourceContainerPresetLosslessImpl.fromJson( + Map json) => + _$$SpotubeAudioSourceContainerPresetLosslessImplFromJson(json); + + @override + final SpotubeMediaCompressionType type; + @override + final String name; + final List _qualities; + @override + List get qualities { + if (_qualities is EqualUnmodifiableListView) return _qualities; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_qualities); + } + + @override + String toString() { + return 'SpotubeAudioSourceContainerPreset.lossless(type: $type, name: $name, qualities: $qualities)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioSourceContainerPresetLosslessImpl && + (identical(other.type, type) || other.type == type) && + (identical(other.name, name) || other.name == name) && + const DeepCollectionEquality() + .equals(other._qualities, _qualities)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, type, name, const DeepCollectionEquality().hash(_qualities)); + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioSourceContainerPresetLosslessImplCopyWith< + _$SpotubeAudioSourceContainerPresetLosslessImpl> + get copyWith => + __$$SpotubeAudioSourceContainerPresetLosslessImplCopyWithImpl< + _$SpotubeAudioSourceContainerPresetLosslessImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossy, + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossless, + }) { + return lossless(type, name, qualities); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + }) { + return lossless?.call(type, name, qualities); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + required TResult orElse(), + }) { + if (lossless != null) { + return lossless(type, name, qualities); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(SpotubeAudioSourceContainerPresetLossy value) + lossy, + required TResult Function(SpotubeAudioSourceContainerPresetLossless value) + lossless, + }) { + return lossless(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult? Function(SpotubeAudioSourceContainerPresetLossless value)? + lossless, + }) { + return lossless?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult Function(SpotubeAudioSourceContainerPresetLossless value)? lossless, + required TResult orElse(), + }) { + if (lossless != null) { + return lossless(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$SpotubeAudioSourceContainerPresetLosslessImplToJson( + this, + ); + } +} + +abstract class SpotubeAudioSourceContainerPresetLossless + implements SpotubeAudioSourceContainerPreset { + factory SpotubeAudioSourceContainerPresetLossless( + {required final SpotubeMediaCompressionType type, + required final String name, + required final List + qualities}) = _$SpotubeAudioSourceContainerPresetLosslessImpl; + + factory SpotubeAudioSourceContainerPresetLossless.fromJson( + Map json) = + _$SpotubeAudioSourceContainerPresetLosslessImpl.fromJson; + + @override + SpotubeMediaCompressionType get type; + @override + String get name; + @override + List get qualities; + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioSourceContainerPresetLosslessImplCopyWith< + _$SpotubeAudioSourceContainerPresetLosslessImpl> + get copyWith => throw _privateConstructorUsedError; +} + +SpotubeAudioLossyContainerQuality _$SpotubeAudioLossyContainerQualityFromJson( + Map json) { + return _SpotubeAudioLossyContainerQuality.fromJson(json); +} + +/// @nodoc +mixin _$SpotubeAudioLossyContainerQuality { + double get bitrate => throw _privateConstructorUsedError; + + /// Serializes this SpotubeAudioLossyContainerQuality to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SpotubeAudioLossyContainerQuality + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SpotubeAudioLossyContainerQualityCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpotubeAudioLossyContainerQualityCopyWith<$Res> { + factory $SpotubeAudioLossyContainerQualityCopyWith( + SpotubeAudioLossyContainerQuality value, + $Res Function(SpotubeAudioLossyContainerQuality) then) = + _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res, + SpotubeAudioLossyContainerQuality>; + @useResult + $Res call({double bitrate}); +} + +/// @nodoc +class _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res, + $Val extends SpotubeAudioLossyContainerQuality> + implements $SpotubeAudioLossyContainerQualityCopyWith<$Res> { + _$SpotubeAudioLossyContainerQualityCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SpotubeAudioLossyContainerQuality + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? bitrate = null, + }) { + return _then(_value.copyWith( + bitrate: null == bitrate + ? _value.bitrate + : bitrate // ignore: cast_nullable_to_non_nullable + as double, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SpotubeAudioLossyContainerQualityImplCopyWith<$Res> + implements $SpotubeAudioLossyContainerQualityCopyWith<$Res> { + factory _$$SpotubeAudioLossyContainerQualityImplCopyWith( + _$SpotubeAudioLossyContainerQualityImpl value, + $Res Function(_$SpotubeAudioLossyContainerQualityImpl) then) = + __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({double bitrate}); +} + +/// @nodoc +class __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res> + extends _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res, + _$SpotubeAudioLossyContainerQualityImpl> + implements _$$SpotubeAudioLossyContainerQualityImplCopyWith<$Res> { + __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl( + _$SpotubeAudioLossyContainerQualityImpl _value, + $Res Function(_$SpotubeAudioLossyContainerQualityImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioLossyContainerQuality + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? bitrate = null, + }) { + return _then(_$SpotubeAudioLossyContainerQualityImpl( + bitrate: null == bitrate + ? _value.bitrate + : bitrate // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioLossyContainerQualityImpl + implements _SpotubeAudioLossyContainerQuality { + _$SpotubeAudioLossyContainerQualityImpl({required this.bitrate}); + + factory _$SpotubeAudioLossyContainerQualityImpl.fromJson( + Map json) => + _$$SpotubeAudioLossyContainerQualityImplFromJson(json); + + @override + final double bitrate; + + @override + String toString() { + return 'SpotubeAudioLossyContainerQuality(bitrate: $bitrate)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioLossyContainerQualityImpl && + (identical(other.bitrate, bitrate) || other.bitrate == bitrate)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, bitrate); + + /// Create a copy of SpotubeAudioLossyContainerQuality + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioLossyContainerQualityImplCopyWith< + _$SpotubeAudioLossyContainerQualityImpl> + get copyWith => __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl< + _$SpotubeAudioLossyContainerQualityImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SpotubeAudioLossyContainerQualityImplToJson( + this, + ); + } +} + +abstract class _SpotubeAudioLossyContainerQuality + implements SpotubeAudioLossyContainerQuality { + factory _SpotubeAudioLossyContainerQuality({required final double bitrate}) = + _$SpotubeAudioLossyContainerQualityImpl; + + factory _SpotubeAudioLossyContainerQuality.fromJson( + Map json) = + _$SpotubeAudioLossyContainerQualityImpl.fromJson; + + @override + double get bitrate; + + /// Create a copy of SpotubeAudioLossyContainerQuality + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioLossyContainerQualityImplCopyWith< + _$SpotubeAudioLossyContainerQualityImpl> + get copyWith => throw _privateConstructorUsedError; +} + +SpotubeAudioLosslessContainerQuality + _$SpotubeAudioLosslessContainerQualityFromJson(Map json) { + return _SpotubeAudioLosslessContainerQuality.fromJson(json); +} + +/// @nodoc +mixin _$SpotubeAudioLosslessContainerQuality { + int get bitDepth => throw _privateConstructorUsedError; + double get sampleRate => throw _privateConstructorUsedError; + + /// Serializes this SpotubeAudioLosslessContainerQuality to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SpotubeAudioLosslessContainerQuality + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SpotubeAudioLosslessContainerQualityCopyWith< + SpotubeAudioLosslessContainerQuality> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpotubeAudioLosslessContainerQualityCopyWith<$Res> { + factory $SpotubeAudioLosslessContainerQualityCopyWith( + SpotubeAudioLosslessContainerQuality value, + $Res Function(SpotubeAudioLosslessContainerQuality) then) = + _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res, + SpotubeAudioLosslessContainerQuality>; + @useResult + $Res call({int bitDepth, double sampleRate}); +} + +/// @nodoc +class _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res, + $Val extends SpotubeAudioLosslessContainerQuality> + implements $SpotubeAudioLosslessContainerQualityCopyWith<$Res> { + _$SpotubeAudioLosslessContainerQualityCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SpotubeAudioLosslessContainerQuality + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? bitDepth = null, + Object? sampleRate = null, + }) { + return _then(_value.copyWith( + bitDepth: null == bitDepth + ? _value.bitDepth + : bitDepth // ignore: cast_nullable_to_non_nullable + as int, + sampleRate: null == sampleRate + ? _value.sampleRate + : sampleRate // ignore: cast_nullable_to_non_nullable + as double, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SpotubeAudioLosslessContainerQualityImplCopyWith<$Res> + implements $SpotubeAudioLosslessContainerQualityCopyWith<$Res> { + factory _$$SpotubeAudioLosslessContainerQualityImplCopyWith( + _$SpotubeAudioLosslessContainerQualityImpl value, + $Res Function(_$SpotubeAudioLosslessContainerQualityImpl) then) = + __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int bitDepth, double sampleRate}); +} + +/// @nodoc +class __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res> + extends _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res, + _$SpotubeAudioLosslessContainerQualityImpl> + implements _$$SpotubeAudioLosslessContainerQualityImplCopyWith<$Res> { + __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl( + _$SpotubeAudioLosslessContainerQualityImpl _value, + $Res Function(_$SpotubeAudioLosslessContainerQualityImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioLosslessContainerQuality + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? bitDepth = null, + Object? sampleRate = null, + }) { + return _then(_$SpotubeAudioLosslessContainerQualityImpl( + bitDepth: null == bitDepth + ? _value.bitDepth + : bitDepth // ignore: cast_nullable_to_non_nullable + as int, + sampleRate: null == sampleRate + ? _value.sampleRate + : sampleRate // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioLosslessContainerQualityImpl + implements _SpotubeAudioLosslessContainerQuality { + _$SpotubeAudioLosslessContainerQualityImpl( + {required this.bitDepth, required this.sampleRate}); + + factory _$SpotubeAudioLosslessContainerQualityImpl.fromJson( + Map json) => + _$$SpotubeAudioLosslessContainerQualityImplFromJson(json); + + @override + final int bitDepth; + @override + final double sampleRate; + + @override + String toString() { + return 'SpotubeAudioLosslessContainerQuality(bitDepth: $bitDepth, sampleRate: $sampleRate)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioLosslessContainerQualityImpl && + (identical(other.bitDepth, bitDepth) || + other.bitDepth == bitDepth) && + (identical(other.sampleRate, sampleRate) || + other.sampleRate == sampleRate)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, bitDepth, sampleRate); + + /// Create a copy of SpotubeAudioLosslessContainerQuality + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioLosslessContainerQualityImplCopyWith< + _$SpotubeAudioLosslessContainerQualityImpl> + get copyWith => __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl< + _$SpotubeAudioLosslessContainerQualityImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SpotubeAudioLosslessContainerQualityImplToJson( + this, + ); + } +} + +abstract class _SpotubeAudioLosslessContainerQuality + implements SpotubeAudioLosslessContainerQuality { + factory _SpotubeAudioLosslessContainerQuality( + {required final int bitDepth, required final double sampleRate}) = + _$SpotubeAudioLosslessContainerQualityImpl; + + factory _SpotubeAudioLosslessContainerQuality.fromJson( + Map json) = + _$SpotubeAudioLosslessContainerQualityImpl.fromJson; + + @override + int get bitDepth; + @override + double get sampleRate; + + /// Create a copy of SpotubeAudioLosslessContainerQuality + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioLosslessContainerQualityImplCopyWith< + _$SpotubeAudioLosslessContainerQualityImpl> + get copyWith => throw _privateConstructorUsedError; +} + +SpotubeAudioSourceMatchObject _$SpotubeAudioSourceMatchObjectFromJson( + Map json) { + return _SpotubeAudioSourceMatchObject.fromJson(json); +} + +/// @nodoc +mixin _$SpotubeAudioSourceMatchObject { + String get id => throw _privateConstructorUsedError; + String get title => throw _privateConstructorUsedError; + List get artists => throw _privateConstructorUsedError; + Duration get duration => throw _privateConstructorUsedError; + String? get thumbnail => throw _privateConstructorUsedError; + String get externalUri => throw _privateConstructorUsedError; + + /// Serializes this SpotubeAudioSourceMatchObject to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SpotubeAudioSourceMatchObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SpotubeAudioSourceMatchObjectCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpotubeAudioSourceMatchObjectCopyWith<$Res> { + factory $SpotubeAudioSourceMatchObjectCopyWith( + SpotubeAudioSourceMatchObject value, + $Res Function(SpotubeAudioSourceMatchObject) then) = + _$SpotubeAudioSourceMatchObjectCopyWithImpl<$Res, + SpotubeAudioSourceMatchObject>; + @useResult + $Res call( + {String id, + String title, + List artists, + Duration duration, + String? thumbnail, + String externalUri}); +} + +/// @nodoc +class _$SpotubeAudioSourceMatchObjectCopyWithImpl<$Res, + $Val extends SpotubeAudioSourceMatchObject> + implements $SpotubeAudioSourceMatchObjectCopyWith<$Res> { + _$SpotubeAudioSourceMatchObjectCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SpotubeAudioSourceMatchObject + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = null, + Object? artists = null, + Object? duration = null, + Object? thumbnail = freezed, + Object? externalUri = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + artists: null == artists + ? _value.artists + : artists // ignore: cast_nullable_to_non_nullable + as List, + duration: null == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as Duration, + thumbnail: freezed == thumbnail + ? _value.thumbnail + : thumbnail // ignore: cast_nullable_to_non_nullable + as String?, + externalUri: null == externalUri + ? _value.externalUri + : externalUri // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SpotubeAudioSourceMatchObjectImplCopyWith<$Res> + implements $SpotubeAudioSourceMatchObjectCopyWith<$Res> { + factory _$$SpotubeAudioSourceMatchObjectImplCopyWith( + _$SpotubeAudioSourceMatchObjectImpl value, + $Res Function(_$SpotubeAudioSourceMatchObjectImpl) then) = + __$$SpotubeAudioSourceMatchObjectImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String title, + List artists, + Duration duration, + String? thumbnail, + String externalUri}); +} + +/// @nodoc +class __$$SpotubeAudioSourceMatchObjectImplCopyWithImpl<$Res> + extends _$SpotubeAudioSourceMatchObjectCopyWithImpl<$Res, + _$SpotubeAudioSourceMatchObjectImpl> + implements _$$SpotubeAudioSourceMatchObjectImplCopyWith<$Res> { + __$$SpotubeAudioSourceMatchObjectImplCopyWithImpl( + _$SpotubeAudioSourceMatchObjectImpl _value, + $Res Function(_$SpotubeAudioSourceMatchObjectImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioSourceMatchObject + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = null, + Object? artists = null, + Object? duration = null, + Object? thumbnail = freezed, + Object? externalUri = null, + }) { + return _then(_$SpotubeAudioSourceMatchObjectImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + artists: null == artists + ? _value._artists + : artists // ignore: cast_nullable_to_non_nullable + as List, + duration: null == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as Duration, + thumbnail: freezed == thumbnail + ? _value.thumbnail + : thumbnail // ignore: cast_nullable_to_non_nullable + as String?, + externalUri: null == externalUri + ? _value.externalUri + : externalUri // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioSourceMatchObjectImpl + implements _SpotubeAudioSourceMatchObject { + _$SpotubeAudioSourceMatchObjectImpl( + {required this.id, + required this.title, + required final List artists, + required this.duration, + this.thumbnail, + required this.externalUri}) + : _artists = artists; + + factory _$SpotubeAudioSourceMatchObjectImpl.fromJson( + Map json) => + _$$SpotubeAudioSourceMatchObjectImplFromJson(json); + + @override + final String id; + @override + final String title; + final List _artists; + @override + List get artists { + if (_artists is EqualUnmodifiableListView) return _artists; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_artists); + } + + @override + final Duration duration; + @override + final String? thumbnail; + @override + final String externalUri; + + @override + String toString() { + return 'SpotubeAudioSourceMatchObject(id: $id, title: $title, artists: $artists, duration: $duration, thumbnail: $thumbnail, externalUri: $externalUri)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioSourceMatchObjectImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.title, title) || other.title == title) && + const DeepCollectionEquality().equals(other._artists, _artists) && + (identical(other.duration, duration) || + other.duration == duration) && + (identical(other.thumbnail, thumbnail) || + other.thumbnail == thumbnail) && + (identical(other.externalUri, externalUri) || + other.externalUri == externalUri)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + title, + const DeepCollectionEquality().hash(_artists), + duration, + thumbnail, + externalUri); + + /// Create a copy of SpotubeAudioSourceMatchObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioSourceMatchObjectImplCopyWith< + _$SpotubeAudioSourceMatchObjectImpl> + get copyWith => __$$SpotubeAudioSourceMatchObjectImplCopyWithImpl< + _$SpotubeAudioSourceMatchObjectImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SpotubeAudioSourceMatchObjectImplToJson( + this, + ); + } +} + +abstract class _SpotubeAudioSourceMatchObject + implements SpotubeAudioSourceMatchObject { + factory _SpotubeAudioSourceMatchObject( + {required final String id, + required final String title, + required final List artists, + required final Duration duration, + final String? thumbnail, + required final String externalUri}) = _$SpotubeAudioSourceMatchObjectImpl; + + factory _SpotubeAudioSourceMatchObject.fromJson(Map json) = + _$SpotubeAudioSourceMatchObjectImpl.fromJson; + + @override + String get id; + @override + String get title; + @override + List get artists; + @override + Duration get duration; + @override + String? get thumbnail; + @override + String get externalUri; + + /// Create a copy of SpotubeAudioSourceMatchObject + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioSourceMatchObjectImplCopyWith< + _$SpotubeAudioSourceMatchObjectImpl> + get copyWith => throw _privateConstructorUsedError; +} + +SpotubeAudioSourceStreamObject _$SpotubeAudioSourceStreamObjectFromJson( + Map json) { + return _SpotubeAudioSourceStreamObject.fromJson(json); +} + +/// @nodoc +mixin _$SpotubeAudioSourceStreamObject { + String get url => throw _privateConstructorUsedError; + String get container => throw _privateConstructorUsedError; + SpotubeMediaCompressionType get type => throw _privateConstructorUsedError; + String? get codec => throw _privateConstructorUsedError; + double? get bitrate => throw _privateConstructorUsedError; + int? get bitDepth => throw _privateConstructorUsedError; + double? get sampleRate => throw _privateConstructorUsedError; + + /// Serializes this SpotubeAudioSourceStreamObject to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SpotubeAudioSourceStreamObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SpotubeAudioSourceStreamObjectCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpotubeAudioSourceStreamObjectCopyWith<$Res> { + factory $SpotubeAudioSourceStreamObjectCopyWith( + SpotubeAudioSourceStreamObject value, + $Res Function(SpotubeAudioSourceStreamObject) then) = + _$SpotubeAudioSourceStreamObjectCopyWithImpl<$Res, + SpotubeAudioSourceStreamObject>; + @useResult + $Res call( + {String url, + String container, + SpotubeMediaCompressionType type, + String? codec, + double? bitrate, + int? bitDepth, + double? sampleRate}); +} + +/// @nodoc +class _$SpotubeAudioSourceStreamObjectCopyWithImpl<$Res, + $Val extends SpotubeAudioSourceStreamObject> + implements $SpotubeAudioSourceStreamObjectCopyWith<$Res> { + _$SpotubeAudioSourceStreamObjectCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SpotubeAudioSourceStreamObject + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? url = null, + Object? container = null, + Object? type = null, + Object? codec = freezed, + Object? bitrate = freezed, + Object? bitDepth = freezed, + Object? sampleRate = freezed, + }) { + return _then(_value.copyWith( + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + container: null == container + ? _value.container + : container // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as SpotubeMediaCompressionType, + codec: freezed == codec + ? _value.codec + : codec // ignore: cast_nullable_to_non_nullable + as String?, + bitrate: freezed == bitrate + ? _value.bitrate + : bitrate // ignore: cast_nullable_to_non_nullable + as double?, + bitDepth: freezed == bitDepth + ? _value.bitDepth + : bitDepth // ignore: cast_nullable_to_non_nullable + as int?, + sampleRate: freezed == sampleRate + ? _value.sampleRate + : sampleRate // ignore: cast_nullable_to_non_nullable + as double?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SpotubeAudioSourceStreamObjectImplCopyWith<$Res> + implements $SpotubeAudioSourceStreamObjectCopyWith<$Res> { + factory _$$SpotubeAudioSourceStreamObjectImplCopyWith( + _$SpotubeAudioSourceStreamObjectImpl value, + $Res Function(_$SpotubeAudioSourceStreamObjectImpl) then) = + __$$SpotubeAudioSourceStreamObjectImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String url, + String container, + SpotubeMediaCompressionType type, + String? codec, + double? bitrate, + int? bitDepth, + double? sampleRate}); +} + +/// @nodoc +class __$$SpotubeAudioSourceStreamObjectImplCopyWithImpl<$Res> + extends _$SpotubeAudioSourceStreamObjectCopyWithImpl<$Res, + _$SpotubeAudioSourceStreamObjectImpl> + implements _$$SpotubeAudioSourceStreamObjectImplCopyWith<$Res> { + __$$SpotubeAudioSourceStreamObjectImplCopyWithImpl( + _$SpotubeAudioSourceStreamObjectImpl _value, + $Res Function(_$SpotubeAudioSourceStreamObjectImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioSourceStreamObject + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? url = null, + Object? container = null, + Object? type = null, + Object? codec = freezed, + Object? bitrate = freezed, + Object? bitDepth = freezed, + Object? sampleRate = freezed, + }) { + return _then(_$SpotubeAudioSourceStreamObjectImpl( + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + container: null == container + ? _value.container + : container // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as SpotubeMediaCompressionType, + codec: freezed == codec + ? _value.codec + : codec // ignore: cast_nullable_to_non_nullable + as String?, + bitrate: freezed == bitrate + ? _value.bitrate + : bitrate // ignore: cast_nullable_to_non_nullable + as double?, + bitDepth: freezed == bitDepth + ? _value.bitDepth + : bitDepth // ignore: cast_nullable_to_non_nullable + as int?, + sampleRate: freezed == sampleRate + ? _value.sampleRate + : sampleRate // ignore: cast_nullable_to_non_nullable + as double?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioSourceStreamObjectImpl + implements _SpotubeAudioSourceStreamObject { + _$SpotubeAudioSourceStreamObjectImpl( + {required this.url, + required this.container, + required this.type, + this.codec, + this.bitrate, + this.bitDepth, + this.sampleRate}); + + factory _$SpotubeAudioSourceStreamObjectImpl.fromJson( + Map json) => + _$$SpotubeAudioSourceStreamObjectImplFromJson(json); + + @override + final String url; + @override + final String container; + @override + final SpotubeMediaCompressionType type; + @override + final String? codec; + @override + final double? bitrate; + @override + final int? bitDepth; + @override + final double? sampleRate; + + @override + String toString() { + return 'SpotubeAudioSourceStreamObject(url: $url, container: $container, type: $type, codec: $codec, bitrate: $bitrate, bitDepth: $bitDepth, sampleRate: $sampleRate)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioSourceStreamObjectImpl && + (identical(other.url, url) || other.url == url) && + (identical(other.container, container) || + other.container == container) && + (identical(other.type, type) || other.type == type) && + (identical(other.codec, codec) || other.codec == codec) && + (identical(other.bitrate, bitrate) || other.bitrate == bitrate) && + (identical(other.bitDepth, bitDepth) || + other.bitDepth == bitDepth) && + (identical(other.sampleRate, sampleRate) || + other.sampleRate == sampleRate)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, url, container, type, codec, bitrate, bitDepth, sampleRate); + + /// Create a copy of SpotubeAudioSourceStreamObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioSourceStreamObjectImplCopyWith< + _$SpotubeAudioSourceStreamObjectImpl> + get copyWith => __$$SpotubeAudioSourceStreamObjectImplCopyWithImpl< + _$SpotubeAudioSourceStreamObjectImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SpotubeAudioSourceStreamObjectImplToJson( + this, + ); + } +} + +abstract class _SpotubeAudioSourceStreamObject + implements SpotubeAudioSourceStreamObject { + factory _SpotubeAudioSourceStreamObject( + {required final String url, + required final String container, + required final SpotubeMediaCompressionType type, + final String? codec, + final double? bitrate, + final int? bitDepth, + final double? sampleRate}) = _$SpotubeAudioSourceStreamObjectImpl; + + factory _SpotubeAudioSourceStreamObject.fromJson(Map json) = + _$SpotubeAudioSourceStreamObjectImpl.fromJson; + + @override + String get url; + @override + String get container; + @override + SpotubeMediaCompressionType get type; + @override + String? get codec; + @override + double? get bitrate; + @override + int? get bitDepth; + @override + double? get sampleRate; + + /// Create a copy of SpotubeAudioSourceStreamObject + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioSourceStreamObjectImplCopyWith< + _$SpotubeAudioSourceStreamObjectImpl> + get copyWith => throw _privateConstructorUsedError; +} + SpotubeFullAlbumObject _$SpotubeFullAlbumObjectFromJson( Map json) { return _SpotubeFullAlbumObject.fromJson(json); @@ -4499,7 +5995,6 @@ PluginConfiguration _$PluginConfigurationFromJson(Map json) { /// @nodoc mixin _$PluginConfiguration { - PluginType get type => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError; String get description => throw _privateConstructorUsedError; String get version => throw _privateConstructorUsedError; @@ -4527,8 +6022,7 @@ abstract class $PluginConfigurationCopyWith<$Res> { _$PluginConfigurationCopyWithImpl<$Res, PluginConfiguration>; @useResult $Res call( - {PluginType type, - String name, + {String name, String description, String version, String author, @@ -4554,7 +6048,6 @@ class _$PluginConfigurationCopyWithImpl<$Res, $Val extends PluginConfiguration> @pragma('vm:prefer-inline') @override $Res call({ - Object? type = null, Object? name = null, Object? description = null, Object? version = null, @@ -4566,10 +6059,6 @@ class _$PluginConfigurationCopyWithImpl<$Res, $Val extends PluginConfiguration> Object? repository = freezed, }) { return _then(_value.copyWith( - type: null == type - ? _value.type - : type // ignore: cast_nullable_to_non_nullable - as PluginType, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -4619,8 +6108,7 @@ abstract class _$$PluginConfigurationImplCopyWith<$Res> @override @useResult $Res call( - {PluginType type, - String name, + {String name, String description, String version, String author, @@ -4644,7 +6132,6 @@ class __$$PluginConfigurationImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? type = null, Object? name = null, Object? description = null, Object? version = null, @@ -4656,10 +6143,6 @@ class __$$PluginConfigurationImplCopyWithImpl<$Res> Object? repository = freezed, }) { return _then(_$PluginConfigurationImpl( - type: null == type - ? _value.type - : type // ignore: cast_nullable_to_non_nullable - as PluginType, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -4704,8 +6187,7 @@ class __$$PluginConfigurationImplCopyWithImpl<$Res> @JsonSerializable() class _$PluginConfigurationImpl extends _PluginConfiguration { _$PluginConfigurationImpl( - {required this.type, - required this.name, + {required this.name, required this.description, required this.version, required this.author, @@ -4721,8 +6203,6 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { factory _$PluginConfigurationImpl.fromJson(Map json) => _$$PluginConfigurationImplFromJson(json); - @override - final PluginType type; @override final String name; @override @@ -4758,7 +6238,7 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { @override String toString() { - return 'PluginConfiguration(type: $type, name: $name, description: $description, version: $version, author: $author, entryPoint: $entryPoint, pluginApiVersion: $pluginApiVersion, apis: $apis, abilities: $abilities, repository: $repository)'; + return 'PluginConfiguration(name: $name, description: $description, version: $version, author: $author, entryPoint: $entryPoint, pluginApiVersion: $pluginApiVersion, apis: $apis, abilities: $abilities, repository: $repository)'; } @override @@ -4766,7 +6246,6 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PluginConfigurationImpl && - (identical(other.type, type) || other.type == type) && (identical(other.name, name) || other.name == name) && (identical(other.description, description) || other.description == description) && @@ -4787,7 +6266,6 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { @override int get hashCode => Object.hash( runtimeType, - type, name, description, version, @@ -4817,8 +6295,7 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { abstract class _PluginConfiguration extends PluginConfiguration { factory _PluginConfiguration( - {required final PluginType type, - required final String name, + {required final String name, required final String description, required final String version, required final String author, @@ -4832,8 +6309,6 @@ abstract class _PluginConfiguration extends PluginConfiguration { factory _PluginConfiguration.fromJson(Map json) = _$PluginConfigurationImpl.fromJson; - @override - PluginType get type; @override String get name; @override diff --git a/lib/models/metadata/metadata.g.dart b/lib/models/metadata/metadata.g.dart index 6f416330..9c45cb7c 100644 --- a/lib/models/metadata/metadata.g.dart +++ b/lib/models/metadata/metadata.g.dart @@ -6,6 +6,123 @@ part of 'metadata.dart'; // JsonSerializableGenerator // ************************************************************************** +_$SpotubeAudioSourceContainerPresetLossyImpl + _$$SpotubeAudioSourceContainerPresetLossyImplFromJson(Map json) => + _$SpotubeAudioSourceContainerPresetLossyImpl( + type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']), + name: json['name'] as String, + qualities: (json['qualities'] as List) + .map((e) => SpotubeAudioLossyContainerQuality.fromJson( + Map.from(e as Map))) + .toList(), + ); + +Map _$$SpotubeAudioSourceContainerPresetLossyImplToJson( + _$SpotubeAudioSourceContainerPresetLossyImpl instance) => + { + 'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!, + 'name': instance.name, + 'qualities': instance.qualities.map((e) => e.toJson()).toList(), + }; + +const _$SpotubeMediaCompressionTypeEnumMap = { + SpotubeMediaCompressionType.lossy: 'lossy', + SpotubeMediaCompressionType.lossless: 'lossless', +}; + +_$SpotubeAudioSourceContainerPresetLosslessImpl + _$$SpotubeAudioSourceContainerPresetLosslessImplFromJson(Map json) => + _$SpotubeAudioSourceContainerPresetLosslessImpl( + type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']), + name: json['name'] as String, + qualities: (json['qualities'] as List) + .map((e) => SpotubeAudioLosslessContainerQuality.fromJson( + Map.from(e as Map))) + .toList(), + ); + +Map _$$SpotubeAudioSourceContainerPresetLosslessImplToJson( + _$SpotubeAudioSourceContainerPresetLosslessImpl instance) => + { + 'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!, + 'name': instance.name, + 'qualities': instance.qualities.map((e) => e.toJson()).toList(), + }; + +_$SpotubeAudioLossyContainerQualityImpl + _$$SpotubeAudioLossyContainerQualityImplFromJson(Map json) => + _$SpotubeAudioLossyContainerQualityImpl( + bitrate: (json['bitrate'] as num).toDouble(), + ); + +Map _$$SpotubeAudioLossyContainerQualityImplToJson( + _$SpotubeAudioLossyContainerQualityImpl instance) => + { + 'bitrate': instance.bitrate, + }; + +_$SpotubeAudioLosslessContainerQualityImpl + _$$SpotubeAudioLosslessContainerQualityImplFromJson(Map json) => + _$SpotubeAudioLosslessContainerQualityImpl( + bitDepth: (json['bitDepth'] as num).toInt(), + sampleRate: (json['sampleRate'] as num).toDouble(), + ); + +Map _$$SpotubeAudioLosslessContainerQualityImplToJson( + _$SpotubeAudioLosslessContainerQualityImpl instance) => + { + 'bitDepth': instance.bitDepth, + 'sampleRate': instance.sampleRate, + }; + +_$SpotubeAudioSourceMatchObjectImpl + _$$SpotubeAudioSourceMatchObjectImplFromJson(Map json) => + _$SpotubeAudioSourceMatchObjectImpl( + id: json['id'] as String, + title: json['title'] as String, + artists: (json['artists'] as List) + .map((e) => e as String) + .toList(), + duration: Duration(microseconds: (json['duration'] as num).toInt()), + thumbnail: json['thumbnail'] as String?, + externalUri: json['externalUri'] as String, + ); + +Map _$$SpotubeAudioSourceMatchObjectImplToJson( + _$SpotubeAudioSourceMatchObjectImpl instance) => + { + 'id': instance.id, + 'title': instance.title, + 'artists': instance.artists, + 'duration': instance.duration.inMicroseconds, + 'thumbnail': instance.thumbnail, + 'externalUri': instance.externalUri, + }; + +_$SpotubeAudioSourceStreamObjectImpl + _$$SpotubeAudioSourceStreamObjectImplFromJson(Map json) => + _$SpotubeAudioSourceStreamObjectImpl( + url: json['url'] as String, + container: json['container'] as String, + type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']), + codec: json['codec'] as String?, + bitrate: (json['bitrate'] as num?)?.toDouble(), + bitDepth: (json['bitDepth'] as num?)?.toInt(), + sampleRate: (json['sampleRate'] as num?)?.toDouble(), + ); + +Map _$$SpotubeAudioSourceStreamObjectImplToJson( + _$SpotubeAudioSourceStreamObjectImpl instance) => + { + 'url': instance.url, + 'container': instance.container, + 'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!, + 'codec': instance.codec, + 'bitrate': instance.bitrate, + 'bitDepth': instance.bitDepth, + 'sampleRate': instance.sampleRate, + }; + _$SpotubeFullAlbumObjectImpl _$$SpotubeFullAlbumObjectImplFromJson(Map json) => _$SpotubeFullAlbumObjectImpl( id: json['id'] as String, @@ -419,7 +536,6 @@ Map _$$SpotubeUserObjectImplToJson( _$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) => _$PluginConfigurationImpl( - type: $enumDecode(_$PluginTypeEnumMap, json['type']), name: json['name'] as String, description: json['description'] as String, version: json['version'] as String, @@ -440,7 +556,6 @@ _$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) => Map _$$PluginConfigurationImplToJson( _$PluginConfigurationImpl instance) => { - 'type': _$PluginTypeEnumMap[instance.type]!, 'name': instance.name, 'description': instance.description, 'version': instance.version, @@ -453,10 +568,6 @@ Map _$$PluginConfigurationImplToJson( 'repository': instance.repository, }; -const _$PluginTypeEnumMap = { - PluginType.metadata: 'metadata', -}; - const _$PluginApisEnumMap = { PluginApis.webview: 'webview', PluginApis.localstorage: 'localstorage', @@ -466,6 +577,8 @@ const _$PluginApisEnumMap = { const _$PluginAbilitiesEnumMap = { PluginAbilities.authentication: 'authentication', PluginAbilities.scrobbling: 'scrobbling', + PluginAbilities.metadata: 'metadata', + PluginAbilities.audioSource: 'audio-source', }; _$PluginUpdateAvailableImpl _$$PluginUpdateAvailableImplFromJson(Map json) => diff --git a/lib/models/metadata/plugin.dart b/lib/models/metadata/plugin.dart index ac6bb0b9..6bc84160 100644 --- a/lib/models/metadata/plugin.dart +++ b/lib/models/metadata/plugin.dart @@ -1,17 +1,20 @@ part of 'metadata.dart'; -enum PluginType { metadata } - enum PluginApis { webview, localstorage, timezone } -enum PluginAbilities { authentication, scrobbling } +enum PluginAbilities { + authentication, + scrobbling, + metadata, + @JsonValue('audio-source') + audioSource, +} @freezed class PluginConfiguration with _$PluginConfiguration { const PluginConfiguration._(); factory PluginConfiguration({ - required PluginType type, required String name, required String description, required String version, diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index cf19c1f5..815fc826 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -10,6 +10,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/youtube_engine/youtube_engine.dart'; import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/metadata/errors/exceptions.dart'; @@ -97,7 +98,6 @@ class MetadataPluginNotifier extends AsyncNotifier { final plugin = plugins[i]; final pluginConfig = PluginConfiguration( - type: PluginType.metadata, name: plugin.name, author: plugin.author, description: plugin.description, @@ -447,6 +447,7 @@ final metadataPluginProvider = FutureProvider( final defaultPlugin = await ref.watch( metadataPluginsProvider.selectAsync((data) => data.defaultPluginConfig), ); + final youtubeEngine = ref.read(youtubeEngineProvider); if (defaultPlugin == null) { return null; @@ -456,6 +457,10 @@ final metadataPluginProvider = FutureProvider( final pluginByteCode = await pluginsNotifier.getPluginByteCode(defaultPlugin); - return await MetadataPlugin.create(defaultPlugin, pluginByteCode); + return await MetadataPlugin.create( + youtubeEngine, + defaultPlugin, + pluginByteCode, + ); }, ); diff --git a/lib/services/metadata/endpoints/audio_source.dart b/lib/services/metadata/endpoints/audio_source.dart new file mode 100644 index 00000000..3493c112 --- /dev/null +++ b/lib/services/metadata/endpoints/audio_source.dart @@ -0,0 +1,38 @@ +import 'package:hetu_script/hetu_script.dart'; +import 'package:hetu_script/values.dart'; +import 'package:spotube/models/metadata/metadata.dart'; + +class MetadataPluginAudioSourceEndpoint { + final Hetu hetu; + MetadataPluginAudioSourceEndpoint(this.hetu); + + HTInstance get hetuMetadataAudioSource => + (hetu.fetch("metadataPlugin") as HTInstance).memberGet("audioSource") + as HTInstance; + + List get supportedPresets { + final raw = hetuMetadataAudioSource.memberGet("supportedPresets") as List; + + return raw + .map((e) => SpotubeAudioSourceContainerPreset.fromJson(e)) + .toList(); + } + + Future> matches( + SpotubeFullTrackObject track, + ) async { + final raw = await hetuMetadataAudioSource + .invoke("matches", positionalArgs: [track]) as List; + + return raw.map((e) => SpotubeAudioSourceMatchObject.fromJson(e)).toList(); + } + + Future> streams( + SpotubeAudioSourceMatchObject match, + ) async { + final raw = await hetuMetadataAudioSource + .invoke("streams", positionalArgs: [match]) as List; + + return raw.map((e) => SpotubeAudioSourceStreamObject.fromJson(e)).toList(); + } +} diff --git a/lib/services/metadata/metadata.dart b/lib/services/metadata/metadata.dart index ab2290f6..5860e0d6 100644 --- a/lib/services/metadata/metadata.dart +++ b/lib/services/metadata/metadata.dart @@ -3,7 +3,9 @@ import 'dart:typed_data'; import 'package:auto_route/auto_route.dart'; import 'package:hetu_otp_util/hetu_otp_util.dart'; import 'package:hetu_script/hetu_script.dart'; -import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart'; +import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart' as spotube_plugin; +import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart' + hide YouTubeEngine; import 'package:hetu_std/hetu_std.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; @@ -15,6 +17,7 @@ import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/services/metadata/apis/localstorage.dart'; import 'package:spotube/services/metadata/endpoints/album.dart'; import 'package:spotube/services/metadata/endpoints/artist.dart'; +import 'package:spotube/services/metadata/endpoints/audio_source.dart'; import 'package:spotube/services/metadata/endpoints/auth.dart'; import 'package:spotube/services/metadata/endpoints/browse.dart'; import 'package:spotube/services/metadata/endpoints/playlist.dart'; @@ -22,13 +25,15 @@ import 'package:spotube/services/metadata/endpoints/search.dart'; import 'package:spotube/services/metadata/endpoints/track.dart'; import 'package:spotube/services/metadata/endpoints/core.dart'; import 'package:spotube/services/metadata/endpoints/user.dart'; +import 'package:spotube/services/youtube_engine/youtube_engine.dart'; const defaultMetadataLimit = "20"; class MetadataPlugin { - static final pluginApiVersion = Version.parse("1.0.0"); + static final pluginApiVersion = Version.parse("2.0.0"); static Future create( + YouTubeEngine youtubeEngine, PluginConfiguration config, Uint8List byteCode, ) async { @@ -76,6 +81,58 @@ class MetadataPlugin { ), ); }, + createYoutubeEngine: () { + return spotube_plugin.YouTubeEngine( + search: (query) async { + final result = await youtubeEngine.searchVideos(query); + return result + .map((video) => { + 'id': video.id.value, + 'title': video.title, + 'author': video.author, + 'duration': video.duration?.inSeconds, + 'description': video.description, + 'uploadDate': video.uploadDate?.toIso8601String(), + 'viewCount': video.engagement.viewCount, + 'likeCount': video.engagement.likeCount, + 'isLive': video.isLive, + }) + .toList(); + }, + getVideo: (videoId) async { + final video = await youtubeEngine.getVideo(videoId); + return { + 'id': video.id.value, + 'title': video.title, + 'author': video.author, + 'duration': video.duration?.inSeconds, + 'description': video.description, + 'uploadDate': video.uploadDate?.toIso8601String(), + 'viewCount': video.engagement.viewCount, + 'likeCount': video.engagement.likeCount, + 'isLive': video.isLive, + }; + }, + streamManifest: (videoId) { + return youtubeEngine.getStreamManifest(videoId).then( + (manifest) { + final streams = manifest.audioOnly + .map( + (stream) => { + 'url': stream.url.toString(), + 'quality': stream.qualityLabel, + 'bitrate': stream.bitrate.bitsPerSecond, + 'container': stream.container.name, + 'videoId': stream.videoId, + }, + ) + .toList(); + return streams; + }, + ); + }, + ); + }, ); await HetuStdLoader.loadBytecodeFlutter(hetu); @@ -98,6 +155,7 @@ class MetadataPlugin { late final MetadataAuthEndpoint auth; + late final MetadataPluginAudioSourceEndpoint audioSource; late final MetadataPluginAlbumEndpoint album; late final MetadataPluginArtistEndpoint artist; late final MetadataPluginBrowseEndpoint browse; @@ -110,6 +168,7 @@ class MetadataPlugin { MetadataPlugin._(this.hetu) { auth = MetadataAuthEndpoint(hetu); + audioSource = MetadataPluginAudioSourceEndpoint(hetu); artist = MetadataPluginArtistEndpoint(hetu); album = MetadataPluginAlbumEndpoint(hetu); browse = MetadataPluginBrowseEndpoint(hetu); diff --git a/pubspec.lock b/pubspec.lock index ff0c689c..08757d74 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1230,10 +1230,10 @@ packages: description: path: "." ref: main - resolved-ref: "01935a75640092af7947bfb21a497240376f0c83" + resolved-ref: "32828156bc111d147709f8d644804227bbdfe8f1" url: "https://github.com/KRTirtho/hetu_spotube_plugin.git" source: git - version: "0.0.1" + version: "0.0.2" hetu_std: dependency: "direct main" description: @@ -2888,10 +2888,11 @@ packages: youtube_explode_dart: dependency: "direct main" description: - name: youtube_explode_dart - sha256: "9ff345caf8351c59eb1b7560837f761e08d2beaea3b4187637942715a31a6f58" - url: "https://pub.dev" - source: hosted + path: "." + ref: HEAD + resolved-ref: caa3023386dbc10e69c99f49f491148094874671 + url: "https://github.com/Coronon/youtube_explode_dart" + source: git version: "2.5.2" yt_dlp_dart: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index 3cc1eb05..46273a32 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -138,7 +138,8 @@ dependencies: wikipedia_api: ^0.1.0 win32_registry: ^1.1.5 window_manager: ^0.4.3 - youtube_explode_dart: ^2.5.1 + youtube_explode_dart: + git: https://github.com/Coronon/youtube_explode_dart yt_dlp_dart: git: url: https://github.com/KRTirtho/yt_dlp_dart.git From f6d9d64b7d316b208dfc7c86e6f2bbdec1ce4719 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 23 Oct 2025 08:57:45 +0600 Subject: [PATCH 02/14] feat(plugins): filter plugins by abilities in plugins page and show abilities as badge --- lib/collections/spotube_icons.dart | 2 +- lib/l10n/app_en.arb | 4 +- lib/l10n/generated/app_localizations.dart | 12 ++-- lib/l10n/generated/app_localizations_ar.dart | 6 +- lib/l10n/generated/app_localizations_bn.dart | 6 +- lib/l10n/generated/app_localizations_ca.dart | 7 +-- lib/l10n/generated/app_localizations_cs.dart | 6 +- lib/l10n/generated/app_localizations_de.dart | 6 +- lib/l10n/generated/app_localizations_en.dart | 6 +- lib/l10n/generated/app_localizations_es.dart | 7 +-- lib/l10n/generated/app_localizations_eu.dart | 6 +- lib/l10n/generated/app_localizations_fa.dart | 6 +- lib/l10n/generated/app_localizations_fi.dart | 6 +- lib/l10n/generated/app_localizations_fr.dart | 7 +-- lib/l10n/generated/app_localizations_hi.dart | 6 +- lib/l10n/generated/app_localizations_id.dart | 6 +- lib/l10n/generated/app_localizations_it.dart | 6 +- lib/l10n/generated/app_localizations_ja.dart | 6 +- lib/l10n/generated/app_localizations_ka.dart | 7 +-- lib/l10n/generated/app_localizations_ko.dart | 6 +- lib/l10n/generated/app_localizations_ne.dart | 6 +- lib/l10n/generated/app_localizations_nl.dart | 6 +- lib/l10n/generated/app_localizations_pl.dart | 6 +- lib/l10n/generated/app_localizations_pt.dart | 6 +- lib/l10n/generated/app_localizations_ru.dart | 6 +- lib/l10n/generated/app_localizations_ta.dart | 6 +- lib/l10n/generated/app_localizations_th.dart | 6 +- lib/l10n/generated/app_localizations_tl.dart | 6 +- lib/l10n/generated/app_localizations_tr.dart | 6 +- lib/l10n/generated/app_localizations_uk.dart | 6 +- lib/l10n/generated/app_localizations_vi.dart | 6 +- lib/l10n/generated/app_localizations_zh.dart | 11 +--- lib/models/metadata/metadata.freezed.dart | 50 +++++++++++++--- lib/models/metadata/metadata.g.dart | 3 + lib/models/metadata/repository.dart | 1 + .../metadata_plugins/installed_plugin.dart | 18 ++++++ .../metadata_plugins/plugin_repository.dart | 11 ++++ lib/pages/settings/metadata_plugins.dart | 41 ++++++++++++- lib/pages/settings/sections/accounts.dart | 4 +- .../metadata_plugin/core/repositories.dart | 1 + untranslated_messages.json | 58 +++++++++++++++++++ 41 files changed, 270 insertions(+), 118 deletions(-) diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 21cf4176..99d9ff74 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -135,7 +135,7 @@ abstract class SpotubeIcons { static const list = FeatherIcons.list; static const device = FeatherIcons.smartphone; static const engine = FeatherIcons.server; - static const extensions = FeatherIcons.package; + static const extensions = Icons.extension_rounded; static const message = FeatherIcons.send; static const upload = FeatherIcons.uploadCloud; static const plugin = Icons.extension_outlined; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 833fa724..8c965aa1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -452,14 +452,14 @@ "disclaimer": "Disclaimer", "third_party_plugin_dmca_notice": "The Spotube team does not hold any responsibility (including legal) for any \"Third-party\" plugins.\nPlease use them at your own risk. For any bugs/issues, please report them to the plugin repository.\n\nIf any \"Third-party\" plugin is breaking ToS/DMCA of any service/legal entity, please ask the \"Third-party\" plugin author or the hosting platform .e.g GitHub/Codeberg to take action. Above listed (\"Third-party\" labelled) are all public/community maintained plugins. We're not curating them, so we cannot take any action on them.\n\n", "input_does_not_match_format": "Input doesn't match the required format", - "metadata_provider_plugins": "Metadata Provider Plugins", + "plugins": "Plugins", "paste_plugin_download_url": "Paste download url or GitHub/Codeberg repo url or direct link to .smplug file", "download_and_install_plugin_from_url": "Download and install plugin from url", "failed_to_add_plugin_error": "Failed to add plugin: {error}", "upload_plugin_from_file": "Upload plugin from file", "installed": "Installed", "available_plugins": "Available plugins", - "configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider", + "configure_plugins": "Configure your own metadata provider and audio source plugins", "audio_scrobblers": "Audio Scrobblers", "scrobbling": "Scrobbling", "source": "Source: ", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index d3124728..8d0610ad 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -2871,11 +2871,11 @@ abstract class AppLocalizations { /// **'Input doesn\'t match the required format'** String get input_does_not_match_format; - /// No description provided for @metadata_provider_plugins. + /// No description provided for @plugins. /// /// In en, this message translates to: - /// **'Metadata Provider Plugins'** - String get metadata_provider_plugins; + /// **'Plugins'** + String get plugins; /// No description provided for @paste_plugin_download_url. /// @@ -2913,11 +2913,11 @@ abstract class AppLocalizations { /// **'Available plugins'** String get available_plugins; - /// No description provided for @configure_your_own_metadata_plugin. + /// No description provided for @configure_plugins. /// /// In en, this message translates to: - /// **'Configure your own playlist/album/artist/feed metadata provider'** - String get configure_your_own_metadata_plugin; + /// **'Configure your own metadata provider and audio source plugins'** + String get configure_plugins; /// No description provided for @audio_scrobblers. /// diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index b974d2e4..09ec2505 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -1504,7 +1504,7 @@ class AppLocalizationsAr extends AppLocalizations { 'المدخل لا يتوافق مع التنسيق المطلوب'; @override - String get metadata_provider_plugins => 'إضافات مزود البيانات'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1529,8 +1529,8 @@ class AppLocalizationsAr extends AppLocalizations { String get available_plugins => 'الإضافات المتوفّرة'; @override - String get configure_your_own_metadata_plugin => - 'تهيئة مزوّد بيانات للقائمة/الألبوم/الفنان/المصدر خاص بك'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'أجهزة تتبع الصوت'; diff --git a/lib/l10n/generated/app_localizations_bn.dart b/lib/l10n/generated/app_localizations_bn.dart index a193c26f..7180a3e5 100644 --- a/lib/l10n/generated/app_localizations_bn.dart +++ b/lib/l10n/generated/app_localizations_bn.dart @@ -1505,7 +1505,7 @@ class AppLocalizationsBn extends AppLocalizations { 'ইনপুট প্রয়োজনীয় ফরম্যাটের সাথে মেলে না'; @override - String get metadata_provider_plugins => 'মেটাডেটা প্রদানকারী প্লাগইনসমূহ'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1530,8 +1530,8 @@ class AppLocalizationsBn extends AppLocalizations { String get available_plugins => 'উপলব্ধ প্লাগইনগুলো'; @override - String get configure_your_own_metadata_plugin => - 'নিজস্ব প্লেলিস্ট/অ্যালবাম/শিল্পী/ফিড মেটাডেটা প্রদানকারী কনফিগার করুন'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'অডিও স্ক্রোব্বলার্স'; diff --git a/lib/l10n/generated/app_localizations_ca.dart b/lib/l10n/generated/app_localizations_ca.dart index 694aa2c7..c9534377 100644 --- a/lib/l10n/generated/app_localizations_ca.dart +++ b/lib/l10n/generated/app_localizations_ca.dart @@ -1514,8 +1514,7 @@ class AppLocalizationsCa extends AppLocalizations { 'L’entrada no coincideix amb el format requerit'; @override - String get metadata_provider_plugins => - 'Complements de proveïdor de metadades'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1540,8 +1539,8 @@ class AppLocalizationsCa extends AppLocalizations { String get available_plugins => 'Complements disponibles'; @override - String get configure_your_own_metadata_plugin => - 'Configura el teu propi proveïdor de metadades per llistes/reproduccions àlbum/artista/flux'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblers d’àudio'; diff --git a/lib/l10n/generated/app_localizations_cs.dart b/lib/l10n/generated/app_localizations_cs.dart index 8ef0e6a9..0a73011c 100644 --- a/lib/l10n/generated/app_localizations_cs.dart +++ b/lib/l10n/generated/app_localizations_cs.dart @@ -1505,7 +1505,7 @@ class AppLocalizationsCs extends AppLocalizations { 'Vstup neodpovídá požadovanému formátu'; @override - String get metadata_provider_plugins => 'Pluginy poskytovatelů metadat'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1530,8 +1530,8 @@ class AppLocalizationsCs extends AppLocalizations { String get available_plugins => 'Dostupné pluginy'; @override - String get configure_your_own_metadata_plugin => - 'Nakonfigurujte si vlastního poskytovatele metadat pro playlist/album/umělec/fid'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Audio scrobblers'; diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 870dd76d..de636d0b 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1517,7 +1517,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Eingabe entspricht nicht dem geforderten Format'; @override - String get metadata_provider_plugins => 'Plugins für Metadatenanbieter'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1542,8 +1542,8 @@ class AppLocalizationsDe extends AppLocalizations { String get available_plugins => 'Verfügbare Plugins'; @override - String get configure_your_own_metadata_plugin => - 'Eigenen Anbieter für Playlist-/Album-/Künstler-/Feed-Metadaten konfigurieren'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Audio-Scrobbler'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 23d379c7..a86718fa 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1503,7 +1503,7 @@ class AppLocalizationsEn extends AppLocalizations { 'Input doesn\'t match the required format'; @override - String get metadata_provider_plugins => 'Metadata Provider Plugins'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1528,8 +1528,8 @@ class AppLocalizationsEn extends AppLocalizations { String get available_plugins => 'Available plugins'; @override - String get configure_your_own_metadata_plugin => - 'Configure your own playlist/album/artist/feed metadata provider'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Audio Scrobblers'; diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index f06c9399..c68ad322 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -1517,8 +1517,7 @@ class AppLocalizationsEs extends AppLocalizations { 'La entrada no coincide con el formato requerido'; @override - String get metadata_provider_plugins => - 'Complementos de proveedor de metadatos'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1543,8 +1542,8 @@ class AppLocalizationsEs extends AppLocalizations { String get available_plugins => 'Complementos disponibles'; @override - String get configure_your_own_metadata_plugin => - 'Configura tu propio proveedor de metadatos para listas/álbum/artista/feeds'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblers de audio'; diff --git a/lib/l10n/generated/app_localizations_eu.dart b/lib/l10n/generated/app_localizations_eu.dart index 296c50ed..677d24c5 100644 --- a/lib/l10n/generated/app_localizations_eu.dart +++ b/lib/l10n/generated/app_localizations_eu.dart @@ -1515,7 +1515,7 @@ class AppLocalizationsEu extends AppLocalizations { 'Sarrera ezin da beharrezko formatutik desberdina izan'; @override - String get metadata_provider_plugins => 'Metadaten hornitzailearen pluginak'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1540,8 +1540,8 @@ class AppLocalizationsEu extends AppLocalizations { String get available_plugins => 'Eskaintzen diren pluginak'; @override - String get configure_your_own_metadata_plugin => - 'Konfiguratu zureko playlists-/album-/artista-/feed-metadaten hornitzailea'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Audio scrobbler-ak'; diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index a1203c57..8cda0e00 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -1503,7 +1503,7 @@ class AppLocalizationsFa extends AppLocalizations { 'ورودی با قالب مورد نیاز تطابق ندارد'; @override - String get metadata_provider_plugins => 'افزونه‌های ارائه‌دهندهٔ متادیتا'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1528,8 +1528,8 @@ class AppLocalizationsFa extends AppLocalizations { String get available_plugins => 'افزونه‌های موجود'; @override - String get configure_your_own_metadata_plugin => - 'پیکربندی ارائه‌دهندهٔ متادیتا برای پلی‌لیست/آلبوم/هنرمند/فید به‌صورت سفارشی'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'اسکراب‌بلرهای صوتی'; diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index b9ee4de6..8342d7ec 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -1503,7 +1503,7 @@ class AppLocalizationsFi extends AppLocalizations { String get input_does_not_match_format => 'Syöte ei vastaa vaadittua muotoa'; @override - String get metadata_provider_plugins => 'Metatietojen tarjoajan lisäosat'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1528,8 +1528,8 @@ class AppLocalizationsFi extends AppLocalizations { String get available_plugins => 'Saatavilla olevat lisäosat'; @override - String get configure_your_own_metadata_plugin => - 'Määritä oma soittolistan/albumin/artistin/syötteen metatietojen tarjoaja'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Äänen scrobblerit'; diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index daee5667..07f42798 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1521,8 +1521,7 @@ class AppLocalizationsFr extends AppLocalizations { 'L\'entrée ne correspond pas au format requis'; @override - String get metadata_provider_plugins => - 'Plugins de fournisseur de métadonnées'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1548,8 +1547,8 @@ class AppLocalizationsFr extends AppLocalizations { String get available_plugins => 'Plugins disponibles'; @override - String get configure_your_own_metadata_plugin => - 'Configurer votre propre fournisseur de métadonnées de playlist/album/artiste/flux'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblers audio'; diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index 65279d70..7cd951b4 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -1509,7 +1509,7 @@ class AppLocalizationsHi extends AppLocalizations { 'इनपुट आवश्यक प्रारूप से मेल नहीं खाता है'; @override - String get metadata_provider_plugins => 'मेटाडेटा प्रदाता प्लगइन'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1534,8 +1534,8 @@ class AppLocalizationsHi extends AppLocalizations { String get available_plugins => 'उपलब्ध प्लगइन'; @override - String get configure_your_own_metadata_plugin => - 'अपनी खुद की प्लेलिस्ट/एल्बम/कलाकार/फ़ीड मेटाडेटा प्रदाता कॉन्फ़िगर करें'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'ऑडियो स्क्रॉबलर्स'; diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index 1e0b9f9f..ad2edbfe 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -1511,7 +1511,7 @@ class AppLocalizationsId extends AppLocalizations { 'Masukan tidak cocok dengan format yang diperlukan'; @override - String get metadata_provider_plugins => 'Plugin Penyedia Metadata'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1536,8 +1536,8 @@ class AppLocalizationsId extends AppLocalizations { String get available_plugins => 'Plugin yang tersedia'; @override - String get configure_your_own_metadata_plugin => - 'Konfigurasi penyedia metadata playlist/album/artis/feed Anda sendiri'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblers Audio'; diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index f92eae63..5e75ac08 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1510,7 +1510,7 @@ class AppLocalizationsIt extends AppLocalizations { 'L\'input non corrisponde al formato richiesto'; @override - String get metadata_provider_plugins => 'Plugin del provider di metadati'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1535,8 +1535,8 @@ class AppLocalizationsIt extends AppLocalizations { String get available_plugins => 'Plugin disponibili'; @override - String get configure_your_own_metadata_plugin => - 'Configura il tuo provider di metadati per playlist/album/artista/feed'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobbler audio'; diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index 0e3d98ab..a2500d63 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -1474,7 +1474,7 @@ class AppLocalizationsJa extends AppLocalizations { String get input_does_not_match_format => '入力が必須フォーマットと一致しません'; @override - String get metadata_provider_plugins => 'メタデータプロバイダープラグイン'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1499,8 +1499,8 @@ class AppLocalizationsJa extends AppLocalizations { String get available_plugins => '利用可能なプラグイン'; @override - String get configure_your_own_metadata_plugin => - '独自のプレイリスト/アルバム/アーティスト/フィードのメタデータプロバイダーを構成'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'オーディオスクロッブラー'; diff --git a/lib/l10n/generated/app_localizations_ka.dart b/lib/l10n/generated/app_localizations_ka.dart index 22d3246f..312c9ccd 100644 --- a/lib/l10n/generated/app_localizations_ka.dart +++ b/lib/l10n/generated/app_localizations_ka.dart @@ -1511,8 +1511,7 @@ class AppLocalizationsKa extends AppLocalizations { 'შეყვანა არ ემთხვევა საჭირო ფორმატს'; @override - String get metadata_provider_plugins => - 'მეტამონაცემების პროვაიდერების პლაგინები'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1537,8 +1536,8 @@ class AppLocalizationsKa extends AppLocalizations { String get available_plugins => 'ხელმისაწვდომი პლაგინები'; @override - String get configure_your_own_metadata_plugin => - 'დააყენეთ თქვენი საკუთარი პლეილისტის/ალბომის/არტისტის/ფიდის მეტამონაცემების პროვაიდერი'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'აუდიო სქრობლერები'; diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index 19b7e544..47f9ad68 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -1479,7 +1479,7 @@ class AppLocalizationsKo extends AppLocalizations { String get input_does_not_match_format => '입력이 필요한 형식과 일치하지 않습니다'; @override - String get metadata_provider_plugins => '메타데이터 제공자 플러그인'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1503,8 +1503,8 @@ class AppLocalizationsKo extends AppLocalizations { String get available_plugins => '사용 가능한 플러그인'; @override - String get configure_your_own_metadata_plugin => - '자신만의 플레이리스트/앨범/아티스트/피드 메타데이터 제공자 구성'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => '오디오 스크로블러'; diff --git a/lib/l10n/generated/app_localizations_ne.dart b/lib/l10n/generated/app_localizations_ne.dart index 53bcb184..fde29abc 100644 --- a/lib/l10n/generated/app_localizations_ne.dart +++ b/lib/l10n/generated/app_localizations_ne.dart @@ -1515,7 +1515,7 @@ class AppLocalizationsNe extends AppLocalizations { String get input_does_not_match_format => 'इनपुट आवश्यक ढाँचासँग मेल खाँदैन'; @override - String get metadata_provider_plugins => 'मेटाडेटा प्रदायक प्लगइनहरू'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1540,8 +1540,8 @@ class AppLocalizationsNe extends AppLocalizations { String get available_plugins => 'उपलब्ध प्लगइनहरू'; @override - String get configure_your_own_metadata_plugin => - 'तपाईंको आफ्नै प्लेलिस्ट/एल्बम/कलाकार/फिड मेटाडेटा प्रदायक कन्फिगर गर्नुहोस्'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'अडियो स्क्रब्बलरहरू'; diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index dd73f907..29cc6c27 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -1509,7 +1509,7 @@ class AppLocalizationsNl extends AppLocalizations { 'Invoer komt niet overeen met het vereiste formaat'; @override - String get metadata_provider_plugins => 'Metadata-aanbieder Plugins'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1534,8 +1534,8 @@ class AppLocalizationsNl extends AppLocalizations { String get available_plugins => 'Beschikbare plugins'; @override - String get configure_your_own_metadata_plugin => - 'Configureer uw eigen metadata-aanbieder voor afspeellijst/album/artiest/feed'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Audioscrobblers'; diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index 095242bc..32ffe065 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -1511,7 +1511,7 @@ class AppLocalizationsPl extends AppLocalizations { 'Wprowadzony tekst nie pasuje do wymaganego formatu'; @override - String get metadata_provider_plugins => 'Wtyczki dostawców metadanych'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1536,8 +1536,8 @@ class AppLocalizationsPl extends AppLocalizations { String get available_plugins => 'Dostępne wtyczki'; @override - String get configure_your_own_metadata_plugin => - 'Skonfiguruj własnego dostawcę metadanych dla playlisty/albumu/artysty/kanału'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblery audio'; diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 4b1fd2bc..0df60587 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -1508,7 +1508,7 @@ class AppLocalizationsPt extends AppLocalizations { 'A entrada não corresponde ao formato exigido'; @override - String get metadata_provider_plugins => 'Plugins do provedor de metadados'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1533,8 +1533,8 @@ class AppLocalizationsPt extends AppLocalizations { String get available_plugins => 'Plugins disponíveis'; @override - String get configure_your_own_metadata_plugin => - 'Configure seu próprio provedor de metadados de playlist/álbum/artista/feed'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblers de áudio'; diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 9cd091e7..9399f981 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -1511,7 +1511,7 @@ class AppLocalizationsRu extends AppLocalizations { 'Введенные данные не соответствуют требуемому формату'; @override - String get metadata_provider_plugins => 'Плагины поставщика метаданных'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1536,8 +1536,8 @@ class AppLocalizationsRu extends AppLocalizations { String get available_plugins => 'Доступные плагины'; @override - String get configure_your_own_metadata_plugin => - 'Настройте свой собственный поставщик метаданных для плейлиста/альбома/артиста/ленты'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Аудио скробблеры'; diff --git a/lib/l10n/generated/app_localizations_ta.dart b/lib/l10n/generated/app_localizations_ta.dart index 204832b2..e6c1d29f 100644 --- a/lib/l10n/generated/app_localizations_ta.dart +++ b/lib/l10n/generated/app_localizations_ta.dart @@ -1517,7 +1517,7 @@ class AppLocalizationsTa extends AppLocalizations { 'உள்ளீடு தேவையான வடிவத்துடன் பொருந்தவில்லை'; @override - String get metadata_provider_plugins => 'மெட்டாடேட்டா வழங்குநர் பிளகின்கள்'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1542,8 +1542,8 @@ class AppLocalizationsTa extends AppLocalizations { String get available_plugins => 'கிடைக்கக்கூடிய பிளகின்கள்'; @override - String get configure_your_own_metadata_plugin => - 'உங்கள் சொந்த பிளேலிஸ்ட்/ஆல்பம்/கலைஞர்/ஊட்ட மெட்டாடேட்டா வழங்குநரை உள்ளமைக்கவும்'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'ஆடியோ ஸ்க்ரோப்ளர்கள்'; diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index 5ea104a7..aea6f623 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -1500,7 +1500,7 @@ class AppLocalizationsTh extends AppLocalizations { String get input_does_not_match_format => 'อินพุตไม่ตรงกับรูปแบบที่ต้องการ'; @override - String get metadata_provider_plugins => 'ปลั๊กอินผู้ให้บริการเมตาดาต้า'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1525,8 +1525,8 @@ class AppLocalizationsTh extends AppLocalizations { String get available_plugins => 'ปลั๊กอินที่มีอยู่'; @override - String get configure_your_own_metadata_plugin => - 'กำหนดค่าผู้ให้บริการเมตาดาต้าเพลย์ลิสต์/อัลบั้ม/ศิลปิน/ฟีดของคุณเอง'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'เครื่อง scrobbler เสียง'; diff --git a/lib/l10n/generated/app_localizations_tl.dart b/lib/l10n/generated/app_localizations_tl.dart index de1916e5..ff2ae5da 100644 --- a/lib/l10n/generated/app_localizations_tl.dart +++ b/lib/l10n/generated/app_localizations_tl.dart @@ -1518,7 +1518,7 @@ class AppLocalizationsTl extends AppLocalizations { 'Ang input ay hindi tumutugma sa kinakailangang format'; @override - String get metadata_provider_plugins => 'Mga Plugin ng Metadata Provider'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1543,8 +1543,8 @@ class AppLocalizationsTl extends AppLocalizations { String get available_plugins => 'Mga available na plugin'; @override - String get configure_your_own_metadata_plugin => - 'I-configure ang iyong sariling playlist/album/artist/feed metadata provider'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Mga Audio Scrobbler'; diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index e6740bb3..64f07b6e 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -1511,7 +1511,7 @@ class AppLocalizationsTr extends AppLocalizations { String get input_does_not_match_format => 'Girdi, gerekli biçimle eşleşmiyor'; @override - String get metadata_provider_plugins => 'Meta Veri Sağlayıcısı Eklentileri'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1536,8 +1536,8 @@ class AppLocalizationsTr extends AppLocalizations { String get available_plugins => 'Mevcut eklentiler'; @override - String get configure_your_own_metadata_plugin => - 'Kendi çalma listenizi/albümünüzü/sanatçınızı/akış meta veri sağlayıcınızı yapılandırın'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Ses Scrobbler\'lar'; diff --git a/lib/l10n/generated/app_localizations_uk.dart b/lib/l10n/generated/app_localizations_uk.dart index aba38ac3..59414ca7 100644 --- a/lib/l10n/generated/app_localizations_uk.dart +++ b/lib/l10n/generated/app_localizations_uk.dart @@ -1507,7 +1507,7 @@ class AppLocalizationsUk extends AppLocalizations { 'Введені дані не відповідають необхідному формату'; @override - String get metadata_provider_plugins => 'Плагіни провайдера метаданих'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1532,8 +1532,8 @@ class AppLocalizationsUk extends AppLocalizations { String get available_plugins => 'Доступні плагіни'; @override - String get configure_your_own_metadata_plugin => - 'Налаштуйте свій власний провайдер метаданих для плейлиста/альбому/виконавця/стрічки'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Аудіо скробблери'; diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index ec449549..0b980dfd 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -1513,7 +1513,7 @@ class AppLocalizationsVi extends AppLocalizations { 'Đầu vào không khớp với định dạng yêu cầu'; @override - String get metadata_provider_plugins => 'Plugin Nhà cung cấp siêu dữ liệu'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1538,8 +1538,8 @@ class AppLocalizationsVi extends AppLocalizations { String get available_plugins => 'Các plugin có sẵn'; @override - String get configure_your_own_metadata_plugin => - 'Cấu hình nhà cung cấp siêu dữ liệu danh sách phát/album/nghệ sĩ/nguồn cấp dữ liệu của riêng bạn'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Bộ scrobbler âm thanh'; diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index c9e18e72..e2845fe3 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -1469,7 +1469,7 @@ class AppLocalizationsZh extends AppLocalizations { String get input_does_not_match_format => '输入与所需格式不匹配'; @override - String get metadata_provider_plugins => '元数据提供者插件'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1493,7 +1493,8 @@ class AppLocalizationsZh extends AppLocalizations { String get available_plugins => '可用插件'; @override - String get configure_your_own_metadata_plugin => '配置您自己的播放列表/专辑/艺人/订阅元数据提供者'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => '音频 Scrobblers'; @@ -2976,9 +2977,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get input_does_not_match_format => '輸入不符合所需格式'; - @override - String get metadata_provider_plugins => '中繼資料供應商外掛程式'; - @override String get paste_plugin_download_url => '貼上下載網址、GitHub/Codeberg 儲存庫網址或 .smplug 檔案的直接連結'; @@ -3000,9 +2998,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get available_plugins => '可用的外掛程式'; - @override - String get configure_your_own_metadata_plugin => '設定您自己的播放清單/專輯/藝人/動態中繼資料供應商'; - @override String get audio_scrobblers => '音訊 Scrobblers'; diff --git a/lib/models/metadata/metadata.freezed.dart b/lib/models/metadata/metadata.freezed.dart index 5d4bc695..f54ee379 100644 --- a/lib/models/metadata/metadata.freezed.dart +++ b/lib/models/metadata/metadata.freezed.dart @@ -6539,6 +6539,7 @@ mixin _$MetadataPluginRepository { String get owner => throw _privateConstructorUsedError; String get description => throw _privateConstructorUsedError; String get repoUrl => throw _privateConstructorUsedError; + List get topics => throw _privateConstructorUsedError; /// Serializes this MetadataPluginRepository to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -6556,7 +6557,12 @@ abstract class $MetadataPluginRepositoryCopyWith<$Res> { $Res Function(MetadataPluginRepository) then) = _$MetadataPluginRepositoryCopyWithImpl<$Res, MetadataPluginRepository>; @useResult - $Res call({String name, String owner, String description, String repoUrl}); + $Res call( + {String name, + String owner, + String description, + String repoUrl, + List topics}); } /// @nodoc @@ -6579,6 +6585,7 @@ class _$MetadataPluginRepositoryCopyWithImpl<$Res, Object? owner = null, Object? description = null, Object? repoUrl = null, + Object? topics = null, }) { return _then(_value.copyWith( name: null == name @@ -6597,6 +6604,10 @@ class _$MetadataPluginRepositoryCopyWithImpl<$Res, ? _value.repoUrl : repoUrl // ignore: cast_nullable_to_non_nullable as String, + topics: null == topics + ? _value.topics + : topics // ignore: cast_nullable_to_non_nullable + as List, ) as $Val); } } @@ -6610,7 +6621,12 @@ abstract class _$$MetadataPluginRepositoryImplCopyWith<$Res> __$$MetadataPluginRepositoryImplCopyWithImpl<$Res>; @override @useResult - $Res call({String name, String owner, String description, String repoUrl}); + $Res call( + {String name, + String owner, + String description, + String repoUrl, + List topics}); } /// @nodoc @@ -6632,6 +6648,7 @@ class __$$MetadataPluginRepositoryImplCopyWithImpl<$Res> Object? owner = null, Object? description = null, Object? repoUrl = null, + Object? topics = null, }) { return _then(_$MetadataPluginRepositoryImpl( name: null == name @@ -6650,6 +6667,10 @@ class __$$MetadataPluginRepositoryImplCopyWithImpl<$Res> ? _value.repoUrl : repoUrl // ignore: cast_nullable_to_non_nullable as String, + topics: null == topics + ? _value._topics + : topics // ignore: cast_nullable_to_non_nullable + as List, )); } } @@ -6661,7 +6682,9 @@ class _$MetadataPluginRepositoryImpl implements _MetadataPluginRepository { {required this.name, required this.owner, required this.description, - required this.repoUrl}); + required this.repoUrl, + required final List topics}) + : _topics = topics; factory _$MetadataPluginRepositoryImpl.fromJson(Map json) => _$$MetadataPluginRepositoryImplFromJson(json); @@ -6674,10 +6697,17 @@ class _$MetadataPluginRepositoryImpl implements _MetadataPluginRepository { final String description; @override final String repoUrl; + final List _topics; + @override + List get topics { + if (_topics is EqualUnmodifiableListView) return _topics; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_topics); + } @override String toString() { - return 'MetadataPluginRepository(name: $name, owner: $owner, description: $description, repoUrl: $repoUrl)'; + return 'MetadataPluginRepository(name: $name, owner: $owner, description: $description, repoUrl: $repoUrl, topics: $topics)'; } @override @@ -6689,13 +6719,14 @@ class _$MetadataPluginRepositoryImpl implements _MetadataPluginRepository { (identical(other.owner, owner) || other.owner == owner) && (identical(other.description, description) || other.description == description) && - (identical(other.repoUrl, repoUrl) || other.repoUrl == repoUrl)); + (identical(other.repoUrl, repoUrl) || other.repoUrl == repoUrl) && + const DeepCollectionEquality().equals(other._topics, _topics)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => - Object.hash(runtimeType, name, owner, description, repoUrl); + int get hashCode => Object.hash(runtimeType, name, owner, description, + repoUrl, const DeepCollectionEquality().hash(_topics)); /// Create a copy of MetadataPluginRepository /// with the given fields replaced by the non-null parameter values. @@ -6719,7 +6750,8 @@ abstract class _MetadataPluginRepository implements MetadataPluginRepository { {required final String name, required final String owner, required final String description, - required final String repoUrl}) = _$MetadataPluginRepositoryImpl; + required final String repoUrl, + required final List topics}) = _$MetadataPluginRepositoryImpl; factory _MetadataPluginRepository.fromJson(Map json) = _$MetadataPluginRepositoryImpl.fromJson; @@ -6732,6 +6764,8 @@ abstract class _MetadataPluginRepository implements MetadataPluginRepository { String get description; @override String get repoUrl; + @override + List get topics; /// Create a copy of MetadataPluginRepository /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/metadata/metadata.g.dart b/lib/models/metadata/metadata.g.dart index 9c45cb7c..7497053c 100644 --- a/lib/models/metadata/metadata.g.dart +++ b/lib/models/metadata/metadata.g.dart @@ -603,6 +603,8 @@ _$MetadataPluginRepositoryImpl _$$MetadataPluginRepositoryImplFromJson( owner: json['owner'] as String, description: json['description'] as String, repoUrl: json['repoUrl'] as String, + topics: + (json['topics'] as List).map((e) => e as String).toList(), ); Map _$$MetadataPluginRepositoryImplToJson( @@ -612,4 +614,5 @@ Map _$$MetadataPluginRepositoryImplToJson( 'owner': instance.owner, 'description': instance.description, 'repoUrl': instance.repoUrl, + 'topics': instance.topics, }; diff --git a/lib/models/metadata/repository.dart b/lib/models/metadata/repository.dart index 06151dee..2a83f791 100644 --- a/lib/models/metadata/repository.dart +++ b/lib/models/metadata/repository.dart @@ -7,6 +7,7 @@ class MetadataPluginRepository with _$MetadataPluginRepository { required String owner, required String description, required String repoUrl, + required List topics, }) = _MetadataPluginRepository; factory MetadataPluginRepository.fromJson(Map json) => diff --git a/lib/modules/metadata_plugins/installed_plugin.dart b/lib/modules/metadata_plugins/installed_plugin.dart index ea8cbf29..523fb335 100644 --- a/lib/modules/metadata_plugins/installed_plugin.dart +++ b/lib/modules/metadata_plugins/installed_plugin.dart @@ -5,6 +5,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/markdown/markdown.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/modules/metadata_plugins/plugin_repository.dart'; import 'package:spotube/modules/metadata_plugins/plugin_update_available_dialog.dart'; import 'package:spotube/provider/metadata_plugin/core/auth.dart'; import 'package:spotube/provider/metadata_plugin/core/support.dart'; @@ -12,6 +13,11 @@ import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart'; import 'package:url_launcher/url_launcher.dart'; +final validAbilities = { + PluginAbilities.metadata: ("Metadata", SpotubeIcons.album), + PluginAbilities.audioSource: ("Audio Source", SpotubeIcons.music), +}; + class MetadataInstalledPluginItem extends HookConsumerWidget { final PluginConfiguration plugin; final bool isDefault; @@ -79,6 +85,18 @@ class MetadataInstalledPluginItem extends HookConsumerWidget { spacing: 8, children: [ Text(plugin.description), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final ability in plugin.abilities) + if (validAbilities.keys.contains(ability)) + SecondaryBadge( + leading: Icon(validAbilities[ability]!.$2), + child: Text(validAbilities[ability]!.$1), + ), + ], + ), if (repoUrl != null) Wrap( spacing: 8, diff --git a/lib/modules/metadata_plugins/plugin_repository.dart b/lib/modules/metadata_plugins/plugin_repository.dart index c303c46b..9bd71f0a 100644 --- a/lib/modules/metadata_plugins/plugin_repository.dart +++ b/lib/modules/metadata_plugins/plugin_repository.dart @@ -11,6 +11,11 @@ import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:change_case/change_case.dart'; +final validTopics = { + "spotube-metadata-plugin": ("Metadata", SpotubeIcons.album), + "spotube-audio-source-plugin": ("Audio Source", SpotubeIcons.music), +}; + class MetadataPluginRepositoryItem extends HookConsumerWidget { final MetadataPluginRepository pluginRepo; const MetadataPluginRepositoryItem({ @@ -208,6 +213,12 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget { ), ), ], + for (final topic in pluginRepo.topics) + if (validTopics.keys.contains(topic)) + SecondaryBadge( + leading: Icon(validTopics[topic]!.$2), + child: Text(validTopics[topic]!.$1), + ), SecondaryBadge( leading: host == "github.com" ? const Icon(SpotubeIcons.github) diff --git a/lib/pages/settings/metadata_plugins.dart b/lib/pages/settings/metadata_plugins.dart index 6698a67f..3601a06d 100644 --- a/lib/pages/settings/metadata_plugins.dart +++ b/lib/pages/settings/metadata_plugins.dart @@ -30,6 +30,7 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final tabState = useState(0); final formKey = useMemoized(() => GlobalKey(), []); final plugins = ref.watch(metadataPluginsProvider); @@ -49,11 +50,30 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { final pluginRepos = pluginReposSnapshot.asData?.value.items ?? []; if (installedPluginIds.isEmpty) return pluginRepos; - return pluginRepos + final availablePlugins = pluginRepos .whereNot((repo) => installedPluginIds.contains(repo.repoUrl)) .toList(); + + if (tabState.value != 0) { + // metadata only plugins + return availablePlugins + .where( + (d) => d.topics.contains( + tabState.value == 1 + ? "spotube-metadata-plugin" + : "spotube-audio-source-plugin", + ), + ) + .toList(); + } + + return availablePlugins; // all plugins }, - [plugins.asData?.value.plugins, pluginReposSnapshot.asData?.value], + [ + plugins.asData?.value.plugins, + pluginReposSnapshot.asData?.value, + tabState.value, + ], ); return SafeArea( @@ -61,7 +81,7 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { child: Scaffold( headers: [ TitleBar( - title: Text(context.l10n.metadata_provider_plugins), + title: Text(context.l10n.plugins), ) ], child: Padding( @@ -193,6 +213,20 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { ), ), const SliverGap(12), + SliverToBoxAdapter( + child: TabList( + index: tabState.value, + onChanged: (value) { + tabState.value = value; + }, + children: const [ + TabItem(child: Text("All")), + TabItem(child: Text("Metadata")), + TabItem(child: Text("Audio Source")), + ], + ), + ), + const SliverGap(12), if (plugins.asData?.value.plugins.isNotEmpty ?? false) SliverToBoxAdapter( child: Row( @@ -249,6 +283,7 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { description: "Loading...", repoUrl: "", owner: "", + topics: [], ), ), ); diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index af8e1b80..ca859ada 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -21,8 +21,8 @@ class SettingsAccountSection extends HookConsumerWidget { children: [ ListTile( leading: const Icon(SpotubeIcons.extensions), - title: Text(context.l10n.metadata_provider_plugins), - subtitle: Text(context.l10n.configure_your_own_metadata_plugin), + title: Text(context.l10n.plugins), + subtitle: Text(context.l10n.configure_plugins), onTap: () { context.pushRoute(const SettingsMetadataProviderRoute()); }, diff --git a/lib/provider/metadata_plugin/core/repositories.dart b/lib/provider/metadata_plugin/core/repositories.dart index 55c11ed2..a78f63d9 100644 --- a/lib/provider/metadata_plugin/core/repositories.dart +++ b/lib/provider/metadata_plugin/core/repositories.dart @@ -49,6 +49,7 @@ class MetadataPluginRepositoriesNotifier owner: repo["owner"]["login"] ?? "", description: repo["description"] ?? "", repoUrl: repo["html_url"] ?? "", + topics: repo["topics"].cast() ?? [], ); }).toList(); diff --git a/untranslated_messages.json b/untranslated_messages.json index ba110540..618ddcf8 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,101 +1,135 @@ { "ar": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "bn": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ca": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "cs": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "de": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "es": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "eu": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "fa": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "fi": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "fr": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "hi": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "id": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "it": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ja": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ka": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ko": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ne": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" @@ -103,72 +137,96 @@ "nl": [ "audio_source", + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "pl": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "pt": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ru": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ta": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "th": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "tl": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "tr": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "uk": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "vi": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "zh": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "zh_TW": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" From 3bc296cf224dbede75beaf02385a9ebf8cf55e5b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 25 Oct 2025 23:23:27 +0600 Subject: [PATCH 03/14] feat: add setting default audio source support --- drift_schemas/app_db/drift_schema_v9.json | 1 + lib/l10n/app_en.arb | 5 +- lib/l10n/generated/app_localizations.dart | 24 +- lib/l10n/generated/app_localizations_ar.dart | 11 +- lib/l10n/generated/app_localizations_bn.dart | 11 +- lib/l10n/generated/app_localizations_ca.dart | 11 +- lib/l10n/generated/app_localizations_cs.dart | 11 +- lib/l10n/generated/app_localizations_de.dart | 11 +- lib/l10n/generated/app_localizations_en.dart | 11 +- lib/l10n/generated/app_localizations_es.dart | 11 +- lib/l10n/generated/app_localizations_eu.dart | 11 +- lib/l10n/generated/app_localizations_fa.dart | 11 +- lib/l10n/generated/app_localizations_fi.dart | 11 +- lib/l10n/generated/app_localizations_fr.dart | 11 +- lib/l10n/generated/app_localizations_hi.dart | 11 +- lib/l10n/generated/app_localizations_id.dart | 11 +- lib/l10n/generated/app_localizations_it.dart | 11 +- lib/l10n/generated/app_localizations_ja.dart | 11 +- lib/l10n/generated/app_localizations_ka.dart | 11 +- lib/l10n/generated/app_localizations_ko.dart | 11 +- lib/l10n/generated/app_localizations_ne.dart | 11 +- lib/l10n/generated/app_localizations_nl.dart | 11 +- lib/l10n/generated/app_localizations_pl.dart | 11 +- lib/l10n/generated/app_localizations_pt.dart | 11 +- lib/l10n/generated/app_localizations_ru.dart | 11 +- lib/l10n/generated/app_localizations_ta.dart | 11 +- lib/l10n/generated/app_localizations_th.dart | 11 +- lib/l10n/generated/app_localizations_tl.dart | 11 +- lib/l10n/generated/app_localizations_tr.dart | 11 +- lib/l10n/generated/app_localizations_uk.dart | 11 +- lib/l10n/generated/app_localizations_vi.dart | 11 +- lib/l10n/generated/app_localizations_zh.dart | 14 +- lib/main.dart | 6 +- lib/models/database/database.dart | 16 +- lib/models/database/database.g.dart | 377 +- lib/models/database/database.steps.dart | 274 +- .../database/tables/metadata_plugins.dart | 9 +- .../metadata_plugins/installed_plugin.dart | 290 +- .../root/use_global_subscriptions.dart | 2 +- lib/pages/settings/metadata_plugins.dart | 45 +- lib/provider/metadata_plugin/core/auth.dart | 35 +- .../metadata_plugin/core/scrobble.dart | 6 +- .../metadata_plugin/core/support.dart | 10 + .../metadata_plugin_provider.dart | 168 +- .../updater/update_checker.dart | 19 +- test/drift/app_db/generated/schema.dart | 43 +- test/drift/app_db/generated/schema_v1.dart | 2 +- test/drift/app_db/generated/schema_v2.dart | 2 +- test/drift/app_db/generated/schema_v3.dart | 2 +- test/drift/app_db/generated/schema_v4.dart | 2 +- test/drift/app_db/generated/schema_v5.dart | 2 +- test/drift/app_db/generated/schema_v6.dart | 2 +- test/drift/app_db/generated/schema_v7.dart | 2 +- test/drift/app_db/generated/schema_v8.dart | 2 +- test/drift/app_db/generated/schema_v9.dart | 3568 +++++++++++++++++ untranslated_messages.json | 116 + 56 files changed, 4960 insertions(+), 392 deletions(-) create mode 100644 drift_schemas/app_db/drift_schema_v9.json create mode 100644 test/drift/app_db/generated/schema_v9.dart diff --git a/drift_schemas/app_db/drift_schema_v9.json b/drift_schemas/app_db/drift_schema_v9.json new file mode 100644 index 00000000..73af2588 --- /dev/null +++ b/drift_schemas/app_db/drift_schema_v9.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"authentication_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"cookie","getter_name":"cookie","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"access_token","getter_name":"accessToken","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"expiration","getter_name":"expiration","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"blacklist_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"element_type","getter_name":"elementType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BlacklistedType.values)","dart_type_name":"BlacklistedType"}},{"name":"element_id","getter_name":"elementId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"preferences_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"audio_quality","getter_name":"audioQuality","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceQualities.high.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceQualities.values)","dart_type_name":"SourceQualities"}},{"name":"album_color_sync","getter_name":"albumColorSync","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"album_color_sync\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"album_color_sync\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"amoled_dark_theme","getter_name":"amoledDarkTheme","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"amoled_dark_theme\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"amoled_dark_theme\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"check_update","getter_name":"checkUpdate","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"check_update\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"check_update\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"normalize_audio","getter_name":"normalizeAudio","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"normalize_audio\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"normalize_audio\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"show_system_tray_icon","getter_name":"showSystemTrayIcon","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"show_system_tray_icon\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"show_system_tray_icon\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"system_title_bar","getter_name":"systemTitleBar","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"system_title_bar\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"system_title_bar\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"skip_non_music","getter_name":"skipNonMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"skip_non_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"skip_non_music\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"close_behavior","getter_name":"closeBehavior","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(CloseBehavior.close.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(CloseBehavior.values)","dart_type_name":"CloseBehavior"}},{"name":"accent_color_scheme","getter_name":"accentColorScheme","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"Slate:0xff64748b\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeColorConverter()","dart_type_name":"SpotubeColor"}},{"name":"layout_mode","getter_name":"layoutMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(LayoutMode.adaptive.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(LayoutMode.values)","dart_type_name":"LayoutMode"}},{"name":"locale","getter_name":"locale","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('{\"languageCode\":\"system\",\"countryCode\":\"system\"}')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const LocaleConverter()","dart_type_name":"Locale"}},{"name":"market","getter_name":"market","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(Market.US.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(Market.values)","dart_type_name":"Market"}},{"name":"search_mode","getter_name":"searchMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SearchMode.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SearchMode.values)","dart_type_name":"SearchMode"}},{"name":"download_location","getter_name":"downloadLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[]},{"name":"local_library_location","getter_name":"localLibraryLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"piped_instance","getter_name":"pipedInstance","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"https://pipedapi.kavin.rocks\")","default_client_dart":null,"dsl_features":[]},{"name":"invidious_instance","getter_name":"invidiousInstance","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"https://inv.nadeko.net\")","default_client_dart":null,"dsl_features":[]},{"name":"theme_mode","getter_name":"themeMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(ThemeMode.system.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ThemeMode.values)","dart_type_name":"ThemeMode"}},{"name":"audio_source","getter_name":"audioSource","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(AudioSource.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(AudioSource.values)","dart_type_name":"AudioSource"}},{"name":"youtube_client_engine","getter_name":"youtubeClientEngine","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(YoutubeClientEngine.youtubeExplode.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(YoutubeClientEngine.values)","dart_type_name":"YoutubeClientEngine"}},{"name":"stream_music_codec","getter_name":"streamMusicCodec","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceCodecs.weba.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceCodecs.values)","dart_type_name":"SourceCodecs"}},{"name":"download_music_codec","getter_name":"downloadMusicCodec","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceCodecs.m4a.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceCodecs.values)","dart_type_name":"SourceCodecs"}},{"name":"discord_presence","getter_name":"discordPresence","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"discord_presence\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"discord_presence\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"endless_playback","getter_name":"endlessPlayback","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"endless_playback\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"endless_playback\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"enable_connect","getter_name":"enableConnect","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"enable_connect\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"enable_connect\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"connect_port","getter_name":"connectPort","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(-1)","default_client_dart":null,"dsl_features":[]},{"name":"cache_music","getter_name":"cacheMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"cache_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"cache_music\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":3,"references":[],"type":"table","data":{"name":"scrobbler_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"username","getter_name":"username","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"password_hash","getter_name":"passwordHash","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":4,"references":[],"type":"table","data":{"name":"skip_segment_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"start","getter_name":"start","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"end","getter_name":"end","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":5,"references":[],"type":"table","data":{"name":"source_match_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_id","getter_name":"sourceId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceType.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceType.values)","dart_type_name":"SourceType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":6,"references":[],"type":"table","data":{"name":"audio_player_state_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"playing","getter_name":"playing","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"playing\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"playing\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"loop_mode","getter_name":"loopMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(PlaylistMode.values)","dart_type_name":"PlaylistMode"}},{"name":"shuffled","getter_name":"shuffled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"shuffled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"shuffled\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"collections","getter_name":"collections","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"tracks","getter_name":"tracks","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"[]\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeTrackObjectListConverter()","dart_type_name":"List"}},{"name":"current_index","getter_name":"currentIndex","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":7,"references":[],"type":"table","data":{"name":"history_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(HistoryEntryType.values)","dart_type_name":"HistoryEntryType"}},{"name":"item_id","getter_name":"itemId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MapTypeConverter()","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":8,"references":[],"type":"table","data":{"name":"lyrics_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"SubtitleTypeConverter()","dart_type_name":"SubtitleSimple"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":9,"references":[],"type":"table","data":{"name":"plugins_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":50}}]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"version","getter_name":"version","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"author","getter_name":"author","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"entry_point","getter_name":"entryPoint","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"apis","getter_name":"apis","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"abilities","getter_name":"abilities","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"selected_for_metadata","getter_name":"selectedForMetadata","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"selected_for_metadata\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"selected_for_metadata\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"selected_for_audio_source","getter_name":"selectedForAudioSource","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"selected_for_audio_source\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"selected_for_audio_source\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"repository","getter_name":"repository","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"plugin_api_version","getter_name":"pluginApiVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('2.0.0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"unique_blacklist","sql":null,"unique":true,"columns":["element_type","element_id"]}},{"id":11,"references":[5],"type":"index","data":{"on":5,"name":"uniq_track_match","sql":null,"unique":true,"columns":["track_id","source_id","source_type"]}}]} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8c965aa1..a292105d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -434,7 +434,10 @@ "update_available": "Update available", "supports_scrobbling": "Supports scrobbling", "plugin_scrobbling_info": "This plugin scrobbles your music to generate your listening history.", - "default_plugin": "Default", + "default_metadata_source": "Default metadata source", + "set_default_metadata_source": "Set default metadata source", + "default_audio_source": "Default audio source", + "set_default_audio_source": "Set default audio source", "set_default": "Set default", "support": "Support", "support_plugin_development": "Support plugin development", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 8d0610ad..0ffcffff 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -2763,11 +2763,29 @@ abstract class AppLocalizations { /// **'This plugin scrobbles your music to generate your listening history.'** String get plugin_scrobbling_info; - /// No description provided for @default_plugin. + /// No description provided for @default_metadata_source. /// /// In en, this message translates to: - /// **'Default'** - String get default_plugin; + /// **'Default metadata source'** + String get default_metadata_source; + + /// No description provided for @set_default_metadata_source. + /// + /// In en, this message translates to: + /// **'Set default metadata source'** + String get set_default_metadata_source; + + /// No description provided for @default_audio_source. + /// + /// In en, this message translates to: + /// **'Default audio source'** + String get default_audio_source; + + /// No description provided for @set_default_audio_source. + /// + /// In en, this message translates to: + /// **'Set default audio source'** + String get set_default_audio_source; /// No description provided for @set_default. /// diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index 09ec2505..05da9c97 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -1443,7 +1443,16 @@ class AppLocalizationsAr extends AppLocalizations { 'تقوم هذه الإضافة بتتبع مقاطعك الموسيقية لإنشاء سجل الاستماع الخاص بك.'; @override - String get default_plugin => 'الافتراضي'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'تعيين كافتراضي'; diff --git a/lib/l10n/generated/app_localizations_bn.dart b/lib/l10n/generated/app_localizations_bn.dart index 7180a3e5..b11a9f2f 100644 --- a/lib/l10n/generated/app_localizations_bn.dart +++ b/lib/l10n/generated/app_localizations_bn.dart @@ -1443,7 +1443,16 @@ class AppLocalizationsBn extends AppLocalizations { 'এই প্লাগইনটি আপনার সঙ্গীত স্ক্রোব্বল করে আপনার শোনা ইতিহাস তৈরি করে।'; @override - String get default_plugin => 'ডিফল্ট'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'ডিফল্ট হিসাবে নির্ধারণ করুন'; diff --git a/lib/l10n/generated/app_localizations_ca.dart b/lib/l10n/generated/app_localizations_ca.dart index c9534377..a18d8c38 100644 --- a/lib/l10n/generated/app_localizations_ca.dart +++ b/lib/l10n/generated/app_localizations_ca.dart @@ -1450,7 +1450,16 @@ class AppLocalizationsCa extends AppLocalizations { 'Aquest complement fa scrobbling de la teva música per generar l’historial d’escoltes.'; @override - String get default_plugin => 'Predeterminat'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Establir com a predeterminat'; diff --git a/lib/l10n/generated/app_localizations_cs.dart b/lib/l10n/generated/app_localizations_cs.dart index 0a73011c..ce5785d4 100644 --- a/lib/l10n/generated/app_localizations_cs.dart +++ b/lib/l10n/generated/app_localizations_cs.dart @@ -1442,7 +1442,16 @@ class AppLocalizationsCs extends AppLocalizations { 'Tento plugin scrobbles vaši hudbu pro vytvoření historie poslechů.'; @override - String get default_plugin => 'Výchozí'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Nastavit jako výchozí'; diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index de636d0b..81a67861 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1455,7 +1455,16 @@ class AppLocalizationsDe extends AppLocalizations { 'Dieses Plugin scrobbelt Ihre Musik, um Ihre Hörhistorie zu erstellen.'; @override - String get default_plugin => 'Standard'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Als Standard festlegen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index a86718fa..513daa77 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1442,7 +1442,16 @@ class AppLocalizationsEn extends AppLocalizations { 'This plugin scrobbles your music to generate your listening history.'; @override - String get default_plugin => 'Default'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Set default'; diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index c68ad322..08426481 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -1452,7 +1452,16 @@ class AppLocalizationsEs extends AppLocalizations { 'Este complemento scrobblea tu música para generar tu historial de reproducción.'; @override - String get default_plugin => 'Predeterminado'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Establecer como predeterminado'; diff --git a/lib/l10n/generated/app_localizations_eu.dart b/lib/l10n/generated/app_localizations_eu.dart index 677d24c5..14b8e01f 100644 --- a/lib/l10n/generated/app_localizations_eu.dart +++ b/lib/l10n/generated/app_localizations_eu.dart @@ -1451,7 +1451,16 @@ class AppLocalizationsEu extends AppLocalizations { 'Plugin honek zure musika scrobbled egiten du zure entzuteen historia sortzeko.'; @override - String get default_plugin => 'Lehenetsia'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Lehenetsi gisa ezarri'; diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index 8cda0e00..d0f73246 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -1441,7 +1441,16 @@ class AppLocalizationsFa extends AppLocalizations { 'این افزونه موسیقی شما را اسکراب می‌کند تا تاریخچهٔ شنیداری‌تان را تولید کند.'; @override - String get default_plugin => 'پیش‌فرض'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'تنظیم به عنوان پیش‌فرض'; diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index 8342d7ec..751eb0c1 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -1443,7 +1443,16 @@ class AppLocalizationsFi extends AppLocalizations { 'Tämä lisäosa scrobblaa musiikkisi luodakseen kuunteluhistoriasi.'; @override - String get default_plugin => 'Oletus'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Aseta oletukseksi'; diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 07f42798..068701cc 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1457,7 +1457,16 @@ class AppLocalizationsFr extends AppLocalizations { 'Ce plugin scrobble votre musique pour générer votre historique d\'écoute.'; @override - String get default_plugin => 'Par défaut'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Définir par défaut'; diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index 7cd951b4..3c16bdfc 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -1448,7 +1448,16 @@ class AppLocalizationsHi extends AppLocalizations { 'यह प्लगइन आपके सुनने के इतिहास को उत्पन्न करने के लिए आपके संगीत को स्क्रॉबल करता है।'; @override - String get default_plugin => 'डिफ़ॉल्ट'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'डिफ़ॉल्ट सेट करें'; diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index ad2edbfe..f1231523 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -1449,7 +1449,16 @@ class AppLocalizationsId extends AppLocalizations { 'Plugin ini scrobble musik Anda untuk menghasilkan riwayat mendengarkan Anda.'; @override - String get default_plugin => 'Bawaan'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Atur sebagai bawaan'; diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 5e75ac08..c781bd31 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1448,7 +1448,16 @@ class AppLocalizationsIt extends AppLocalizations { 'Questo plugin scrobbla la tua musica per generare la tua cronologia di ascolti.'; @override - String get default_plugin => 'Predefinito'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Imposta come predefinito'; diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index a2500d63..525e6c66 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -1416,7 +1416,16 @@ class AppLocalizationsJa extends AppLocalizations { String get plugin_scrobbling_info => 'このプラグインは、あなたの音楽をscrobbleして視聴履歴を生成します。'; @override - String get default_plugin => 'デフォルト'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'デフォルトに設定'; diff --git a/lib/l10n/generated/app_localizations_ka.dart b/lib/l10n/generated/app_localizations_ka.dart index 312c9ccd..3d960d7e 100644 --- a/lib/l10n/generated/app_localizations_ka.dart +++ b/lib/l10n/generated/app_localizations_ka.dart @@ -1448,7 +1448,16 @@ class AppLocalizationsKa extends AppLocalizations { 'ეს პლაგინი აწარმოებს თქვენი მუსიკის სქრობლინგს, რათა შექმნას თქვენი მოსმენის ისტორია.'; @override - String get default_plugin => 'ნაგულისხმევი'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'ნაგულისხმევად დაყენება'; diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index 47f9ad68..12a3a5e3 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -1421,7 +1421,16 @@ class AppLocalizationsKo extends AppLocalizations { String get plugin_scrobbling_info => '이 플러그인은 음악을 스크로블하여 청취 기록을 생성합니다.'; @override - String get default_plugin => '기본'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => '기본값으로 설정'; diff --git a/lib/l10n/generated/app_localizations_ne.dart b/lib/l10n/generated/app_localizations_ne.dart index fde29abc..64b3311e 100644 --- a/lib/l10n/generated/app_localizations_ne.dart +++ b/lib/l10n/generated/app_localizations_ne.dart @@ -1454,7 +1454,16 @@ class AppLocalizationsNe extends AppLocalizations { 'यो प्लगइनले तपाईंको सुन्ने इतिहास उत्पन्न गर्न तपाईंको संगीतलाई स्क्रब्बल गर्दछ।'; @override - String get default_plugin => 'पूर्वनिर्धारित'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'पूर्वनिर्धारित सेट गर्नुहोस्'; diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index 29cc6c27..78d34a27 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -1446,7 +1446,16 @@ class AppLocalizationsNl extends AppLocalizations { 'Deze plugin scrobblet uw muziek om uw luistergeschiedenis te genereren.'; @override - String get default_plugin => 'Standaard'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Instellen als standaard'; diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index 32ffe065..afe29fca 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -1449,7 +1449,16 @@ class AppLocalizationsPl extends AppLocalizations { 'Ta wtyczka scrobbluje Twoją muzykę, aby wygenerować historię odsłuchań.'; @override - String get default_plugin => 'Domyślna'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Ustaw jako domyślną'; diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 0df60587..74ead955 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -1446,7 +1446,16 @@ class AppLocalizationsPt extends AppLocalizations { 'Este plugin faz o scrobbling de sua música para gerar seu histórico de audição.'; @override - String get default_plugin => 'Padrão'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Definir como padrão'; diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 9399f981..f6ad081e 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -1448,7 +1448,16 @@ class AppLocalizationsRu extends AppLocalizations { 'Этот плагин скробблит вашу музыку для создания вашей истории прослушиваний.'; @override - String get default_plugin => 'По умолчанию'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Установить по умолчанию'; diff --git a/lib/l10n/generated/app_localizations_ta.dart b/lib/l10n/generated/app_localizations_ta.dart index e6c1d29f..94719207 100644 --- a/lib/l10n/generated/app_localizations_ta.dart +++ b/lib/l10n/generated/app_localizations_ta.dart @@ -1455,7 +1455,16 @@ class AppLocalizationsTa extends AppLocalizations { 'இந்த பிளகின் உங்கள் கேட்பதின் வரலாற்றை உருவாக்க உங்கள் இசையை ஸ்க்ரோப்ள் செய்கிறது.'; @override - String get default_plugin => 'இயல்புநிலை'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'இயல்புநிலையாக அமைக்கவும்'; diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index aea6f623..11d82dd6 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -1440,7 +1440,16 @@ class AppLocalizationsTh extends AppLocalizations { 'ปลั๊กอินนี้จะ scrobble เพลงของคุณเพื่อสร้างประวัติการฟังของคุณ'; @override - String get default_plugin => 'ค่าเริ่มต้น'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'ตั้งค่าเริ่มต้น'; diff --git a/lib/l10n/generated/app_localizations_tl.dart b/lib/l10n/generated/app_localizations_tl.dart index ff2ae5da..ac8415f5 100644 --- a/lib/l10n/generated/app_localizations_tl.dart +++ b/lib/l10n/generated/app_localizations_tl.dart @@ -1456,7 +1456,16 @@ class AppLocalizationsTl extends AppLocalizations { 'Sinis-scrobble ng plugin na ito ang iyong musika upang mabuo ang iyong kasaysayan ng pakikinig.'; @override - String get default_plugin => 'Default'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Itakda bilang default'; diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index 64f07b6e..cd4f7122 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -1450,7 +1450,16 @@ class AppLocalizationsTr extends AppLocalizations { 'Bu eklenti, dinleme geçmişinizi oluşturmak için müziğinizi scrobble eder.'; @override - String get default_plugin => 'Varsayılan'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Varsayılan olarak ayarla'; diff --git a/lib/l10n/generated/app_localizations_uk.dart b/lib/l10n/generated/app_localizations_uk.dart index 59414ca7..a6297b1e 100644 --- a/lib/l10n/generated/app_localizations_uk.dart +++ b/lib/l10n/generated/app_localizations_uk.dart @@ -1446,7 +1446,16 @@ class AppLocalizationsUk extends AppLocalizations { 'Цей плагін скроббить вашу музику, щоб створити вашу історію прослуховувань.'; @override - String get default_plugin => 'За замовчуванням'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Встановити за замовчуванням'; diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index 0b980dfd..1421c907 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -1450,7 +1450,16 @@ class AppLocalizationsVi extends AppLocalizations { 'Plugin này scrobble nhạc của bạn để tạo lịch sử nghe của bạn.'; @override - String get default_plugin => 'Mặc định'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Đặt làm mặc định'; diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index e2845fe3..6232965a 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -1412,7 +1412,16 @@ class AppLocalizationsZh extends AppLocalizations { String get plugin_scrobbling_info => '此插件会 scrobble 您的音乐以生成您的收听历史记录。'; @override - String get default_plugin => '默认'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => '设为默认'; @@ -2920,9 +2929,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get plugin_scrobbling_info => '此外掛程式會 Scrobble 您的音樂以產生您的收聽記錄。'; - @override - String get default_plugin => '預設'; - @override String get set_default => '設為預設'; diff --git a/lib/main.dart b/lib/main.dart index b5789d6f..f29933e6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -150,11 +150,13 @@ class Spotube extends HookConsumerWidget { ref.listen(audioPlayerStreamListenersProvider, (_, __) {}); ref.listen(bonsoirProvider, (_, __) {}); ref.listen(connectClientsProvider, (_, __) {}); - ref.listen(metadataPluginsProvider, (_, __) {}); - ref.listen(metadataPluginProvider, (_, __) {}); ref.listen(serverProvider, (_, __) {}); ref.listen(trayManagerProvider, (_, __) {}); + ref.listen(metadataPluginsProvider, (_, __) {}); + ref.listen(metadataPluginProvider, (_, __) {}); + ref.listen(audioSourcePluginProvider, (_, __) {}); ref.listen(metadataPluginUpdateCheckerProvider, (_, __) {}); + ref.listen(audioSourcePluginUpdateCheckerProvider, (_, __) {}); useFixWindowStretching(); useDisableBatteryOptimizations(); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index bc30627d..2df41e9a 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -58,14 +58,14 @@ part 'typeconverters/subtitle.dart'; AudioPlayerStateTable, HistoryTable, LyricsTable, - MetadataPluginsTable, + PluginsTable, ], ) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override - int get schemaVersion => 8; + int get schemaVersion => 9; @override MigrationStrategy get migration { @@ -199,6 +199,18 @@ class AppDatabase extends _$AppDatabase { } }); }, + from8To9: (m, schema) async { + await m.renameTable(schema.pluginsTable, "metadata_plugins_table"); + await m.renameColumn( + schema.pluginsTable, + "selected", + pluginsTable.selectedForMetadata, + ); + await m.addColumn( + schema.pluginsTable, + pluginsTable.selectedForAudioSource, + ); + }, ), ); } diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 8c4def7c..70f6aa26 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -3740,12 +3740,12 @@ class LyricsTableCompanion extends UpdateCompanion { } } -class $MetadataPluginsTableTable extends MetadataPluginsTable - with TableInfo<$MetadataPluginsTableTable, MetadataPluginsTableData> { +class $PluginsTableTable extends PluginsTable + with TableInfo<$PluginsTableTable, PluginsTableData> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $MetadataPluginsTableTable(this.attachedDatabase, [this._alias]); + $PluginsTableTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( @@ -3790,24 +3790,32 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable late final GeneratedColumnWithTypeConverter, String> apis = GeneratedColumn('apis', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>( - $MetadataPluginsTableTable.$converterapis); + .withConverter>($PluginsTableTable.$converterapis); @override late final GeneratedColumnWithTypeConverter, String> abilities = GeneratedColumn('abilities', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>( - $MetadataPluginsTableTable.$converterabilities); - static const VerificationMeta _selectedMeta = - const VerificationMeta('selected'); + .withConverter>($PluginsTableTable.$converterabilities); + static const VerificationMeta _selectedForMetadataMeta = + const VerificationMeta('selectedForMetadata'); @override - late final GeneratedColumn selected = GeneratedColumn( - 'selected', aliasedName, false, + late final GeneratedColumn selectedForMetadata = GeneratedColumn( + 'selected_for_metadata', aliasedName, false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("selected" IN (0, 1))'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_metadata" IN (0, 1))'), defaultValue: const Constant(false)); + static const VerificationMeta _selectedForAudioSourceMeta = + const VerificationMeta('selectedForAudioSource'); + @override + late final GeneratedColumn selectedForAudioSource = + GeneratedColumn('selected_for_audio_source', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_audio_source" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _repositoryMeta = const VerificationMeta('repository'); @override @@ -3821,7 +3829,7 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable 'plugin_api_version', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: false, - defaultValue: const Constant('1.0.0')); + defaultValue: const Constant('2.0.0')); @override List get $columns => [ id, @@ -3832,7 +3840,8 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable entryPoint, apis, abilities, - selected, + selectedForMetadata, + selectedForAudioSource, repository, pluginApiVersion ]; @@ -3840,10 +3849,9 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'metadata_plugins_table'; + static const String $name = 'plugins_table'; @override - VerificationContext validateIntegrity( - Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); @@ -3884,9 +3892,17 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable } else if (isInserting) { context.missing(_entryPointMeta); } - if (data.containsKey('selected')) { - context.handle(_selectedMeta, - selected.isAcceptableOrUnknown(data['selected']!, _selectedMeta)); + if (data.containsKey('selected_for_metadata')) { + context.handle( + _selectedForMetadataMeta, + selectedForMetadata.isAcceptableOrUnknown( + data['selected_for_metadata']!, _selectedForMetadataMeta)); + } + if (data.containsKey('selected_for_audio_source')) { + context.handle( + _selectedForAudioSourceMeta, + selectedForAudioSource.isAcceptableOrUnknown( + data['selected_for_audio_source']!, _selectedForAudioSourceMeta)); } if (data.containsKey('repository')) { context.handle( @@ -3906,10 +3922,9 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable @override Set get $primaryKey => {id}; @override - MetadataPluginsTableData map(Map data, - {String? tablePrefix}) { + PluginsTableData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return MetadataPluginsTableData( + return PluginsTableData( id: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}id'])!, name: attachedDatabase.typeMapping @@ -3922,14 +3937,17 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable .read(DriftSqlType.string, data['${effectivePrefix}author'])!, entryPoint: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}entry_point'])!, - apis: $MetadataPluginsTableTable.$converterapis.fromSql(attachedDatabase + apis: $PluginsTableTable.$converterapis.fromSql(attachedDatabase .typeMapping .read(DriftSqlType.string, data['${effectivePrefix}apis'])!), - abilities: $MetadataPluginsTableTable.$converterabilities.fromSql( - attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}abilities'])!), - selected: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}selected'])!, + abilities: $PluginsTableTable.$converterabilities.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}abilities'])!), + selectedForMetadata: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}selected_for_metadata'])!, + selectedForAudioSource: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}selected_for_audio_source'])!, repository: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}repository']), pluginApiVersion: attachedDatabase.typeMapping.read( @@ -3938,8 +3956,8 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable } @override - $MetadataPluginsTableTable createAlias(String alias) { - return $MetadataPluginsTableTable(attachedDatabase, alias); + $PluginsTableTable createAlias(String alias) { + return $PluginsTableTable(attachedDatabase, alias); } static TypeConverter, String> $converterapis = @@ -3948,8 +3966,8 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable const StringListConverter(); } -class MetadataPluginsTableData extends DataClass - implements Insertable { +class PluginsTableData extends DataClass + implements Insertable { final int id; final String name; final String description; @@ -3958,10 +3976,11 @@ class MetadataPluginsTableData extends DataClass final String entryPoint; final List apis; final List abilities; - final bool selected; + final bool selectedForMetadata; + final bool selectedForAudioSource; final String? repository; final String pluginApiVersion; - const MetadataPluginsTableData( + const PluginsTableData( {required this.id, required this.name, required this.description, @@ -3970,7 +3989,8 @@ class MetadataPluginsTableData extends DataClass required this.entryPoint, required this.apis, required this.abilities, - required this.selected, + required this.selectedForMetadata, + required this.selectedForAudioSource, this.repository, required this.pluginApiVersion}); @override @@ -3983,14 +4003,15 @@ class MetadataPluginsTableData extends DataClass map['author'] = Variable(author); map['entry_point'] = Variable(entryPoint); { - map['apis'] = Variable( - $MetadataPluginsTableTable.$converterapis.toSql(apis)); + map['apis'] = + Variable($PluginsTableTable.$converterapis.toSql(apis)); } { map['abilities'] = Variable( - $MetadataPluginsTableTable.$converterabilities.toSql(abilities)); + $PluginsTableTable.$converterabilities.toSql(abilities)); } - map['selected'] = Variable(selected); + map['selected_for_metadata'] = Variable(selectedForMetadata); + map['selected_for_audio_source'] = Variable(selectedForAudioSource); if (!nullToAbsent || repository != null) { map['repository'] = Variable(repository); } @@ -3998,8 +4019,8 @@ class MetadataPluginsTableData extends DataClass return map; } - MetadataPluginsTableCompanion toCompanion(bool nullToAbsent) { - return MetadataPluginsTableCompanion( + PluginsTableCompanion toCompanion(bool nullToAbsent) { + return PluginsTableCompanion( id: Value(id), name: Value(name), description: Value(description), @@ -4008,7 +4029,8 @@ class MetadataPluginsTableData extends DataClass entryPoint: Value(entryPoint), apis: Value(apis), abilities: Value(abilities), - selected: Value(selected), + selectedForMetadata: Value(selectedForMetadata), + selectedForAudioSource: Value(selectedForAudioSource), repository: repository == null && nullToAbsent ? const Value.absent() : Value(repository), @@ -4016,10 +4038,10 @@ class MetadataPluginsTableData extends DataClass ); } - factory MetadataPluginsTableData.fromJson(Map json, + factory PluginsTableData.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return MetadataPluginsTableData( + return PluginsTableData( id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), description: serializer.fromJson(json['description']), @@ -4028,7 +4050,10 @@ class MetadataPluginsTableData extends DataClass entryPoint: serializer.fromJson(json['entryPoint']), apis: serializer.fromJson>(json['apis']), abilities: serializer.fromJson>(json['abilities']), - selected: serializer.fromJson(json['selected']), + selectedForMetadata: + serializer.fromJson(json['selectedForMetadata']), + selectedForAudioSource: + serializer.fromJson(json['selectedForAudioSource']), repository: serializer.fromJson(json['repository']), pluginApiVersion: serializer.fromJson(json['pluginApiVersion']), ); @@ -4045,13 +4070,14 @@ class MetadataPluginsTableData extends DataClass 'entryPoint': serializer.toJson(entryPoint), 'apis': serializer.toJson>(apis), 'abilities': serializer.toJson>(abilities), - 'selected': serializer.toJson(selected), + 'selectedForMetadata': serializer.toJson(selectedForMetadata), + 'selectedForAudioSource': serializer.toJson(selectedForAudioSource), 'repository': serializer.toJson(repository), 'pluginApiVersion': serializer.toJson(pluginApiVersion), }; } - MetadataPluginsTableData copyWith( + PluginsTableData copyWith( {int? id, String? name, String? description, @@ -4060,10 +4086,11 @@ class MetadataPluginsTableData extends DataClass String? entryPoint, List? apis, List? abilities, - bool? selected, + bool? selectedForMetadata, + bool? selectedForAudioSource, Value repository = const Value.absent(), String? pluginApiVersion}) => - MetadataPluginsTableData( + PluginsTableData( id: id ?? this.id, name: name ?? this.name, description: description ?? this.description, @@ -4072,13 +4099,14 @@ class MetadataPluginsTableData extends DataClass entryPoint: entryPoint ?? this.entryPoint, apis: apis ?? this.apis, abilities: abilities ?? this.abilities, - selected: selected ?? this.selected, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, repository: repository.present ? repository.value : this.repository, pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, ); - MetadataPluginsTableData copyWithCompanion( - MetadataPluginsTableCompanion data) { - return MetadataPluginsTableData( + PluginsTableData copyWithCompanion(PluginsTableCompanion data) { + return PluginsTableData( id: data.id.present ? data.id.value : this.id, name: data.name.present ? data.name.value : this.name, description: @@ -4089,7 +4117,12 @@ class MetadataPluginsTableData extends DataClass data.entryPoint.present ? data.entryPoint.value : this.entryPoint, apis: data.apis.present ? data.apis.value : this.apis, abilities: data.abilities.present ? data.abilities.value : this.abilities, - selected: data.selected.present ? data.selected.value : this.selected, + selectedForMetadata: data.selectedForMetadata.present + ? data.selectedForMetadata.value + : this.selectedForMetadata, + selectedForAudioSource: data.selectedForAudioSource.present + ? data.selectedForAudioSource.value + : this.selectedForAudioSource, repository: data.repository.present ? data.repository.value : this.repository, pluginApiVersion: data.pluginApiVersion.present @@ -4100,7 +4133,7 @@ class MetadataPluginsTableData extends DataClass @override String toString() { - return (StringBuffer('MetadataPluginsTableData(') + return (StringBuffer('PluginsTableData(') ..write('id: $id, ') ..write('name: $name, ') ..write('description: $description, ') @@ -4109,7 +4142,8 @@ class MetadataPluginsTableData extends DataClass ..write('entryPoint: $entryPoint, ') ..write('apis: $apis, ') ..write('abilities: $abilities, ') - ..write('selected: $selected, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') ..write('repository: $repository, ') ..write('pluginApiVersion: $pluginApiVersion') ..write(')')) @@ -4117,12 +4151,23 @@ class MetadataPluginsTableData extends DataClass } @override - int get hashCode => Object.hash(id, name, description, version, author, - entryPoint, apis, abilities, selected, repository, pluginApiVersion); + int get hashCode => Object.hash( + id, + name, + description, + version, + author, + entryPoint, + apis, + abilities, + selectedForMetadata, + selectedForAudioSource, + repository, + pluginApiVersion); @override bool operator ==(Object other) => identical(this, other) || - (other is MetadataPluginsTableData && + (other is PluginsTableData && other.id == this.id && other.name == this.name && other.description == this.description && @@ -4131,13 +4176,13 @@ class MetadataPluginsTableData extends DataClass other.entryPoint == this.entryPoint && other.apis == this.apis && other.abilities == this.abilities && - other.selected == this.selected && + other.selectedForMetadata == this.selectedForMetadata && + other.selectedForAudioSource == this.selectedForAudioSource && other.repository == this.repository && other.pluginApiVersion == this.pluginApiVersion); } -class MetadataPluginsTableCompanion - extends UpdateCompanion { +class PluginsTableCompanion extends UpdateCompanion { final Value id; final Value name; final Value description; @@ -4146,10 +4191,11 @@ class MetadataPluginsTableCompanion final Value entryPoint; final Value> apis; final Value> abilities; - final Value selected; + final Value selectedForMetadata; + final Value selectedForAudioSource; final Value repository; final Value pluginApiVersion; - const MetadataPluginsTableCompanion({ + const PluginsTableCompanion({ this.id = const Value.absent(), this.name = const Value.absent(), this.description = const Value.absent(), @@ -4158,11 +4204,12 @@ class MetadataPluginsTableCompanion this.entryPoint = const Value.absent(), this.apis = const Value.absent(), this.abilities = const Value.absent(), - this.selected = const Value.absent(), + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), this.repository = const Value.absent(), this.pluginApiVersion = const Value.absent(), }); - MetadataPluginsTableCompanion.insert({ + PluginsTableCompanion.insert({ this.id = const Value.absent(), required String name, required String description, @@ -4171,7 +4218,8 @@ class MetadataPluginsTableCompanion required String entryPoint, required List apis, required List abilities, - this.selected = const Value.absent(), + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), this.repository = const Value.absent(), this.pluginApiVersion = const Value.absent(), }) : name = Value(name), @@ -4181,7 +4229,7 @@ class MetadataPluginsTableCompanion entryPoint = Value(entryPoint), apis = Value(apis), abilities = Value(abilities); - static Insertable custom({ + static Insertable custom({ Expression? id, Expression? name, Expression? description, @@ -4190,7 +4238,8 @@ class MetadataPluginsTableCompanion Expression? entryPoint, Expression? apis, Expression? abilities, - Expression? selected, + Expression? selectedForMetadata, + Expression? selectedForAudioSource, Expression? repository, Expression? pluginApiVersion, }) { @@ -4203,13 +4252,16 @@ class MetadataPluginsTableCompanion if (entryPoint != null) 'entry_point': entryPoint, if (apis != null) 'apis': apis, if (abilities != null) 'abilities': abilities, - if (selected != null) 'selected': selected, + if (selectedForMetadata != null) + 'selected_for_metadata': selectedForMetadata, + if (selectedForAudioSource != null) + 'selected_for_audio_source': selectedForAudioSource, if (repository != null) 'repository': repository, if (pluginApiVersion != null) 'plugin_api_version': pluginApiVersion, }); } - MetadataPluginsTableCompanion copyWith( + PluginsTableCompanion copyWith( {Value? id, Value? name, Value? description, @@ -4218,10 +4270,11 @@ class MetadataPluginsTableCompanion Value? entryPoint, Value>? apis, Value>? abilities, - Value? selected, + Value? selectedForMetadata, + Value? selectedForAudioSource, Value? repository, Value? pluginApiVersion}) { - return MetadataPluginsTableCompanion( + return PluginsTableCompanion( id: id ?? this.id, name: name ?? this.name, description: description ?? this.description, @@ -4230,7 +4283,9 @@ class MetadataPluginsTableCompanion entryPoint: entryPoint ?? this.entryPoint, apis: apis ?? this.apis, abilities: abilities ?? this.abilities, - selected: selected ?? this.selected, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, repository: repository ?? this.repository, pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, ); @@ -4258,16 +4313,19 @@ class MetadataPluginsTableCompanion map['entry_point'] = Variable(entryPoint.value); } if (apis.present) { - map['apis'] = Variable( - $MetadataPluginsTableTable.$converterapis.toSql(apis.value)); + map['apis'] = + Variable($PluginsTableTable.$converterapis.toSql(apis.value)); } if (abilities.present) { - map['abilities'] = Variable($MetadataPluginsTableTable - .$converterabilities - .toSql(abilities.value)); + map['abilities'] = Variable( + $PluginsTableTable.$converterabilities.toSql(abilities.value)); } - if (selected.present) { - map['selected'] = Variable(selected.value); + if (selectedForMetadata.present) { + map['selected_for_metadata'] = Variable(selectedForMetadata.value); + } + if (selectedForAudioSource.present) { + map['selected_for_audio_source'] = + Variable(selectedForAudioSource.value); } if (repository.present) { map['repository'] = Variable(repository.value); @@ -4280,7 +4338,7 @@ class MetadataPluginsTableCompanion @override String toString() { - return (StringBuffer('MetadataPluginsTableCompanion(') + return (StringBuffer('PluginsTableCompanion(') ..write('id: $id, ') ..write('name: $name, ') ..write('description: $description, ') @@ -4289,7 +4347,8 @@ class MetadataPluginsTableCompanion ..write('entryPoint: $entryPoint, ') ..write('apis: $apis, ') ..write('abilities: $abilities, ') - ..write('selected: $selected, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') ..write('repository: $repository, ') ..write('pluginApiVersion: $pluginApiVersion') ..write(')')) @@ -4314,8 +4373,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { $AudioPlayerStateTableTable(this); late final $HistoryTableTable historyTable = $HistoryTableTable(this); late final $LyricsTableTable lyricsTable = $LyricsTableTable(this); - late final $MetadataPluginsTableTable metadataPluginsTable = - $MetadataPluginsTableTable(this); + late final $PluginsTableTable pluginsTable = $PluginsTableTable(this); late final Index uniqueBlacklist = Index('unique_blacklist', 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); late final Index uniqTrackMatch = Index('uniq_track_match', @@ -4334,7 +4392,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { audioPlayerStateTable, historyTable, lyricsTable, - metadataPluginsTable, + pluginsTable, uniqueBlacklist, uniqTrackMatch ]; @@ -6280,8 +6338,8 @@ typedef $$LyricsTableTableProcessedTableManager = ProcessedTableManager< ), LyricsTableData, PrefetchHooks Function()>; -typedef $$MetadataPluginsTableTableCreateCompanionBuilder - = MetadataPluginsTableCompanion Function({ +typedef $$PluginsTableTableCreateCompanionBuilder = PluginsTableCompanion + Function({ Value id, required String name, required String description, @@ -6290,12 +6348,13 @@ typedef $$MetadataPluginsTableTableCreateCompanionBuilder required String entryPoint, required List apis, required List abilities, - Value selected, + Value selectedForMetadata, + Value selectedForAudioSource, Value repository, Value pluginApiVersion, }); -typedef $$MetadataPluginsTableTableUpdateCompanionBuilder - = MetadataPluginsTableCompanion Function({ +typedef $$PluginsTableTableUpdateCompanionBuilder = PluginsTableCompanion + Function({ Value id, Value name, Value description, @@ -6304,14 +6363,15 @@ typedef $$MetadataPluginsTableTableUpdateCompanionBuilder Value entryPoint, Value> apis, Value> abilities, - Value selected, + Value selectedForMetadata, + Value selectedForAudioSource, Value repository, Value pluginApiVersion, }); -class $$MetadataPluginsTableTableFilterComposer - extends Composer<_$AppDatabase, $MetadataPluginsTableTable> { - $$MetadataPluginsTableTableFilterComposer({ +class $$PluginsTableTableFilterComposer + extends Composer<_$AppDatabase, $PluginsTableTable> { + $$PluginsTableTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -6346,8 +6406,13 @@ class $$MetadataPluginsTableTableFilterComposer column: $table.abilities, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get selected => $composableBuilder( - column: $table.selected, builder: (column) => ColumnFilters(column)); + ColumnFilters get selectedForMetadata => $composableBuilder( + column: $table.selectedForMetadata, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get selectedForAudioSource => $composableBuilder( + column: $table.selectedForAudioSource, + builder: (column) => ColumnFilters(column)); ColumnFilters get repository => $composableBuilder( column: $table.repository, builder: (column) => ColumnFilters(column)); @@ -6357,9 +6422,9 @@ class $$MetadataPluginsTableTableFilterComposer builder: (column) => ColumnFilters(column)); } -class $$MetadataPluginsTableTableOrderingComposer - extends Composer<_$AppDatabase, $MetadataPluginsTableTable> { - $$MetadataPluginsTableTableOrderingComposer({ +class $$PluginsTableTableOrderingComposer + extends Composer<_$AppDatabase, $PluginsTableTable> { + $$PluginsTableTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -6390,8 +6455,13 @@ class $$MetadataPluginsTableTableOrderingComposer ColumnOrderings get abilities => $composableBuilder( column: $table.abilities, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get selected => $composableBuilder( - column: $table.selected, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get selectedForMetadata => $composableBuilder( + column: $table.selectedForMetadata, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get selectedForAudioSource => $composableBuilder( + column: $table.selectedForAudioSource, + builder: (column) => ColumnOrderings(column)); ColumnOrderings get repository => $composableBuilder( column: $table.repository, builder: (column) => ColumnOrderings(column)); @@ -6401,9 +6471,9 @@ class $$MetadataPluginsTableTableOrderingComposer builder: (column) => ColumnOrderings(column)); } -class $$MetadataPluginsTableTableAnnotationComposer - extends Composer<_$AppDatabase, $MetadataPluginsTableTable> { - $$MetadataPluginsTableTableAnnotationComposer({ +class $$PluginsTableTableAnnotationComposer + extends Composer<_$AppDatabase, $PluginsTableTable> { + $$PluginsTableTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -6434,8 +6504,11 @@ class $$MetadataPluginsTableTableAnnotationComposer GeneratedColumnWithTypeConverter, String> get abilities => $composableBuilder(column: $table.abilities, builder: (column) => column); - GeneratedColumn get selected => - $composableBuilder(column: $table.selected, builder: (column) => column); + GeneratedColumn get selectedForMetadata => $composableBuilder( + column: $table.selectedForMetadata, builder: (column) => column); + + GeneratedColumn get selectedForAudioSource => $composableBuilder( + column: $table.selectedForAudioSource, builder: (column) => column); GeneratedColumn get repository => $composableBuilder( column: $table.repository, builder: (column) => column); @@ -6444,35 +6517,31 @@ class $$MetadataPluginsTableTableAnnotationComposer column: $table.pluginApiVersion, builder: (column) => column); } -class $$MetadataPluginsTableTableTableManager extends RootTableManager< +class $$PluginsTableTableTableManager extends RootTableManager< _$AppDatabase, - $MetadataPluginsTableTable, - MetadataPluginsTableData, - $$MetadataPluginsTableTableFilterComposer, - $$MetadataPluginsTableTableOrderingComposer, - $$MetadataPluginsTableTableAnnotationComposer, - $$MetadataPluginsTableTableCreateCompanionBuilder, - $$MetadataPluginsTableTableUpdateCompanionBuilder, + $PluginsTableTable, + PluginsTableData, + $$PluginsTableTableFilterComposer, + $$PluginsTableTableOrderingComposer, + $$PluginsTableTableAnnotationComposer, + $$PluginsTableTableCreateCompanionBuilder, + $$PluginsTableTableUpdateCompanionBuilder, ( - MetadataPluginsTableData, - BaseReferences<_$AppDatabase, $MetadataPluginsTableTable, - MetadataPluginsTableData> + PluginsTableData, + BaseReferences<_$AppDatabase, $PluginsTableTable, PluginsTableData> ), - MetadataPluginsTableData, + PluginsTableData, PrefetchHooks Function()> { - $$MetadataPluginsTableTableTableManager( - _$AppDatabase db, $MetadataPluginsTableTable table) + $$PluginsTableTableTableManager(_$AppDatabase db, $PluginsTableTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$MetadataPluginsTableTableFilterComposer($db: db, $table: table), + $$PluginsTableTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$MetadataPluginsTableTableOrderingComposer( - $db: db, $table: table), + $$PluginsTableTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$MetadataPluginsTableTableAnnotationComposer( - $db: db, $table: table), + $$PluginsTableTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value name = const Value.absent(), @@ -6482,11 +6551,12 @@ class $$MetadataPluginsTableTableTableManager extends RootTableManager< Value entryPoint = const Value.absent(), Value> apis = const Value.absent(), Value> abilities = const Value.absent(), - Value selected = const Value.absent(), + Value selectedForMetadata = const Value.absent(), + Value selectedForAudioSource = const Value.absent(), Value repository = const Value.absent(), Value pluginApiVersion = const Value.absent(), }) => - MetadataPluginsTableCompanion( + PluginsTableCompanion( id: id, name: name, description: description, @@ -6495,7 +6565,8 @@ class $$MetadataPluginsTableTableTableManager extends RootTableManager< entryPoint: entryPoint, apis: apis, abilities: abilities, - selected: selected, + selectedForMetadata: selectedForMetadata, + selectedForAudioSource: selectedForAudioSource, repository: repository, pluginApiVersion: pluginApiVersion, ), @@ -6508,11 +6579,12 @@ class $$MetadataPluginsTableTableTableManager extends RootTableManager< required String entryPoint, required List apis, required List abilities, - Value selected = const Value.absent(), + Value selectedForMetadata = const Value.absent(), + Value selectedForAudioSource = const Value.absent(), Value repository = const Value.absent(), Value pluginApiVersion = const Value.absent(), }) => - MetadataPluginsTableCompanion.insert( + PluginsTableCompanion.insert( id: id, name: name, description: description, @@ -6521,7 +6593,8 @@ class $$MetadataPluginsTableTableTableManager extends RootTableManager< entryPoint: entryPoint, apis: apis, abilities: abilities, - selected: selected, + selectedForMetadata: selectedForMetadata, + selectedForAudioSource: selectedForAudioSource, repository: repository, pluginApiVersion: pluginApiVersion, ), @@ -6532,23 +6605,21 @@ class $$MetadataPluginsTableTableTableManager extends RootTableManager< )); } -typedef $$MetadataPluginsTableTableProcessedTableManager - = ProcessedTableManager< - _$AppDatabase, - $MetadataPluginsTableTable, - MetadataPluginsTableData, - $$MetadataPluginsTableTableFilterComposer, - $$MetadataPluginsTableTableOrderingComposer, - $$MetadataPluginsTableTableAnnotationComposer, - $$MetadataPluginsTableTableCreateCompanionBuilder, - $$MetadataPluginsTableTableUpdateCompanionBuilder, - ( - MetadataPluginsTableData, - BaseReferences<_$AppDatabase, $MetadataPluginsTableTable, - MetadataPluginsTableData> - ), - MetadataPluginsTableData, - PrefetchHooks Function()>; +typedef $$PluginsTableTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $PluginsTableTable, + PluginsTableData, + $$PluginsTableTableFilterComposer, + $$PluginsTableTableOrderingComposer, + $$PluginsTableTableAnnotationComposer, + $$PluginsTableTableCreateCompanionBuilder, + $$PluginsTableTableUpdateCompanionBuilder, + ( + PluginsTableData, + BaseReferences<_$AppDatabase, $PluginsTableTable, PluginsTableData> + ), + PluginsTableData, + PrefetchHooks Function()>; class $AppDatabaseManager { final _$AppDatabase _db; @@ -6571,6 +6642,6 @@ class $AppDatabaseManager { $$HistoryTableTableTableManager(_db, _db.historyTable); $$LyricsTableTableTableManager get lyricsTable => $$LyricsTableTableTableManager(_db, _db.lyricsTable); - $$MetadataPluginsTableTableTableManager get metadataPluginsTable => - $$MetadataPluginsTableTableTableManager(_db, _db.metadataPluginsTable); + $$PluginsTableTableTableManager get pluginsTable => + $$PluginsTableTableTableManager(_db, _db.pluginsTable); } diff --git a/lib/models/database/database.steps.dart b/lib/models/database/database.steps.dart index a228f5a7..babe71b9 100644 --- a/lib/models/database/database.steps.dart +++ b/lib/models/database/database.steps.dart @@ -1,3 +1,4 @@ +// dart format width=80 import 'package:drift/internal/versioned_schema.dart' as i0; import 'package:drift/drift.dart' as i1; import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import @@ -1407,7 +1408,7 @@ final class Schema5 extends i0.VersionedSchema { i1.GeneratedColumn _column_55(String aliasedName) => i1.GeneratedColumn('accent_color_scheme', aliasedName, false, type: i1.DriftSqlType.string, - defaultValue: const Constant("Slate:0xff64748b")); + defaultValue: const Constant("Orange:0xFFf97315")); final class Schema6 extends i0.VersionedSchema { Schema6({required super.database}) : super(version: 6); @@ -2053,7 +2054,7 @@ final class Schema8 extends i0.VersionedSchema { _column_13, _column_14, _column_15, - _column_55, + _column_69, _column_17, _column_18, _column_19, @@ -2188,7 +2189,7 @@ final class Schema8 extends i0.VersionedSchema { _column_65, _column_66, _column_67, - _column_69, + _column_70, ], attachedDatabase: database, ), @@ -2200,8 +2201,267 @@ final class Schema8 extends i0.VersionedSchema { } i1.GeneratedColumn _column_69(String aliasedName) => + i1.GeneratedColumn('accent_color_scheme', aliasedName, false, + type: i1.DriftSqlType.string, + defaultValue: const Constant("Slate:0xff64748b")); +i1.GeneratedColumn _column_70(String aliasedName) => i1.GeneratedColumn('plugin_api_version', aliasedName, false, type: i1.DriftSqlType.string, defaultValue: const Constant('1.0.0')); + +final class Schema9 extends i0.VersionedSchema { + Schema9({required super.database}) : super(version: 9); + @override + late final List entities = [ + authenticationTable, + blacklistTable, + preferencesTable, + scrobblerTable, + skipSegmentTable, + sourceMatchTable, + audioPlayerStateTable, + historyTable, + lyricsTable, + pluginsTable, + uniqueBlacklist, + uniqTrackMatch, + ]; + late final Shape0 authenticationTable = Shape0( + source: i0.VersionedTable( + entityName: 'authentication_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 blacklistTable = Shape1( + source: i0.VersionedTable( + entityName: 'blacklist_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_4, + _column_5, + _column_6, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape13 preferencesTable = Shape13( + source: i0.VersionedTable( + entityName: 'preferences_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_7, + _column_8, + _column_9, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + _column_15, + _column_69, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_22, + _column_23, + _column_24, + _column_25, + _column_26, + _column_54, + _column_27, + _column_28, + _column_29, + _column_30, + _column_31, + _column_56, + _column_53, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 scrobblerTable = Shape3( + source: i0.VersionedTable( + entityName: 'scrobbler_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_32, + _column_33, + _column_34, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape4 skipSegmentTable = Shape4( + source: i0.VersionedTable( + entityName: 'skip_segment_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_35, + _column_36, + _column_37, + _column_32, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 sourceMatchTable = Shape5( + source: i0.VersionedTable( + entityName: 'source_match_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_37, + _column_38, + _column_39, + _column_32, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape14 audioPlayerStateTable = Shape14( + source: i0.VersionedTable( + entityName: 'audio_player_state_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_40, + _column_41, + _column_42, + _column_43, + _column_57, + _column_58, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 historyTable = Shape9( + source: i0.VersionedTable( + entityName: 'history_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_32, + _column_50, + _column_51, + _column_52, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 lyricsTable = Shape10( + source: i0.VersionedTable( + entityName: 'lyrics_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_37, + _column_52, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape16 pluginsTable = Shape16( + source: i0.VersionedTable( + entityName: 'plugins_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_59, + _column_60, + _column_61, + _column_62, + _column_63, + _column_64, + _column_65, + _column_71, + _column_72, + _column_67, + _column_73, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Index uniqueBlacklist = i1.Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); + final i1.Index uniqTrackMatch = i1.Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); +} + +class Shape16 extends i0.VersionedTable { + Shape16({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get description => + columnsByName['description']! as i1.GeneratedColumn; + i1.GeneratedColumn get version => + columnsByName['version']! as i1.GeneratedColumn; + i1.GeneratedColumn get author => + columnsByName['author']! as i1.GeneratedColumn; + i1.GeneratedColumn get entryPoint => + columnsByName['entry_point']! as i1.GeneratedColumn; + i1.GeneratedColumn get apis => + columnsByName['apis']! as i1.GeneratedColumn; + i1.GeneratedColumn get abilities => + columnsByName['abilities']! as i1.GeneratedColumn; + i1.GeneratedColumn get selectedForMetadata => + columnsByName['selected_for_metadata']! as i1.GeneratedColumn; + i1.GeneratedColumn get selectedForAudioSource => + columnsByName['selected_for_audio_source']! as i1.GeneratedColumn; + i1.GeneratedColumn get repository => + columnsByName['repository']! as i1.GeneratedColumn; + i1.GeneratedColumn get pluginApiVersion => + columnsByName['plugin_api_version']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_71(String aliasedName) => + i1.GeneratedColumn('selected_for_metadata', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_metadata" IN (0, 1))'), + defaultValue: const Constant(false)); +i1.GeneratedColumn _column_72(String aliasedName) => + i1.GeneratedColumn('selected_for_audio_source', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_audio_source" IN (0, 1))'), + defaultValue: const Constant(false)); +i1.GeneratedColumn _column_73(String aliasedName) => + i1.GeneratedColumn('plugin_api_version', aliasedName, false, + type: i1.DriftSqlType.string, defaultValue: const Constant('2.0.0')); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -2210,6 +2470,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema6 schema) from5To6, required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, + required Future Function(i1.Migrator m, Schema9 schema) from8To9, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -2248,6 +2509,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from7To8(migrator, schema); return 8; + case 8: + final schema = Schema9(database: database); + final migrator = i1.Migrator(database, schema); + await from8To9(migrator, schema); + return 9; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -2262,6 +2528,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema6 schema) from5To6, required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, + required Future Function(i1.Migrator m, Schema9 schema) from8To9, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( @@ -2272,4 +2539,5 @@ i1.OnUpgrade stepByStep({ from5To6: from5To6, from6To7: from6To7, from7To8: from7To8, + from8To9: from8To9, )); diff --git a/lib/models/database/tables/metadata_plugins.dart b/lib/models/database/tables/metadata_plugins.dart index 8fa3b064..3447497d 100644 --- a/lib/models/database/tables/metadata_plugins.dart +++ b/lib/models/database/tables/metadata_plugins.dart @@ -1,6 +1,6 @@ part of '../database.dart'; -class MetadataPluginsTable extends Table { +class PluginsTable extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text().withLength(min: 1, max: 50)(); TextColumn get description => text()(); @@ -9,8 +9,11 @@ class MetadataPluginsTable extends Table { TextColumn get entryPoint => text()(); TextColumn get apis => text().map(const StringListConverter())(); TextColumn get abilities => text().map(const StringListConverter())(); - BoolColumn get selected => boolean().withDefault(const Constant(false))(); + BoolColumn get selectedForMetadata => + boolean().withDefault(const Constant(false))(); + BoolColumn get selectedForAudioSource => + boolean().withDefault(const Constant(false))(); TextColumn get repository => text().nullable()(); TextColumn get pluginApiVersion => - text().withDefault(const Constant('1.0.0'))(); + text().withDefault(const Constant('2.0.0'))(); } diff --git a/lib/modules/metadata_plugins/installed_plugin.dart b/lib/modules/metadata_plugins/installed_plugin.dart index 523fb335..34881aaf 100644 --- a/lib/modules/metadata_plugins/installed_plugin.dart +++ b/lib/modules/metadata_plugins/installed_plugin.dart @@ -3,9 +3,9 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/markdown/markdown.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/modules/metadata_plugins/plugin_repository.dart'; import 'package:spotube/modules/metadata_plugins/plugin_update_available_dialog.dart'; import 'package:spotube/provider/metadata_plugin/core/auth.dart'; import 'package:spotube/provider/metadata_plugin/core/support.dart'; @@ -20,27 +20,52 @@ final validAbilities = { class MetadataInstalledPluginItem extends HookConsumerWidget { final PluginConfiguration plugin; - final bool isDefault; + final bool isDefaultMetadata; + final bool isDefaultAudioSource; const MetadataInstalledPluginItem({ super.key, required this.plugin, - required this.isDefault, + required this.isDefaultMetadata, + required this.isDefaultAudioSource, }); @override Widget build(BuildContext context, ref) { + final mediaQuery = MediaQuery.sizeOf(context); + final metadataPlugin = ref.watch(metadataPluginProvider); - final isAuthenticatedSnapshot = - ref.watch(metadataPluginAuthenticatedProvider); + final audioSourcePlugin = ref.watch(audioSourcePluginProvider); + final pluginSnapshot = switch ((isDefaultMetadata, isDefaultAudioSource)) { + (true, _) => metadataPlugin, + (false, true) => audioSourcePlugin, + _ => null, + }; + final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier); - final requiresAuth = - isDefault && plugin.abilities.contains(PluginAbilities.authentication); - final supportsScrobbling = - isDefault && plugin.abilities.contains(PluginAbilities.scrobbling); - final isAuthenticated = isAuthenticatedSnapshot.asData?.value == true; - final updateAvailable = - isDefault ? ref.watch(metadataPluginUpdateCheckerProvider) : null; - final hasUpdate = isDefault && updateAvailable?.asData?.value != null; + + final requiresAuth = (isDefaultMetadata || isDefaultAudioSource) && + plugin.abilities.contains(PluginAbilities.authentication); + final supportsScrobbling = isDefaultMetadata && + plugin.abilities.contains(PluginAbilities.scrobbling); + + final isMetadataAuthenticatedSnapshot = + ref.watch(metadataPluginAuthenticatedProvider); + final isAudioSourceAuthenticatedSnapshot = + ref.watch(audioSourcePluginAuthenticatedProvider); + final isAuthenticated = + isMetadataAuthenticatedSnapshot.asData?.value == true || + isAudioSourceAuthenticatedSnapshot.asData?.value == true; + + final metadataUpdateAvailable = + ref.watch(metadataPluginUpdateCheckerProvider); + final audioSourceUpdateAvailable = + ref.watch(audioSourcePluginUpdateCheckerProvider); + final updateAvailable = switch ((isDefaultMetadata, isDefaultAudioSource)) { + (true, _) => metadataUpdateAvailable, + (false, true) => audioSourceUpdateAvailable, + _ => null, + }; + final hasUpdate = updateAvailable?.asData?.value != null; return Card( child: Column( @@ -218,111 +243,158 @@ class MetadataInstalledPluginItem extends HookConsumerWidget { ], ), ), - Row( + Wrap( spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.spaceBetween, children: [ - Button.secondary( - enabled: !isDefault, - onPressed: () async { - await pluginsNotifier.setDefaultPlugin(plugin); - }, - child: Text( - isDefault - ? context.l10n.default_plugin - : context.l10n.set_default, - ), - ), - if (isDefault) - Consumer(builder: (context, ref, _) { - final supportTextSnapshot = - ref.watch(metadataPluginSupportTextProvider); - - if (supportTextSnapshot.hasValue && - supportTextSnapshot.value == null) { - return const SizedBox.shrink(); - } - - final bgColor = context.theme.brightness == Brightness.dark - ? const Color.fromARGB(255, 255, 145, 175) - : Colors.pink[600]; - final textColor = context.theme.brightness == Brightness.dark - ? Colors.pink[700] - : Colors.pink[50]; - - final mediaQuery = MediaQuery.sizeOf(context); - - return Button( - style: ButtonVariance.secondary.copyWith( - decoration: (context, states, value) { - return value.copyWithIfBoxDecoration( - color: bgColor, - ); - }, - textStyle: (context, states, value) { - return value.copyWith( - color: textColor, - ); - }, - iconTheme: (context, states, value) { - return value.copyWith( - color: textColor, - ); + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + if (plugin.abilities.contains(PluginAbilities.metadata)) + Button.secondary( + enabled: !isDefaultMetadata, + onPressed: () async { + await pluginsNotifier.setDefaultMetadataPlugin(plugin); }, + child: Text( + isDefaultMetadata + ? context.l10n.default_metadata_source + : context.l10n.set_default_metadata_source, + ), ), - leading: const Icon(SpotubeIcons.heartFilled), - child: Text(context.l10n.support), - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: - Text(context.l10n.support_plugin_development), - content: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: mediaQuery.height * 0.8, - maxWidth: 720, - ), - child: SizedBox( - width: double.infinity, - child: SingleChildScrollView( - child: AppMarkdown( - data: supportTextSnapshot.value ?? "", + if (plugin.abilities.contains(PluginAbilities.audioSource)) + Button.secondary( + enabled: !isDefaultAudioSource, + onPressed: () async { + await pluginsNotifier + .setDefaultAudioSourcePlugin(plugin); + }, + child: Text( + isDefaultAudioSource + ? context.l10n.default_audio_source + : context.l10n.set_default_audio_source, + ), + ), + ], + ), + Row( + mainAxisSize: + mediaQuery.smAndUp ? MainAxisSize.min : MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + spacing: 8, + children: [ + if (isDefaultMetadata || isDefaultAudioSource) + Consumer(builder: (context, ref, _) { + final metadataSupportTextSnapshot = + ref.watch(metadataPluginSupportTextProvider); + final audioSourceSupportTextSnapshot = + ref.watch(audioSourcePluginSupportTextProvider); + + final supportTextSnapshot = + switch ((isDefaultMetadata, isDefaultAudioSource)) { + (true, _) => metadataSupportTextSnapshot, + (false, true) => audioSourceSupportTextSnapshot, + _ => null, + }; + + if ((supportTextSnapshot?.hasValue ?? false) && + supportTextSnapshot?.value == null) { + return const SizedBox.shrink(); + } + + final bgColor = + context.theme.brightness == Brightness.dark + ? const Color.fromARGB(255, 255, 145, 175) + : Colors.pink[600]; + final textColor = + context.theme.brightness == Brightness.dark + ? Colors.pink[700] + : Colors.pink[50]; + + final mediaQuery = MediaQuery.sizeOf(context); + + return Button( + style: ButtonVariance.secondary.copyWith( + decoration: (context, states, value) { + return value.copyWithIfBoxDecoration( + color: bgColor, + ); + }, + textStyle: (context, states, value) { + return value.copyWith( + color: textColor, + ); + }, + iconTheme: (context, states, value) { + return value.copyWith( + color: textColor, + ); + }, + ), + leading: const Icon(SpotubeIcons.heartFilled), + child: Text(context.l10n.support), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + context.l10n.support_plugin_development), + content: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: mediaQuery.height * 0.8, + maxWidth: 720, + ), + child: SizedBox( + width: double.infinity, + child: SingleChildScrollView( + child: AppMarkdown( + data: supportTextSnapshot + ?.asData?.value ?? + "", + ), + ), ), ), - ), - ), - actions: [ - Button.secondary( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(context.l10n.close), - ), - ], + actions: [ + Button.secondary( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(context.l10n.close), + ), + ], + ); + }, ); }, ); - }, - ); - }), - const Spacer(), - if (isDefault && requiresAuth && !isAuthenticated) - Button.primary( - onPressed: () async { - await metadataPlugin.asData?.value?.auth.authenticate(); - }, - leading: const Icon(SpotubeIcons.login), - child: Text(context.l10n.login), - ) - else if (isDefault && requiresAuth && isAuthenticated) - Button.destructive( - onPressed: () async { - await metadataPlugin.asData?.value?.auth.logout(); - }, - leading: const Icon(SpotubeIcons.logout), - child: Text(context.l10n.logout), - ) + }), + if ((isDefaultMetadata || isDefaultAudioSource) && + requiresAuth && + !isAuthenticated) + Button.primary( + onPressed: () async { + await pluginSnapshot?.asData?.value?.auth + .authenticate(); + }, + leading: const Icon(SpotubeIcons.login), + child: Text(context.l10n.login), + ) + else if ((isDefaultMetadata || isDefaultAudioSource) && + requiresAuth && + isAuthenticated) + Button.destructive( + onPressed: () async { + await pluginSnapshot?.asData?.value?.auth.logout(); + }, + leading: const Icon(SpotubeIcons.logout), + child: Text(context.l10n.logout), + ), + ], + ) ], ) ], diff --git a/lib/modules/root/use_global_subscriptions.dart b/lib/modules/root/use_global_subscriptions.dart index 68f70b5a..9a492d31 100644 --- a/lib/modules/root/use_global_subscriptions.dart +++ b/lib/modules/root/use_global_subscriptions.dart @@ -31,7 +31,7 @@ void useGlobalSubscriptions(WidgetRef ref) { showDialog( context: context, builder: (context) => MetadataPluginUpdateAvailableDialog( - plugin: pluginConfig.defaultPluginConfig!, + plugin: pluginConfig.defaultMetadataPluginConfig!, update: pluginUpdate, ), ); diff --git a/lib/pages/settings/metadata_plugins.dart b/lib/pages/settings/metadata_plugins.dart index 3601a06d..d4cb1ecf 100644 --- a/lib/pages/settings/metadata_plugins.dart +++ b/lib/pages/settings/metadata_plugins.dart @@ -56,15 +56,15 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { if (tabState.value != 0) { // metadata only plugins - return availablePlugins - .where( - (d) => d.topics.contains( - tabState.value == 1 - ? "spotube-metadata-plugin" - : "spotube-audio-source-plugin", - ), - ) - .toList(); + return availablePlugins.where( + (d) { + return d.topics.contains( + tabState.value == 1 + ? "spotube-metadata-plugin" + : "spotube-audio-source-plugin", + ); + }, + ).toList(); } return availablePlugins; // all plugins @@ -76,6 +76,18 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { ], ); + final installedPlugins = useMemoized?>(() { + if (tabState.value == 0) return plugins.asData?.value.plugins; + + return plugins.asData?.value.plugins.where((d) { + return d.abilities.contains( + tabState.value == 1 + ? PluginAbilities.metadata + : PluginAbilities.audioSource, + ); + }).toList(); + }, [tabState.value, plugins.asData?.value]); + return SafeArea( bottom: false, child: Scaffold( @@ -241,15 +253,20 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { ), const SliverGap(20), SliverList.separated( - itemCount: plugins.asData?.value.plugins.length ?? 0, + itemCount: installedPlugins?.length ?? 0, separatorBuilder: (context, index) => const Gap(12), itemBuilder: (context, index) { - final plugin = plugins.asData!.value.plugins[index]; - final isDefault = - plugins.asData!.value.defaultPlugin == index; + final plugin = installedPlugins![index]; + final isDefaultMetadata = + plugins.asData!.value.defaultMetadataPluginConfig?.slug == + plugin.slug; + final isDefaultAudioSource = plugins + .asData!.value.defaultAudioSourcePluginConfig?.slug == + plugin.slug; return MetadataInstalledPluginItem( plugin: plugin, - isDefault: isDefault, + isDefaultMetadata: isDefaultMetadata, + isDefaultAudioSource: isDefaultAudioSource, ); }, ), diff --git a/lib/provider/metadata_plugin/core/auth.dart b/lib/provider/metadata_plugin/core/auth.dart index 9aa696fc..647b94f9 100644 --- a/lib/provider/metadata_plugin/core/auth.dart +++ b/lib/provider/metadata_plugin/core/auth.dart @@ -8,7 +8,7 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier { @override FutureOr build() async { final defaultPluginConfig = ref.watch(metadataPluginsProvider); - if (defaultPluginConfig.asData?.value.defaultPluginConfig?.abilities + if (defaultPluginConfig.asData?.value.defaultMetadataPluginConfig?.abilities .contains(PluginAbilities.authentication) != true) { return false; @@ -35,3 +35,36 @@ final metadataPluginAuthenticatedProvider = AsyncNotifierProvider( MetadataPluginAuthenticatedNotifier.new, ); + +class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier { + @override + FutureOr build() async { + final defaultPluginConfig = ref.watch(metadataPluginsProvider); + if (defaultPluginConfig + .asData?.value.defaultAudioSourcePluginConfig?.abilities + .contains(PluginAbilities.authentication) != + true) { + return false; + } + + final defaultPlugin = await ref.watch(audioSourcePluginProvider.future); + if (defaultPlugin == null) { + return false; + } + + final sub = defaultPlugin.auth.authStateStream.listen((event) { + state = AsyncData(defaultPlugin.auth.isAuthenticated()); + }); + + ref.onDispose(() { + sub.cancel(); + }); + + return defaultPlugin.auth.isAuthenticated(); + } +} + +final audioSourcePluginAuthenticatedProvider = + AsyncNotifierProvider( + MetadataPluginAuthenticatedNotifier.new, +); diff --git a/lib/provider/metadata_plugin/core/scrobble.dart b/lib/provider/metadata_plugin/core/scrobble.dart index 376572ad..0f8fcc19 100644 --- a/lib/provider/metadata_plugin/core/scrobble.dart +++ b/lib/provider/metadata_plugin/core/scrobble.dart @@ -10,8 +10,10 @@ class MetadataPluginScrobbleNotifier @override build() { final metadataPlugin = ref.watch(metadataPluginProvider); - final pluginConfig = - ref.watch(metadataPluginsProvider).valueOrNull?.defaultPluginConfig; + final pluginConfig = ref + .watch(metadataPluginsProvider) + .valueOrNull + ?.defaultMetadataPluginConfig; if (metadataPlugin.valueOrNull == null || pluginConfig == null || diff --git a/lib/provider/metadata_plugin/core/support.dart b/lib/provider/metadata_plugin/core/support.dart index 88bfbf5c..8864f1b1 100644 --- a/lib/provider/metadata_plugin/core/support.dart +++ b/lib/provider/metadata_plugin/core/support.dart @@ -9,3 +9,13 @@ final metadataPluginSupportTextProvider = FutureProvider((ref) async { } return await metadataPlugin.core.support; }); + +final audioSourcePluginSupportTextProvider = + FutureProvider((ref) async { + final audioSourcePlugin = await ref.watch(audioSourcePluginProvider.future); + + if (audioSourcePlugin == null) { + throw 'No metadata plugin available'; + } + return await audioSourcePlugin.core.support; +}); diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index 815fc826..13d72c93 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -25,18 +25,28 @@ final allowedDomainsRegex = RegExp( class MetadataPluginState { final List plugins; - final int defaultPlugin; + final int defaultMetadataPlugin; + final int defaultAudioSourcePlugin; const MetadataPluginState({ this.plugins = const [], - this.defaultPlugin = -1, + this.defaultMetadataPlugin = -1, + this.defaultAudioSourcePlugin = -1, }); - PluginConfiguration? get defaultPluginConfig { - if (defaultPlugin < 0 || defaultPlugin >= plugins.length) { + PluginConfiguration? get defaultMetadataPluginConfig { + if (defaultMetadataPlugin < 0 || defaultMetadataPlugin >= plugins.length) { return null; } - return plugins[defaultPlugin]; + return plugins[defaultMetadataPlugin]; + } + + PluginConfiguration? get defaultAudioSourcePluginConfig { + if (defaultAudioSourcePlugin < 0 || + defaultAudioSourcePlugin >= plugins.length) { + return null; + } + return plugins[defaultAudioSourcePlugin]; } factory MetadataPluginState.fromJson(Map json) { @@ -44,24 +54,30 @@ class MetadataPluginState { plugins: (json["plugins"] as List) .map((e) => PluginConfiguration.fromJson(e)) .toList(), - defaultPlugin: json["default_plugin"] ?? -1, + defaultMetadataPlugin: json["default_metadata_plugin"] ?? -1, + defaultAudioSourcePlugin: json['default_audio_source_plugin'], ); } Map toJson() { return { "plugins": plugins.map((e) => e.toJson()).toList(), - "default_plugin": defaultPlugin, + "default_metadata_plugin": defaultMetadataPlugin, + "default_audio_source_plugin": defaultAudioSourcePlugin }; } MetadataPluginState copyWith({ List? plugins, - int? defaultPlugin, + int? defaultMetadataPlugin, + int? defaultAudioSourcePlugin, }) { return MetadataPluginState( plugins: plugins ?? this.plugins, - defaultPlugin: defaultPlugin ?? this.defaultPlugin, + defaultMetadataPlugin: + defaultMetadataPlugin ?? this.defaultMetadataPlugin, + defaultAudioSourcePlugin: + defaultAudioSourcePlugin ?? this.defaultAudioSourcePlugin, ); } } @@ -73,7 +89,7 @@ class MetadataPluginNotifier extends AsyncNotifier { build() async { final database = ref.watch(databaseProvider); - final subscription = database.metadataPluginsTable.select().watch().listen( + final subscription = database.pluginsTable.select().watch().listen( (event) async { state = AsyncValue.data(await toStatePlugins(event)); }, @@ -83,15 +99,16 @@ class MetadataPluginNotifier extends AsyncNotifier { subscription.cancel(); }); - final plugins = await database.metadataPluginsTable.select().get(); + final plugins = await database.pluginsTable.select().get(); return await toStatePlugins(plugins); } Future toStatePlugins( - List plugins, + List plugins, ) async { - int defaultPlugin = -1; + int defaultMetadataPlugin = -1; + int defaultAudioSourcePlugin = -1; final pluginConfigs = []; for (int i = 0; i < plugins.length; i++) { @@ -133,20 +150,24 @@ class MetadataPluginNotifier extends AsyncNotifier { !await pluginJsonFile.exists() || !await pluginBinaryFile.exists()) { // Delete the plugin entry from DB if the plugin files are not there. - await database.metadataPluginsTable.deleteOne(plugin); + await database.pluginsTable.deleteOne(plugin); continue; } pluginConfigs.add(pluginConfig); - if (plugin.selected) { - defaultPlugin = pluginConfigs.length - 1; + if (plugin.selectedForMetadata) { + defaultMetadataPlugin = pluginConfigs.length - 1; + } + if (plugin.selectedForAudioSource) { + defaultAudioSourcePlugin = pluginConfigs.length - 1; } } return MetadataPluginState( plugins: pluginConfigs, - defaultPlugin: defaultPlugin, + defaultMetadataPlugin: defaultMetadataPlugin, + defaultAudioSourcePlugin: defaultAudioSourcePlugin, ); } @@ -327,7 +348,7 @@ class MetadataPluginNotifier extends AsyncNotifier { Future addPlugin(PluginConfiguration plugin) async { _assertPluginApiCompatibility(plugin); - final pluginRes = await (database.metadataPluginsTable.select() + final pluginRes = await (database.pluginsTable.select() ..where( (tbl) => tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author), @@ -339,8 +360,8 @@ class MetadataPluginNotifier extends AsyncNotifier { throw MetadataPluginException.duplicatePlugin(); } - await database.metadataPluginsTable.insertOne( - MetadataPluginsTableCompanion.insert( + await database.pluginsTable.insertOne( + PluginsTableCompanion.insert( name: plugin.name, author: plugin.author, description: plugin.description, @@ -351,7 +372,14 @@ class MetadataPluginNotifier extends AsyncNotifier { pluginApiVersion: Value(plugin.pluginApiVersion), repository: Value(plugin.repository), // Setting the very first plugin as the default plugin - selected: Value(state.valueOrNull?.plugins.isEmpty ?? true), + selectedForMetadata: Value( + (state.valueOrNull?.plugins.isEmpty ?? true) && + plugin.abilities.contains(PluginAbilities.metadata), + ), + selectedForAudioSource: Value( + (state.valueOrNull?.plugins.isEmpty ?? true) && + plugin.abilities.contains(PluginAbilities.audioSource), + ), ), ); } @@ -362,17 +390,32 @@ class MetadataPluginNotifier extends AsyncNotifier { if (pluginExtractionDir.existsSync()) { await pluginExtractionDir.delete(recursive: true); } - await database.metadataPluginsTable.deleteWhere((tbl) => + await database.pluginsTable.deleteWhere((tbl) => tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author)); // Same here, if the removed plugin is the default plugin // set the first available plugin as the default plugin // only when there is 1 remaining plugin - if (state.valueOrNull?.defaultPluginConfig == plugin) { - final remainingPlugins = - state.valueOrNull?.plugins.where((p) => p != plugin) ?? []; + if (state.valueOrNull?.defaultMetadataPluginConfig == plugin) { + final remainingPlugins = state.valueOrNull?.plugins.where( + (p) => + p != plugin && p.abilities.contains(PluginAbilities.metadata), + ) ?? + []; if (remainingPlugins.length == 1) { - await setDefaultPlugin(remainingPlugins.first); + await setDefaultMetadataPlugin(remainingPlugins.first); + } + } + + if (state.valueOrNull?.defaultAudioSourcePluginConfig == plugin) { + final remainingPlugins = state.valueOrNull?.plugins.where( + (p) => + p != plugin && + p.abilities.contains(PluginAbilities.audioSource), + ) ?? + []; + if (remainingPlugins.length == 1) { + await setDefaultAudioSourcePlugin(remainingPlugins.first); } } } @@ -381,7 +424,10 @@ class MetadataPluginNotifier extends AsyncNotifier { PluginConfiguration plugin, PluginUpdateAvailable update, ) async { - final isDefault = plugin == state.valueOrNull?.defaultPluginConfig; + final isDefaultMetadata = + plugin == state.valueOrNull?.defaultMetadataPluginConfig; + final isDefaultAudioSource = + plugin == state.valueOrNull?.defaultAudioSourcePluginConfig; final pluginUpdatedConfig = await downloadAndCachePlugin(update.downloadUrl); @@ -394,21 +440,46 @@ class MetadataPluginNotifier extends AsyncNotifier { await removePlugin(plugin); await addPlugin(pluginUpdatedConfig); - if (isDefault) { - await setDefaultPlugin(pluginUpdatedConfig); + if (isDefaultMetadata) { + await setDefaultMetadataPlugin(pluginUpdatedConfig); + } + if (isDefaultAudioSource) { + await setDefaultAudioSourcePlugin(pluginUpdatedConfig); } } - Future setDefaultPlugin(PluginConfiguration plugin) async { - await database.metadataPluginsTable - .update() - .write(const MetadataPluginsTableCompanion(selected: Value(false))); + Future setDefaultMetadataPlugin(PluginConfiguration plugin) async { + assert( + plugin.abilities.contains(PluginAbilities.metadata), + "Must be a metadata plugin", + ); - await (database.metadataPluginsTable.update() + await database.pluginsTable + .update() + .write(const PluginsTableCompanion(selectedForMetadata: Value(false))); + + await (database.pluginsTable.update() ..where((tbl) => tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author))) .write( - const MetadataPluginsTableCompanion(selected: Value(true)), + const PluginsTableCompanion(selectedForMetadata: Value(true)), + ); + } + + Future setDefaultAudioSourcePlugin(PluginConfiguration plugin) async { + assert( + plugin.abilities.contains(PluginAbilities.audioSource), + "Must be an audio-source plugin", + ); + + await database.pluginsTable.update().write( + const PluginsTableCompanion(selectedForAudioSource: Value(false))); + + await (database.pluginsTable.update() + ..where((tbl) => + tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author))) + .write( + const PluginsTableCompanion(selectedForAudioSource: Value(true)), ); } @@ -445,7 +516,32 @@ final metadataPluginsProvider = final metadataPluginProvider = FutureProvider( (ref) async { final defaultPlugin = await ref.watch( - metadataPluginsProvider.selectAsync((data) => data.defaultPluginConfig), + metadataPluginsProvider + .selectAsync((data) => data.defaultMetadataPluginConfig), + ); + final youtubeEngine = ref.read(youtubeEngineProvider); + + if (defaultPlugin == null) { + return null; + } + + final pluginsNotifier = ref.read(metadataPluginsProvider.notifier); + final pluginByteCode = + await pluginsNotifier.getPluginByteCode(defaultPlugin); + + return await MetadataPlugin.create( + youtubeEngine, + defaultPlugin, + pluginByteCode, + ); + }, +); + +final audioSourcePluginProvider = FutureProvider( + (ref) async { + final defaultPlugin = await ref.watch( + metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig), ); final youtubeEngine = ref.read(youtubeEngineProvider); diff --git a/lib/provider/metadata_plugin/updater/update_checker.dart b/lib/provider/metadata_plugin/updater/update_checker.dart index b53ab2b5..6a7dc589 100644 --- a/lib/provider/metadata_plugin/updater/update_checker.dart +++ b/lib/provider/metadata_plugin/updater/update_checker.dart @@ -8,10 +8,25 @@ final metadataPluginUpdateCheckerProvider = final metadataPlugin = await ref.watch(metadataPluginProvider.future); if (metadataPlugin == null || - metadataPluginConfigs.defaultPluginConfig == null) { + metadataPluginConfigs.defaultMetadataPluginConfig == null) { return null; } return metadataPlugin.core - .checkUpdate(metadataPluginConfigs.defaultPluginConfig!); + .checkUpdate(metadataPluginConfigs.defaultMetadataPluginConfig!); +}); + +final audioSourcePluginUpdateCheckerProvider = + FutureProvider((ref) async { + final audioSourcePluginConfigs = + await ref.watch(metadataPluginsProvider.future); + final audioSourcePlugin = await ref.watch(audioSourcePluginProvider.future); + + if (audioSourcePlugin == null || + audioSourcePluginConfigs.defaultAudioSourcePluginConfig == null) { + return null; + } + + return audioSourcePlugin.core + .checkUpdate(audioSourcePluginConfigs.defaultAudioSourcePluginConfig!); }); diff --git a/test/drift/app_db/generated/schema.dart b/test/drift/app_db/generated/schema.dart index dfd3edf3..413b4408 100644 --- a/test/drift/app_db/generated/schema.dart +++ b/test/drift/app_db/generated/schema.dart @@ -1,41 +1,44 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; import 'package:drift/internal/migrations.dart'; -import 'schema_v5.dart' as v5; -import 'schema_v4.dart' as v4; -import 'schema_v8.dart' as v8; -import 'schema_v3.dart' as v3; -import 'schema_v2.dart' as v2; import 'schema_v1.dart' as v1; -import 'schema_v7.dart' as v7; +import 'schema_v2.dart' as v2; +import 'schema_v3.dart' as v3; +import 'schema_v4.dart' as v4; +import 'schema_v5.dart' as v5; import 'schema_v6.dart' as v6; +import 'schema_v7.dart' as v7; +import 'schema_v8.dart' as v8; +import 'schema_v9.dart' as v9; class GeneratedHelper implements SchemaInstantiationHelper { @override GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { switch (version) { - case 5: - return v5.DatabaseAtV5(db); - case 4: - return v4.DatabaseAtV4(db); - case 8: - return v8.DatabaseAtV8(db); - case 3: - return v3.DatabaseAtV3(db); - case 2: - return v2.DatabaseAtV2(db); case 1: return v1.DatabaseAtV1(db); - case 7: - return v7.DatabaseAtV7(db); + case 2: + return v2.DatabaseAtV2(db); + case 3: + return v3.DatabaseAtV3(db); + case 4: + return v4.DatabaseAtV4(db); + case 5: + return v5.DatabaseAtV5(db); case 6: return v6.DatabaseAtV6(db); + case 7: + return v7.DatabaseAtV7(db); + case 8: + return v8.DatabaseAtV8(db); + case 9: + return v9.DatabaseAtV9(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5, 6, 7, 8]; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9]; } diff --git a/test/drift/app_db/generated/schema_v1.dart b/test/drift/app_db/generated/schema_v1.dart index 7a849d18..ca848561 100644 --- a/test/drift/app_db/generated/schema_v1.dart +++ b/test/drift/app_db/generated/schema_v1.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v2.dart b/test/drift/app_db/generated/schema_v2.dart index 4b28750d..c9642f86 100644 --- a/test/drift/app_db/generated/schema_v2.dart +++ b/test/drift/app_db/generated/schema_v2.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v3.dart b/test/drift/app_db/generated/schema_v3.dart index 7ddf4d2b..f6416823 100644 --- a/test/drift/app_db/generated/schema_v3.dart +++ b/test/drift/app_db/generated/schema_v3.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v4.dart b/test/drift/app_db/generated/schema_v4.dart index c8f07c6e..4206abdb 100644 --- a/test/drift/app_db/generated/schema_v4.dart +++ b/test/drift/app_db/generated/schema_v4.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v5.dart b/test/drift/app_db/generated/schema_v5.dart index 72c48612..4283aa98 100644 --- a/test/drift/app_db/generated/schema_v5.dart +++ b/test/drift/app_db/generated/schema_v5.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v6.dart b/test/drift/app_db/generated/schema_v6.dart index 9e556976..c0ef0442 100644 --- a/test/drift/app_db/generated/schema_v6.dart +++ b/test/drift/app_db/generated/schema_v6.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v7.dart b/test/drift/app_db/generated/schema_v7.dart index b28397ab..b476efbd 100644 --- a/test/drift/app_db/generated/schema_v7.dart +++ b/test/drift/app_db/generated/schema_v7.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v8.dart b/test/drift/app_db/generated/schema_v8.dart index 33fb4dad..7008eaff 100644 --- a/test/drift/app_db/generated/schema_v8.dart +++ b/test/drift/app_db/generated/schema_v8.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v9.dart b/test/drift/app_db/generated/schema_v9.dart new file mode 100644 index 00000000..cde63c2f --- /dev/null +++ b/test/drift/app_db/generated/schema_v9.dart @@ -0,0 +1,3568 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class AuthenticationTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthenticationTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn cookie = GeneratedColumn( + 'cookie', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn accessToken = GeneratedColumn( + 'access_token', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn expiration = GeneratedColumn( + 'expiration', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + @override + List get $columns => [id, cookie, accessToken, expiration]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'authentication_table'; + @override + Set get $primaryKey => {id}; + @override + AuthenticationTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthenticationTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + cookie: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}cookie'])!, + accessToken: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}access_token'])!, + expiration: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}expiration'])!, + ); + } + + @override + AuthenticationTable createAlias(String alias) { + return AuthenticationTable(attachedDatabase, alias); + } +} + +class AuthenticationTableData extends DataClass + implements Insertable { + final int id; + final String cookie; + final String accessToken; + final DateTime expiration; + const AuthenticationTableData( + {required this.id, + required this.cookie, + required this.accessToken, + required this.expiration}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['cookie'] = Variable(cookie); + map['access_token'] = Variable(accessToken); + map['expiration'] = Variable(expiration); + return map; + } + + AuthenticationTableCompanion toCompanion(bool nullToAbsent) { + return AuthenticationTableCompanion( + id: Value(id), + cookie: Value(cookie), + accessToken: Value(accessToken), + expiration: Value(expiration), + ); + } + + factory AuthenticationTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthenticationTableData( + id: serializer.fromJson(json['id']), + cookie: serializer.fromJson(json['cookie']), + accessToken: serializer.fromJson(json['accessToken']), + expiration: serializer.fromJson(json['expiration']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'cookie': serializer.toJson(cookie), + 'accessToken': serializer.toJson(accessToken), + 'expiration': serializer.toJson(expiration), + }; + } + + AuthenticationTableData copyWith( + {int? id, + String? cookie, + String? accessToken, + DateTime? expiration}) => + AuthenticationTableData( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + AuthenticationTableData copyWithCompanion(AuthenticationTableCompanion data) { + return AuthenticationTableData( + id: data.id.present ? data.id.value : this.id, + cookie: data.cookie.present ? data.cookie.value : this.cookie, + accessToken: + data.accessToken.present ? data.accessToken.value : this.accessToken, + expiration: + data.expiration.present ? data.expiration.value : this.expiration, + ); + } + + @override + String toString() { + return (StringBuffer('AuthenticationTableData(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, cookie, accessToken, expiration); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthenticationTableData && + other.id == this.id && + other.cookie == this.cookie && + other.accessToken == this.accessToken && + other.expiration == this.expiration); +} + +class AuthenticationTableCompanion + extends UpdateCompanion { + final Value id; + final Value cookie; + final Value accessToken; + final Value expiration; + const AuthenticationTableCompanion({ + this.id = const Value.absent(), + this.cookie = const Value.absent(), + this.accessToken = const Value.absent(), + this.expiration = const Value.absent(), + }); + AuthenticationTableCompanion.insert({ + this.id = const Value.absent(), + required String cookie, + required String accessToken, + required DateTime expiration, + }) : cookie = Value(cookie), + accessToken = Value(accessToken), + expiration = Value(expiration); + static Insertable custom({ + Expression? id, + Expression? cookie, + Expression? accessToken, + Expression? expiration, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (cookie != null) 'cookie': cookie, + if (accessToken != null) 'access_token': accessToken, + if (expiration != null) 'expiration': expiration, + }); + } + + AuthenticationTableCompanion copyWith( + {Value? id, + Value? cookie, + Value? accessToken, + Value? expiration}) { + return AuthenticationTableCompanion( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (cookie.present) { + map['cookie'] = Variable(cookie.value); + } + if (accessToken.present) { + map['access_token'] = Variable(accessToken.value); + } + if (expiration.present) { + map['expiration'] = Variable(expiration.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthenticationTableCompanion(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } +} + +class BlacklistTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + BlacklistTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn elementType = GeneratedColumn( + 'element_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn elementId = GeneratedColumn( + 'element_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, name, elementType, elementId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'blacklist_table'; + @override + Set get $primaryKey => {id}; + @override + BlacklistTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BlacklistTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + elementType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_type'])!, + elementId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_id'])!, + ); + } + + @override + BlacklistTable createAlias(String alias) { + return BlacklistTable(attachedDatabase, alias); + } +} + +class BlacklistTableData extends DataClass + implements Insertable { + final int id; + final String name; + final String elementType; + final String elementId; + const BlacklistTableData( + {required this.id, + required this.name, + required this.elementType, + required this.elementId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['element_type'] = Variable(elementType); + map['element_id'] = Variable(elementId); + return map; + } + + BlacklistTableCompanion toCompanion(bool nullToAbsent) { + return BlacklistTableCompanion( + id: Value(id), + name: Value(name), + elementType: Value(elementType), + elementId: Value(elementId), + ); + } + + factory BlacklistTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BlacklistTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + elementType: serializer.fromJson(json['elementType']), + elementId: serializer.fromJson(json['elementId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'elementType': serializer.toJson(elementType), + 'elementId': serializer.toJson(elementId), + }; + } + + BlacklistTableData copyWith( + {int? id, String? name, String? elementType, String? elementId}) => + BlacklistTableData( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + BlacklistTableData copyWithCompanion(BlacklistTableCompanion data) { + return BlacklistTableData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + elementType: + data.elementType.present ? data.elementType.value : this.elementType, + elementId: data.elementId.present ? data.elementId.value : this.elementId, + ); + } + + @override + String toString() { + return (StringBuffer('BlacklistTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, elementType, elementId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BlacklistTableData && + other.id == this.id && + other.name == this.name && + other.elementType == this.elementType && + other.elementId == this.elementId); +} + +class BlacklistTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value elementType; + final Value elementId; + const BlacklistTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.elementType = const Value.absent(), + this.elementId = const Value.absent(), + }); + BlacklistTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required String elementType, + required String elementId, + }) : name = Value(name), + elementType = Value(elementType), + elementId = Value(elementId); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? elementType, + Expression? elementId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (elementType != null) 'element_type': elementType, + if (elementId != null) 'element_id': elementId, + }); + } + + BlacklistTableCompanion copyWith( + {Value? id, + Value? name, + Value? elementType, + Value? elementId}) { + return BlacklistTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (elementType.present) { + map['element_type'] = Variable(elementType.value); + } + if (elementId.present) { + map['element_id'] = Variable(elementId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BlacklistTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } +} + +class PreferencesTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PreferencesTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn audioQuality = GeneratedColumn( + 'audio_quality', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceQualities.high.name)); + late final GeneratedColumn albumColorSync = GeneratedColumn( + 'album_color_sync', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("album_color_sync" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn amoledDarkTheme = GeneratedColumn( + 'amoled_dark_theme', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("amoled_dark_theme" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn checkUpdate = GeneratedColumn( + 'check_update', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("check_update" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn normalizeAudio = GeneratedColumn( + 'normalize_audio', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("normalize_audio" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn showSystemTrayIcon = GeneratedColumn( + 'show_system_tray_icon', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("show_system_tray_icon" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn systemTitleBar = GeneratedColumn( + 'system_title_bar', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("system_title_bar" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn skipNonMusic = GeneratedColumn( + 'skip_non_music', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("skip_non_music" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn closeBehavior = GeneratedColumn( + 'close_behavior', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(CloseBehavior.close.name)); + late final GeneratedColumn accentColorScheme = + GeneratedColumn('accent_color_scheme', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("Slate:0xff64748b")); + late final GeneratedColumn layoutMode = GeneratedColumn( + 'layout_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(LayoutMode.adaptive.name)); + late final GeneratedColumn locale = GeneratedColumn( + 'locale', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: + const Constant('{"languageCode":"system","countryCode":"system"}')); + late final GeneratedColumn market = GeneratedColumn( + 'market', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(Market.US.name)); + late final GeneratedColumn searchMode = GeneratedColumn( + 'search_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SearchMode.youtube.name)); + late final GeneratedColumn downloadLocation = GeneratedColumn( + 'download_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")); + late final GeneratedColumn localLibraryLocation = + GeneratedColumn('local_library_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")); + late final GeneratedColumn pipedInstance = GeneratedColumn( + 'piped_instance', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("https://pipedapi.kavin.rocks")); + late final GeneratedColumn invidiousInstance = + GeneratedColumn('invidious_instance', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("https://inv.nadeko.net")); + late final GeneratedColumn themeMode = GeneratedColumn( + 'theme_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(ThemeMode.system.name)); + late final GeneratedColumn audioSource = GeneratedColumn( + 'audio_source', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(AudioSource.youtube.name)); + late final GeneratedColumn youtubeClientEngine = + GeneratedColumn('youtube_client_engine', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name)); + late final GeneratedColumn streamMusicCodec = GeneratedColumn( + 'stream_music_codec', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceCodecs.weba.name)); + late final GeneratedColumn downloadMusicCodec = + GeneratedColumn('download_music_codec', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceCodecs.m4a.name)); + late final GeneratedColumn discordPresence = GeneratedColumn( + 'discord_presence', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("discord_presence" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn endlessPlayback = GeneratedColumn( + 'endless_playback', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("endless_playback" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn enableConnect = GeneratedColumn( + 'enable_connect', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("enable_connect" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn connectPort = GeneratedColumn( + 'connect_port', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(-1)); + late final GeneratedColumn cacheMusic = GeneratedColumn( + 'cache_music', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("cache_music" IN (0, 1))'), + defaultValue: const Constant(true)); + @override + List get $columns => [ + id, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + invidiousInstance, + themeMode, + audioSource, + youtubeClientEngine, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect, + connectPort, + cacheMusic + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'preferences_table'; + @override + Set get $primaryKey => {id}; + @override + PreferencesTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PreferencesTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + audioQuality: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}audio_quality'])!, + albumColorSync: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}album_color_sync'])!, + amoledDarkTheme: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}amoled_dark_theme'])!, + checkUpdate: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}check_update'])!, + normalizeAudio: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}normalize_audio'])!, + showSystemTrayIcon: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}show_system_tray_icon'])!, + systemTitleBar: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}system_title_bar'])!, + skipNonMusic: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}skip_non_music'])!, + closeBehavior: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}close_behavior'])!, + accentColorScheme: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}accent_color_scheme'])!, + layoutMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}layout_mode'])!, + locale: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}locale'])!, + market: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}market'])!, + searchMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}search_mode'])!, + downloadLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}download_location'])!, + localLibraryLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_library_location'])!, + pipedInstance: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, + invidiousInstance: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}invidious_instance'])!, + themeMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}theme_mode'])!, + audioSource: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}audio_source'])!, + youtubeClientEngine: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}youtube_client_engine'])!, + streamMusicCodec: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}stream_music_codec'])!, + downloadMusicCodec: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}download_music_codec'])!, + discordPresence: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}discord_presence'])!, + endlessPlayback: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}endless_playback'])!, + enableConnect: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}enable_connect'])!, + connectPort: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}connect_port'])!, + cacheMusic: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}cache_music'])!, + ); + } + + @override + PreferencesTable createAlias(String alias) { + return PreferencesTable(attachedDatabase, alias); + } +} + +class PreferencesTableData extends DataClass + implements Insertable { + final int id; + final String audioQuality; + final bool albumColorSync; + final bool amoledDarkTheme; + final bool checkUpdate; + final bool normalizeAudio; + final bool showSystemTrayIcon; + final bool systemTitleBar; + final bool skipNonMusic; + final String closeBehavior; + final String accentColorScheme; + final String layoutMode; + final String locale; + final String market; + final String searchMode; + final String downloadLocation; + final String localLibraryLocation; + final String pipedInstance; + final String invidiousInstance; + final String themeMode; + final String audioSource; + final String youtubeClientEngine; + final String streamMusicCodec; + final String downloadMusicCodec; + final bool discordPresence; + final bool endlessPlayback; + final bool enableConnect; + final int connectPort; + final bool cacheMusic; + const PreferencesTableData( + {required this.id, + required this.audioQuality, + required this.albumColorSync, + required this.amoledDarkTheme, + required this.checkUpdate, + required this.normalizeAudio, + required this.showSystemTrayIcon, + required this.systemTitleBar, + required this.skipNonMusic, + required this.closeBehavior, + required this.accentColorScheme, + required this.layoutMode, + required this.locale, + required this.market, + required this.searchMode, + required this.downloadLocation, + required this.localLibraryLocation, + required this.pipedInstance, + required this.invidiousInstance, + required this.themeMode, + required this.audioSource, + required this.youtubeClientEngine, + required this.streamMusicCodec, + required this.downloadMusicCodec, + required this.discordPresence, + required this.endlessPlayback, + required this.enableConnect, + required this.connectPort, + required this.cacheMusic}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['audio_quality'] = Variable(audioQuality); + map['album_color_sync'] = Variable(albumColorSync); + map['amoled_dark_theme'] = Variable(amoledDarkTheme); + map['check_update'] = Variable(checkUpdate); + map['normalize_audio'] = Variable(normalizeAudio); + map['show_system_tray_icon'] = Variable(showSystemTrayIcon); + map['system_title_bar'] = Variable(systemTitleBar); + map['skip_non_music'] = Variable(skipNonMusic); + map['close_behavior'] = Variable(closeBehavior); + map['accent_color_scheme'] = Variable(accentColorScheme); + map['layout_mode'] = Variable(layoutMode); + map['locale'] = Variable(locale); + map['market'] = Variable(market); + map['search_mode'] = Variable(searchMode); + map['download_location'] = Variable(downloadLocation); + map['local_library_location'] = Variable(localLibraryLocation); + map['piped_instance'] = Variable(pipedInstance); + map['invidious_instance'] = Variable(invidiousInstance); + map['theme_mode'] = Variable(themeMode); + map['audio_source'] = Variable(audioSource); + map['youtube_client_engine'] = Variable(youtubeClientEngine); + map['stream_music_codec'] = Variable(streamMusicCodec); + map['download_music_codec'] = Variable(downloadMusicCodec); + map['discord_presence'] = Variable(discordPresence); + map['endless_playback'] = Variable(endlessPlayback); + map['enable_connect'] = Variable(enableConnect); + map['connect_port'] = Variable(connectPort); + map['cache_music'] = Variable(cacheMusic); + return map; + } + + PreferencesTableCompanion toCompanion(bool nullToAbsent) { + return PreferencesTableCompanion( + id: Value(id), + audioQuality: Value(audioQuality), + albumColorSync: Value(albumColorSync), + amoledDarkTheme: Value(amoledDarkTheme), + checkUpdate: Value(checkUpdate), + normalizeAudio: Value(normalizeAudio), + showSystemTrayIcon: Value(showSystemTrayIcon), + systemTitleBar: Value(systemTitleBar), + skipNonMusic: Value(skipNonMusic), + closeBehavior: Value(closeBehavior), + accentColorScheme: Value(accentColorScheme), + layoutMode: Value(layoutMode), + locale: Value(locale), + market: Value(market), + searchMode: Value(searchMode), + downloadLocation: Value(downloadLocation), + localLibraryLocation: Value(localLibraryLocation), + pipedInstance: Value(pipedInstance), + invidiousInstance: Value(invidiousInstance), + themeMode: Value(themeMode), + audioSource: Value(audioSource), + youtubeClientEngine: Value(youtubeClientEngine), + streamMusicCodec: Value(streamMusicCodec), + downloadMusicCodec: Value(downloadMusicCodec), + discordPresence: Value(discordPresence), + endlessPlayback: Value(endlessPlayback), + enableConnect: Value(enableConnect), + connectPort: Value(connectPort), + cacheMusic: Value(cacheMusic), + ); + } + + factory PreferencesTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PreferencesTableData( + id: serializer.fromJson(json['id']), + audioQuality: serializer.fromJson(json['audioQuality']), + albumColorSync: serializer.fromJson(json['albumColorSync']), + amoledDarkTheme: serializer.fromJson(json['amoledDarkTheme']), + checkUpdate: serializer.fromJson(json['checkUpdate']), + normalizeAudio: serializer.fromJson(json['normalizeAudio']), + showSystemTrayIcon: serializer.fromJson(json['showSystemTrayIcon']), + systemTitleBar: serializer.fromJson(json['systemTitleBar']), + skipNonMusic: serializer.fromJson(json['skipNonMusic']), + closeBehavior: serializer.fromJson(json['closeBehavior']), + accentColorScheme: serializer.fromJson(json['accentColorScheme']), + layoutMode: serializer.fromJson(json['layoutMode']), + locale: serializer.fromJson(json['locale']), + market: serializer.fromJson(json['market']), + searchMode: serializer.fromJson(json['searchMode']), + downloadLocation: serializer.fromJson(json['downloadLocation']), + localLibraryLocation: + serializer.fromJson(json['localLibraryLocation']), + pipedInstance: serializer.fromJson(json['pipedInstance']), + invidiousInstance: serializer.fromJson(json['invidiousInstance']), + themeMode: serializer.fromJson(json['themeMode']), + audioSource: serializer.fromJson(json['audioSource']), + youtubeClientEngine: + serializer.fromJson(json['youtubeClientEngine']), + streamMusicCodec: serializer.fromJson(json['streamMusicCodec']), + downloadMusicCodec: + serializer.fromJson(json['downloadMusicCodec']), + discordPresence: serializer.fromJson(json['discordPresence']), + endlessPlayback: serializer.fromJson(json['endlessPlayback']), + enableConnect: serializer.fromJson(json['enableConnect']), + connectPort: serializer.fromJson(json['connectPort']), + cacheMusic: serializer.fromJson(json['cacheMusic']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'audioQuality': serializer.toJson(audioQuality), + 'albumColorSync': serializer.toJson(albumColorSync), + 'amoledDarkTheme': serializer.toJson(amoledDarkTheme), + 'checkUpdate': serializer.toJson(checkUpdate), + 'normalizeAudio': serializer.toJson(normalizeAudio), + 'showSystemTrayIcon': serializer.toJson(showSystemTrayIcon), + 'systemTitleBar': serializer.toJson(systemTitleBar), + 'skipNonMusic': serializer.toJson(skipNonMusic), + 'closeBehavior': serializer.toJson(closeBehavior), + 'accentColorScheme': serializer.toJson(accentColorScheme), + 'layoutMode': serializer.toJson(layoutMode), + 'locale': serializer.toJson(locale), + 'market': serializer.toJson(market), + 'searchMode': serializer.toJson(searchMode), + 'downloadLocation': serializer.toJson(downloadLocation), + 'localLibraryLocation': serializer.toJson(localLibraryLocation), + 'pipedInstance': serializer.toJson(pipedInstance), + 'invidiousInstance': serializer.toJson(invidiousInstance), + 'themeMode': serializer.toJson(themeMode), + 'audioSource': serializer.toJson(audioSource), + 'youtubeClientEngine': serializer.toJson(youtubeClientEngine), + 'streamMusicCodec': serializer.toJson(streamMusicCodec), + 'downloadMusicCodec': serializer.toJson(downloadMusicCodec), + 'discordPresence': serializer.toJson(discordPresence), + 'endlessPlayback': serializer.toJson(endlessPlayback), + 'enableConnect': serializer.toJson(enableConnect), + 'connectPort': serializer.toJson(connectPort), + 'cacheMusic': serializer.toJson(cacheMusic), + }; + } + + PreferencesTableData copyWith( + {int? id, + String? audioQuality, + bool? albumColorSync, + bool? amoledDarkTheme, + bool? checkUpdate, + bool? normalizeAudio, + bool? showSystemTrayIcon, + bool? systemTitleBar, + bool? skipNonMusic, + String? closeBehavior, + String? accentColorScheme, + String? layoutMode, + String? locale, + String? market, + String? searchMode, + String? downloadLocation, + String? localLibraryLocation, + String? pipedInstance, + String? invidiousInstance, + String? themeMode, + String? audioSource, + String? youtubeClientEngine, + String? streamMusicCodec, + String? downloadMusicCodec, + bool? discordPresence, + bool? endlessPlayback, + bool? enableConnect, + int? connectPort, + bool? cacheMusic}) => + PreferencesTableData( + id: id ?? this.id, + audioQuality: audioQuality ?? this.audioQuality, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + invidiousInstance: invidiousInstance ?? this.invidiousInstance, + themeMode: themeMode ?? this.themeMode, + audioSource: audioSource ?? this.audioSource, + youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, + streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, + downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + connectPort: connectPort ?? this.connectPort, + cacheMusic: cacheMusic ?? this.cacheMusic, + ); + PreferencesTableData copyWithCompanion(PreferencesTableCompanion data) { + return PreferencesTableData( + id: data.id.present ? data.id.value : this.id, + audioQuality: data.audioQuality.present + ? data.audioQuality.value + : this.audioQuality, + albumColorSync: data.albumColorSync.present + ? data.albumColorSync.value + : this.albumColorSync, + amoledDarkTheme: data.amoledDarkTheme.present + ? data.amoledDarkTheme.value + : this.amoledDarkTheme, + checkUpdate: + data.checkUpdate.present ? data.checkUpdate.value : this.checkUpdate, + normalizeAudio: data.normalizeAudio.present + ? data.normalizeAudio.value + : this.normalizeAudio, + showSystemTrayIcon: data.showSystemTrayIcon.present + ? data.showSystemTrayIcon.value + : this.showSystemTrayIcon, + systemTitleBar: data.systemTitleBar.present + ? data.systemTitleBar.value + : this.systemTitleBar, + skipNonMusic: data.skipNonMusic.present + ? data.skipNonMusic.value + : this.skipNonMusic, + closeBehavior: data.closeBehavior.present + ? data.closeBehavior.value + : this.closeBehavior, + accentColorScheme: data.accentColorScheme.present + ? data.accentColorScheme.value + : this.accentColorScheme, + layoutMode: + data.layoutMode.present ? data.layoutMode.value : this.layoutMode, + locale: data.locale.present ? data.locale.value : this.locale, + market: data.market.present ? data.market.value : this.market, + searchMode: + data.searchMode.present ? data.searchMode.value : this.searchMode, + downloadLocation: data.downloadLocation.present + ? data.downloadLocation.value + : this.downloadLocation, + localLibraryLocation: data.localLibraryLocation.present + ? data.localLibraryLocation.value + : this.localLibraryLocation, + pipedInstance: data.pipedInstance.present + ? data.pipedInstance.value + : this.pipedInstance, + invidiousInstance: data.invidiousInstance.present + ? data.invidiousInstance.value + : this.invidiousInstance, + themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, + audioSource: + data.audioSource.present ? data.audioSource.value : this.audioSource, + youtubeClientEngine: data.youtubeClientEngine.present + ? data.youtubeClientEngine.value + : this.youtubeClientEngine, + streamMusicCodec: data.streamMusicCodec.present + ? data.streamMusicCodec.value + : this.streamMusicCodec, + downloadMusicCodec: data.downloadMusicCodec.present + ? data.downloadMusicCodec.value + : this.downloadMusicCodec, + discordPresence: data.discordPresence.present + ? data.discordPresence.value + : this.discordPresence, + endlessPlayback: data.endlessPlayback.present + ? data.endlessPlayback.value + : this.endlessPlayback, + enableConnect: data.enableConnect.present + ? data.enableConnect.value + : this.enableConnect, + connectPort: + data.connectPort.present ? data.connectPort.value : this.connectPort, + cacheMusic: + data.cacheMusic.present ? data.cacheMusic.value : this.cacheMusic, + ); + } + + @override + String toString() { + return (StringBuffer('PreferencesTableData(') + ..write('id: $id, ') + ..write('audioQuality: $audioQuality, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('invidiousInstance: $invidiousInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSource: $audioSource, ') + ..write('youtubeClientEngine: $youtubeClientEngine, ') + ..write('streamMusicCodec: $streamMusicCodec, ') + ..write('downloadMusicCodec: $downloadMusicCodec, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect, ') + ..write('connectPort: $connectPort, ') + ..write('cacheMusic: $cacheMusic') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + id, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + invidiousInstance, + themeMode, + audioSource, + youtubeClientEngine, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect, + connectPort, + cacheMusic + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PreferencesTableData && + other.id == this.id && + other.audioQuality == this.audioQuality && + other.albumColorSync == this.albumColorSync && + other.amoledDarkTheme == this.amoledDarkTheme && + other.checkUpdate == this.checkUpdate && + other.normalizeAudio == this.normalizeAudio && + other.showSystemTrayIcon == this.showSystemTrayIcon && + other.systemTitleBar == this.systemTitleBar && + other.skipNonMusic == this.skipNonMusic && + other.closeBehavior == this.closeBehavior && + other.accentColorScheme == this.accentColorScheme && + other.layoutMode == this.layoutMode && + other.locale == this.locale && + other.market == this.market && + other.searchMode == this.searchMode && + other.downloadLocation == this.downloadLocation && + other.localLibraryLocation == this.localLibraryLocation && + other.pipedInstance == this.pipedInstance && + other.invidiousInstance == this.invidiousInstance && + other.themeMode == this.themeMode && + other.audioSource == this.audioSource && + other.youtubeClientEngine == this.youtubeClientEngine && + other.streamMusicCodec == this.streamMusicCodec && + other.downloadMusicCodec == this.downloadMusicCodec && + other.discordPresence == this.discordPresence && + other.endlessPlayback == this.endlessPlayback && + other.enableConnect == this.enableConnect && + other.connectPort == this.connectPort && + other.cacheMusic == this.cacheMusic); +} + +class PreferencesTableCompanion extends UpdateCompanion { + final Value id; + final Value audioQuality; + final Value albumColorSync; + final Value amoledDarkTheme; + final Value checkUpdate; + final Value normalizeAudio; + final Value showSystemTrayIcon; + final Value systemTitleBar; + final Value skipNonMusic; + final Value closeBehavior; + final Value accentColorScheme; + final Value layoutMode; + final Value locale; + final Value market; + final Value searchMode; + final Value downloadLocation; + final Value localLibraryLocation; + final Value pipedInstance; + final Value invidiousInstance; + final Value themeMode; + final Value audioSource; + final Value youtubeClientEngine; + final Value streamMusicCodec; + final Value downloadMusicCodec; + final Value discordPresence; + final Value endlessPlayback; + final Value enableConnect; + final Value connectPort; + final Value cacheMusic; + const PreferencesTableCompanion({ + this.id = const Value.absent(), + this.audioQuality = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.invidiousInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSource = const Value.absent(), + this.youtubeClientEngine = const Value.absent(), + this.streamMusicCodec = const Value.absent(), + this.downloadMusicCodec = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + this.connectPort = const Value.absent(), + this.cacheMusic = const Value.absent(), + }); + PreferencesTableCompanion.insert({ + this.id = const Value.absent(), + this.audioQuality = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.invidiousInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSource = const Value.absent(), + this.youtubeClientEngine = const Value.absent(), + this.streamMusicCodec = const Value.absent(), + this.downloadMusicCodec = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + this.connectPort = const Value.absent(), + this.cacheMusic = const Value.absent(), + }); + static Insertable custom({ + Expression? id, + Expression? audioQuality, + Expression? albumColorSync, + Expression? amoledDarkTheme, + Expression? checkUpdate, + Expression? normalizeAudio, + Expression? showSystemTrayIcon, + Expression? systemTitleBar, + Expression? skipNonMusic, + Expression? closeBehavior, + Expression? accentColorScheme, + Expression? layoutMode, + Expression? locale, + Expression? market, + Expression? searchMode, + Expression? downloadLocation, + Expression? localLibraryLocation, + Expression? pipedInstance, + Expression? invidiousInstance, + Expression? themeMode, + Expression? audioSource, + Expression? youtubeClientEngine, + Expression? streamMusicCodec, + Expression? downloadMusicCodec, + Expression? discordPresence, + Expression? endlessPlayback, + Expression? enableConnect, + Expression? connectPort, + Expression? cacheMusic, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (audioQuality != null) 'audio_quality': audioQuality, + if (albumColorSync != null) 'album_color_sync': albumColorSync, + if (amoledDarkTheme != null) 'amoled_dark_theme': amoledDarkTheme, + if (checkUpdate != null) 'check_update': checkUpdate, + if (normalizeAudio != null) 'normalize_audio': normalizeAudio, + if (showSystemTrayIcon != null) + 'show_system_tray_icon': showSystemTrayIcon, + if (systemTitleBar != null) 'system_title_bar': systemTitleBar, + if (skipNonMusic != null) 'skip_non_music': skipNonMusic, + if (closeBehavior != null) 'close_behavior': closeBehavior, + if (accentColorScheme != null) 'accent_color_scheme': accentColorScheme, + if (layoutMode != null) 'layout_mode': layoutMode, + if (locale != null) 'locale': locale, + if (market != null) 'market': market, + if (searchMode != null) 'search_mode': searchMode, + if (downloadLocation != null) 'download_location': downloadLocation, + if (localLibraryLocation != null) + 'local_library_location': localLibraryLocation, + if (pipedInstance != null) 'piped_instance': pipedInstance, + if (invidiousInstance != null) 'invidious_instance': invidiousInstance, + if (themeMode != null) 'theme_mode': themeMode, + if (audioSource != null) 'audio_source': audioSource, + if (youtubeClientEngine != null) + 'youtube_client_engine': youtubeClientEngine, + if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec, + if (downloadMusicCodec != null) + 'download_music_codec': downloadMusicCodec, + if (discordPresence != null) 'discord_presence': discordPresence, + if (endlessPlayback != null) 'endless_playback': endlessPlayback, + if (enableConnect != null) 'enable_connect': enableConnect, + if (connectPort != null) 'connect_port': connectPort, + if (cacheMusic != null) 'cache_music': cacheMusic, + }); + } + + PreferencesTableCompanion copyWith( + {Value? id, + Value? audioQuality, + Value? albumColorSync, + Value? amoledDarkTheme, + Value? checkUpdate, + Value? normalizeAudio, + Value? showSystemTrayIcon, + Value? systemTitleBar, + Value? skipNonMusic, + Value? closeBehavior, + Value? accentColorScheme, + Value? layoutMode, + Value? locale, + Value? market, + Value? searchMode, + Value? downloadLocation, + Value? localLibraryLocation, + Value? pipedInstance, + Value? invidiousInstance, + Value? themeMode, + Value? audioSource, + Value? youtubeClientEngine, + Value? streamMusicCodec, + Value? downloadMusicCodec, + Value? discordPresence, + Value? endlessPlayback, + Value? enableConnect, + Value? connectPort, + Value? cacheMusic}) { + return PreferencesTableCompanion( + id: id ?? this.id, + audioQuality: audioQuality ?? this.audioQuality, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + invidiousInstance: invidiousInstance ?? this.invidiousInstance, + themeMode: themeMode ?? this.themeMode, + audioSource: audioSource ?? this.audioSource, + youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, + streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, + downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + connectPort: connectPort ?? this.connectPort, + cacheMusic: cacheMusic ?? this.cacheMusic, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (audioQuality.present) { + map['audio_quality'] = Variable(audioQuality.value); + } + if (albumColorSync.present) { + map['album_color_sync'] = Variable(albumColorSync.value); + } + if (amoledDarkTheme.present) { + map['amoled_dark_theme'] = Variable(amoledDarkTheme.value); + } + if (checkUpdate.present) { + map['check_update'] = Variable(checkUpdate.value); + } + if (normalizeAudio.present) { + map['normalize_audio'] = Variable(normalizeAudio.value); + } + if (showSystemTrayIcon.present) { + map['show_system_tray_icon'] = Variable(showSystemTrayIcon.value); + } + if (systemTitleBar.present) { + map['system_title_bar'] = Variable(systemTitleBar.value); + } + if (skipNonMusic.present) { + map['skip_non_music'] = Variable(skipNonMusic.value); + } + if (closeBehavior.present) { + map['close_behavior'] = Variable(closeBehavior.value); + } + if (accentColorScheme.present) { + map['accent_color_scheme'] = Variable(accentColorScheme.value); + } + if (layoutMode.present) { + map['layout_mode'] = Variable(layoutMode.value); + } + if (locale.present) { + map['locale'] = Variable(locale.value); + } + if (market.present) { + map['market'] = Variable(market.value); + } + if (searchMode.present) { + map['search_mode'] = Variable(searchMode.value); + } + if (downloadLocation.present) { + map['download_location'] = Variable(downloadLocation.value); + } + if (localLibraryLocation.present) { + map['local_library_location'] = + Variable(localLibraryLocation.value); + } + if (pipedInstance.present) { + map['piped_instance'] = Variable(pipedInstance.value); + } + if (invidiousInstance.present) { + map['invidious_instance'] = Variable(invidiousInstance.value); + } + if (themeMode.present) { + map['theme_mode'] = Variable(themeMode.value); + } + if (audioSource.present) { + map['audio_source'] = Variable(audioSource.value); + } + if (youtubeClientEngine.present) { + map['youtube_client_engine'] = + Variable(youtubeClientEngine.value); + } + if (streamMusicCodec.present) { + map['stream_music_codec'] = Variable(streamMusicCodec.value); + } + if (downloadMusicCodec.present) { + map['download_music_codec'] = Variable(downloadMusicCodec.value); + } + if (discordPresence.present) { + map['discord_presence'] = Variable(discordPresence.value); + } + if (endlessPlayback.present) { + map['endless_playback'] = Variable(endlessPlayback.value); + } + if (enableConnect.present) { + map['enable_connect'] = Variable(enableConnect.value); + } + if (connectPort.present) { + map['connect_port'] = Variable(connectPort.value); + } + if (cacheMusic.present) { + map['cache_music'] = Variable(cacheMusic.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PreferencesTableCompanion(') + ..write('id: $id, ') + ..write('audioQuality: $audioQuality, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('invidiousInstance: $invidiousInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSource: $audioSource, ') + ..write('youtubeClientEngine: $youtubeClientEngine, ') + ..write('streamMusicCodec: $streamMusicCodec, ') + ..write('downloadMusicCodec: $downloadMusicCodec, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect, ') + ..write('connectPort: $connectPort, ') + ..write('cacheMusic: $cacheMusic') + ..write(')')) + .toString(); + } +} + +class ScrobblerTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ScrobblerTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + late final GeneratedColumn username = GeneratedColumn( + 'username', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn passwordHash = GeneratedColumn( + 'password_hash', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, username, passwordHash]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'scrobbler_table'; + @override + Set get $primaryKey => {id}; + @override + ScrobblerTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ScrobblerTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username'])!, + passwordHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}password_hash'])!, + ); + } + + @override + ScrobblerTable createAlias(String alias) { + return ScrobblerTable(attachedDatabase, alias); + } +} + +class ScrobblerTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final String username; + final String passwordHash; + const ScrobblerTableData( + {required this.id, + required this.createdAt, + required this.username, + required this.passwordHash}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['username'] = Variable(username); + map['password_hash'] = Variable(passwordHash); + return map; + } + + ScrobblerTableCompanion toCompanion(bool nullToAbsent) { + return ScrobblerTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + username: Value(username), + passwordHash: Value(passwordHash), + ); + } + + factory ScrobblerTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ScrobblerTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + username: serializer.fromJson(json['username']), + passwordHash: serializer.fromJson(json['passwordHash']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'username': serializer.toJson(username), + 'passwordHash': serializer.toJson(passwordHash), + }; + } + + ScrobblerTableData copyWith( + {int? id, + DateTime? createdAt, + String? username, + String? passwordHash}) => + ScrobblerTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + ScrobblerTableData copyWithCompanion(ScrobblerTableCompanion data) { + return ScrobblerTableData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + username: data.username.present ? data.username.value : this.username, + passwordHash: data.passwordHash.present + ? data.passwordHash.value + : this.passwordHash, + ); + } + + @override + String toString() { + return (StringBuffer('ScrobblerTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, username, passwordHash); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScrobblerTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.username == this.username && + other.passwordHash == this.passwordHash); +} + +class ScrobblerTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value username; + final Value passwordHash; + const ScrobblerTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.username = const Value.absent(), + this.passwordHash = const Value.absent(), + }); + ScrobblerTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required String username, + required String passwordHash, + }) : username = Value(username), + passwordHash = Value(passwordHash); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? username, + Expression? passwordHash, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (username != null) 'username': username, + if (passwordHash != null) 'password_hash': passwordHash, + }); + } + + ScrobblerTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? username, + Value? passwordHash}) { + return ScrobblerTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (passwordHash.present) { + map['password_hash'] = Variable(passwordHash.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ScrobblerTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } +} + +class SkipSegmentTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SkipSegmentTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn start = GeneratedColumn( + 'start', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn end = GeneratedColumn( + 'end', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [id, start, end, trackId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'skip_segment_table'; + @override + Set get $primaryKey => {id}; + @override + SkipSegmentTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SkipSegmentTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + start: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}start'])!, + end: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}end'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SkipSegmentTable createAlias(String alias) { + return SkipSegmentTable(attachedDatabase, alias); + } +} + +class SkipSegmentTableData extends DataClass + implements Insertable { + final int id; + final int start; + final int end; + final String trackId; + final DateTime createdAt; + const SkipSegmentTableData( + {required this.id, + required this.start, + required this.end, + required this.trackId, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['start'] = Variable(start); + map['end'] = Variable(end); + map['track_id'] = Variable(trackId); + map['created_at'] = Variable(createdAt); + return map; + } + + SkipSegmentTableCompanion toCompanion(bool nullToAbsent) { + return SkipSegmentTableCompanion( + id: Value(id), + start: Value(start), + end: Value(end), + trackId: Value(trackId), + createdAt: Value(createdAt), + ); + } + + factory SkipSegmentTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SkipSegmentTableData( + id: serializer.fromJson(json['id']), + start: serializer.fromJson(json['start']), + end: serializer.fromJson(json['end']), + trackId: serializer.fromJson(json['trackId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'start': serializer.toJson(start), + 'end': serializer.toJson(end), + 'trackId': serializer.toJson(trackId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SkipSegmentTableData copyWith( + {int? id, + int? start, + int? end, + String? trackId, + DateTime? createdAt}) => + SkipSegmentTableData( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + SkipSegmentTableData copyWithCompanion(SkipSegmentTableCompanion data) { + return SkipSegmentTableData( + id: data.id.present ? data.id.value : this.id, + start: data.start.present ? data.start.value : this.start, + end: data.end.present ? data.end.value : this.end, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SkipSegmentTableData(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, start, end, trackId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SkipSegmentTableData && + other.id == this.id && + other.start == this.start && + other.end == this.end && + other.trackId == this.trackId && + other.createdAt == this.createdAt); +} + +class SkipSegmentTableCompanion extends UpdateCompanion { + final Value id; + final Value start; + final Value end; + final Value trackId; + final Value createdAt; + const SkipSegmentTableCompanion({ + this.id = const Value.absent(), + this.start = const Value.absent(), + this.end = const Value.absent(), + this.trackId = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SkipSegmentTableCompanion.insert({ + this.id = const Value.absent(), + required int start, + required int end, + required String trackId, + this.createdAt = const Value.absent(), + }) : start = Value(start), + end = Value(end), + trackId = Value(trackId); + static Insertable custom({ + Expression? id, + Expression? start, + Expression? end, + Expression? trackId, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (start != null) 'start': start, + if (end != null) 'end': end, + if (trackId != null) 'track_id': trackId, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SkipSegmentTableCompanion copyWith( + {Value? id, + Value? start, + Value? end, + Value? trackId, + Value? createdAt}) { + return SkipSegmentTableCompanion( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (start.present) { + map['start'] = Variable(start.value); + } + if (end.present) { + map['end'] = Variable(end.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SkipSegmentTableCompanion(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class SourceMatchTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SourceMatchTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn sourceId = GeneratedColumn( + 'source_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceType.youtube.name)); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [id, trackId, sourceId, sourceType, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'source_match_table'; + @override + Set get $primaryKey => {id}; + @override + SourceMatchTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SourceMatchTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + sourceId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_id'])!, + sourceType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_type'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SourceMatchTable createAlias(String alias) { + return SourceMatchTable(attachedDatabase, alias); + } +} + +class SourceMatchTableData extends DataClass + implements Insertable { + final int id; + final String trackId; + final String sourceId; + final String sourceType; + final DateTime createdAt; + const SourceMatchTableData( + {required this.id, + required this.trackId, + required this.sourceId, + required this.sourceType, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + map['source_id'] = Variable(sourceId); + map['source_type'] = Variable(sourceType); + map['created_at'] = Variable(createdAt); + return map; + } + + SourceMatchTableCompanion toCompanion(bool nullToAbsent) { + return SourceMatchTableCompanion( + id: Value(id), + trackId: Value(trackId), + sourceId: Value(sourceId), + sourceType: Value(sourceType), + createdAt: Value(createdAt), + ); + } + + factory SourceMatchTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SourceMatchTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + sourceId: serializer.fromJson(json['sourceId']), + sourceType: serializer.fromJson(json['sourceType']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'sourceId': serializer.toJson(sourceId), + 'sourceType': serializer.toJson(sourceType), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SourceMatchTableData copyWith( + {int? id, + String? trackId, + String? sourceId, + String? sourceType, + DateTime? createdAt}) => + SourceMatchTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceId: sourceId ?? this.sourceId, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + SourceMatchTableData copyWithCompanion(SourceMatchTableCompanion data) { + return SourceMatchTableData( + id: data.id.present ? data.id.value : this.id, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + sourceId: data.sourceId.present ? data.sourceId.value : this.sourceId, + sourceType: + data.sourceType.present ? data.sourceType.value : this.sourceType, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SourceMatchTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceId: $sourceId, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, trackId, sourceId, sourceType, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SourceMatchTableData && + other.id == this.id && + other.trackId == this.trackId && + other.sourceId == this.sourceId && + other.sourceType == this.sourceType && + other.createdAt == this.createdAt); +} + +class SourceMatchTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value sourceId; + final Value sourceType; + final Value createdAt; + const SourceMatchTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.sourceId = const Value.absent(), + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SourceMatchTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required String sourceId, + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }) : trackId = Value(trackId), + sourceId = Value(sourceId); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? sourceId, + Expression? sourceType, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (sourceId != null) 'source_id': sourceId, + if (sourceType != null) 'source_type': sourceType, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SourceMatchTableCompanion copyWith( + {Value? id, + Value? trackId, + Value? sourceId, + Value? sourceType, + Value? createdAt}) { + return SourceMatchTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceId: sourceId ?? this.sourceId, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (sourceId.present) { + map['source_id'] = Variable(sourceId.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SourceMatchTableCompanion(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceId: $sourceId, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class AudioPlayerStateTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AudioPlayerStateTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn playing = GeneratedColumn( + 'playing', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))')); + late final GeneratedColumn loopMode = GeneratedColumn( + 'loop_mode', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn shuffled = GeneratedColumn( + 'shuffled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))')); + late final GeneratedColumn collections = GeneratedColumn( + 'collections', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn tracks = GeneratedColumn( + 'tracks', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("[]")); + late final GeneratedColumn currentIndex = GeneratedColumn( + 'current_index', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + @override + List get $columns => + [id, playing, loopMode, shuffled, collections, tracks, currentIndex]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'audio_player_state_table'; + @override + Set get $primaryKey => {id}; + @override + AudioPlayerStateTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AudioPlayerStateTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + playing: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}playing'])!, + loopMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}loop_mode'])!, + shuffled: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}shuffled'])!, + collections: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}collections'])!, + tracks: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}tracks'])!, + currentIndex: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}current_index'])!, + ); + } + + @override + AudioPlayerStateTable createAlias(String alias) { + return AudioPlayerStateTable(attachedDatabase, alias); + } +} + +class AudioPlayerStateTableData extends DataClass + implements Insertable { + final int id; + final bool playing; + final String loopMode; + final bool shuffled; + final String collections; + final String tracks; + final int currentIndex; + const AudioPlayerStateTableData( + {required this.id, + required this.playing, + required this.loopMode, + required this.shuffled, + required this.collections, + required this.tracks, + required this.currentIndex}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['playing'] = Variable(playing); + map['loop_mode'] = Variable(loopMode); + map['shuffled'] = Variable(shuffled); + map['collections'] = Variable(collections); + map['tracks'] = Variable(tracks); + map['current_index'] = Variable(currentIndex); + return map; + } + + AudioPlayerStateTableCompanion toCompanion(bool nullToAbsent) { + return AudioPlayerStateTableCompanion( + id: Value(id), + playing: Value(playing), + loopMode: Value(loopMode), + shuffled: Value(shuffled), + collections: Value(collections), + tracks: Value(tracks), + currentIndex: Value(currentIndex), + ); + } + + factory AudioPlayerStateTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AudioPlayerStateTableData( + id: serializer.fromJson(json['id']), + playing: serializer.fromJson(json['playing']), + loopMode: serializer.fromJson(json['loopMode']), + shuffled: serializer.fromJson(json['shuffled']), + collections: serializer.fromJson(json['collections']), + tracks: serializer.fromJson(json['tracks']), + currentIndex: serializer.fromJson(json['currentIndex']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'playing': serializer.toJson(playing), + 'loopMode': serializer.toJson(loopMode), + 'shuffled': serializer.toJson(shuffled), + 'collections': serializer.toJson(collections), + 'tracks': serializer.toJson(tracks), + 'currentIndex': serializer.toJson(currentIndex), + }; + } + + AudioPlayerStateTableData copyWith( + {int? id, + bool? playing, + String? loopMode, + bool? shuffled, + String? collections, + String? tracks, + int? currentIndex}) => + AudioPlayerStateTableData( + id: id ?? this.id, + playing: playing ?? this.playing, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, + tracks: tracks ?? this.tracks, + currentIndex: currentIndex ?? this.currentIndex, + ); + AudioPlayerStateTableData copyWithCompanion( + AudioPlayerStateTableCompanion data) { + return AudioPlayerStateTableData( + id: data.id.present ? data.id.value : this.id, + playing: data.playing.present ? data.playing.value : this.playing, + loopMode: data.loopMode.present ? data.loopMode.value : this.loopMode, + shuffled: data.shuffled.present ? data.shuffled.value : this.shuffled, + collections: + data.collections.present ? data.collections.value : this.collections, + tracks: data.tracks.present ? data.tracks.value : this.tracks, + currentIndex: data.currentIndex.present + ? data.currentIndex.value + : this.currentIndex, + ); + } + + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableData(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections, ') + ..write('tracks: $tracks, ') + ..write('currentIndex: $currentIndex') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, playing, loopMode, shuffled, collections, tracks, currentIndex); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AudioPlayerStateTableData && + other.id == this.id && + other.playing == this.playing && + other.loopMode == this.loopMode && + other.shuffled == this.shuffled && + other.collections == this.collections && + other.tracks == this.tracks && + other.currentIndex == this.currentIndex); +} + +class AudioPlayerStateTableCompanion + extends UpdateCompanion { + final Value id; + final Value playing; + final Value loopMode; + final Value shuffled; + final Value collections; + final Value tracks; + final Value currentIndex; + const AudioPlayerStateTableCompanion({ + this.id = const Value.absent(), + this.playing = const Value.absent(), + this.loopMode = const Value.absent(), + this.shuffled = const Value.absent(), + this.collections = const Value.absent(), + this.tracks = const Value.absent(), + this.currentIndex = const Value.absent(), + }); + AudioPlayerStateTableCompanion.insert({ + this.id = const Value.absent(), + required bool playing, + required String loopMode, + required bool shuffled, + required String collections, + this.tracks = const Value.absent(), + this.currentIndex = const Value.absent(), + }) : playing = Value(playing), + loopMode = Value(loopMode), + shuffled = Value(shuffled), + collections = Value(collections); + static Insertable custom({ + Expression? id, + Expression? playing, + Expression? loopMode, + Expression? shuffled, + Expression? collections, + Expression? tracks, + Expression? currentIndex, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (playing != null) 'playing': playing, + if (loopMode != null) 'loop_mode': loopMode, + if (shuffled != null) 'shuffled': shuffled, + if (collections != null) 'collections': collections, + if (tracks != null) 'tracks': tracks, + if (currentIndex != null) 'current_index': currentIndex, + }); + } + + AudioPlayerStateTableCompanion copyWith( + {Value? id, + Value? playing, + Value? loopMode, + Value? shuffled, + Value? collections, + Value? tracks, + Value? currentIndex}) { + return AudioPlayerStateTableCompanion( + id: id ?? this.id, + playing: playing ?? this.playing, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, + tracks: tracks ?? this.tracks, + currentIndex: currentIndex ?? this.currentIndex, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (playing.present) { + map['playing'] = Variable(playing.value); + } + if (loopMode.present) { + map['loop_mode'] = Variable(loopMode.value); + } + if (shuffled.present) { + map['shuffled'] = Variable(shuffled.value); + } + if (collections.present) { + map['collections'] = Variable(collections.value); + } + if (tracks.present) { + map['tracks'] = Variable(tracks.value); + } + if (currentIndex.present) { + map['current_index'] = Variable(currentIndex.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableCompanion(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections, ') + ..write('tracks: $tracks, ') + ..write('currentIndex: $currentIndex') + ..write(')')) + .toString(); + } +} + +class HistoryTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + HistoryTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn itemId = GeneratedColumn( + 'item_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, type, itemId, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'history_table'; + @override + Set get $primaryKey => {id}; + @override + HistoryTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return HistoryTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + itemId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}item_id'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + ); + } + + @override + HistoryTable createAlias(String alias) { + return HistoryTable(attachedDatabase, alias); + } +} + +class HistoryTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final String type; + final String itemId; + final String data; + const HistoryTableData( + {required this.id, + required this.createdAt, + required this.type, + required this.itemId, + required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['type'] = Variable(type); + map['item_id'] = Variable(itemId); + map['data'] = Variable(data); + return map; + } + + HistoryTableCompanion toCompanion(bool nullToAbsent) { + return HistoryTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + type: Value(type), + itemId: Value(itemId), + data: Value(data), + ); + } + + factory HistoryTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return HistoryTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + type: serializer.fromJson(json['type']), + itemId: serializer.fromJson(json['itemId']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'type': serializer.toJson(type), + 'itemId': serializer.toJson(itemId), + 'data': serializer.toJson(data), + }; + } + + HistoryTableData copyWith( + {int? id, + DateTime? createdAt, + String? type, + String? itemId, + String? data}) => + HistoryTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + HistoryTableData copyWithCompanion(HistoryTableCompanion data) { + return HistoryTableData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + type: data.type.present ? data.type.value : this.type, + itemId: data.itemId.present ? data.itemId.value : this.itemId, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('HistoryTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, type, itemId, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is HistoryTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.type == this.type && + other.itemId == this.itemId && + other.data == this.data); +} + +class HistoryTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value type; + final Value itemId; + final Value data; + const HistoryTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.type = const Value.absent(), + this.itemId = const Value.absent(), + this.data = const Value.absent(), + }); + HistoryTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required String type, + required String itemId, + required String data, + }) : type = Value(type), + itemId = Value(itemId), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? type, + Expression? itemId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (type != null) 'type': type, + if (itemId != null) 'item_id': itemId, + if (data != null) 'data': data, + }); + } + + HistoryTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? type, + Value? itemId, + Value? data}) { + return HistoryTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (itemId.present) { + map['item_id'] = Variable(itemId.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('HistoryTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + +class LyricsTable extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LyricsTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, trackId, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'lyrics_table'; + @override + Set get $primaryKey => {id}; + @override + LyricsTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LyricsTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + ); + } + + @override + LyricsTable createAlias(String alias) { + return LyricsTable(attachedDatabase, alias); + } +} + +class LyricsTableData extends DataClass implements Insertable { + final int id; + final String trackId; + final String data; + const LyricsTableData( + {required this.id, required this.trackId, required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + map['data'] = Variable(data); + return map; + } + + LyricsTableCompanion toCompanion(bool nullToAbsent) { + return LyricsTableCompanion( + id: Value(id), + trackId: Value(trackId), + data: Value(data), + ); + } + + factory LyricsTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LyricsTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'data': serializer.toJson(data), + }; + } + + LyricsTableData copyWith({int? id, String? trackId, String? data}) => + LyricsTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + data: data ?? this.data, + ); + LyricsTableData copyWithCompanion(LyricsTableCompanion data) { + return LyricsTableData( + id: data.id.present ? data.id.value : this.id, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('LyricsTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, trackId, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LyricsTableData && + other.id == this.id && + other.trackId == this.trackId && + other.data == this.data); +} + +class LyricsTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value data; + const LyricsTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.data = const Value.absent(), + }); + LyricsTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required String data, + }) : trackId = Value(trackId), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (data != null) 'data': data, + }); + } + + LyricsTableCompanion copyWith( + {Value? id, Value? trackId, Value? data}) { + return LyricsTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LyricsTableCompanion(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + +class PluginsTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PluginsTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + additionalChecks: + GeneratedColumn.checkTextLength(minTextLength: 1, maxTextLength: 50), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn version = GeneratedColumn( + 'version', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn author = GeneratedColumn( + 'author', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn entryPoint = GeneratedColumn( + 'entry_point', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn apis = GeneratedColumn( + 'apis', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn abilities = GeneratedColumn( + 'abilities', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn selectedForMetadata = GeneratedColumn( + 'selected_for_metadata', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_metadata" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn selectedForAudioSource = + GeneratedColumn('selected_for_audio_source', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_audio_source" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn repository = GeneratedColumn( + 'repository', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn pluginApiVersion = GeneratedColumn( + 'plugin_api_version', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('2.0.0')); + @override + List get $columns => [ + id, + name, + description, + version, + author, + entryPoint, + apis, + abilities, + selectedForMetadata, + selectedForAudioSource, + repository, + pluginApiVersion + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'plugins_table'; + @override + Set get $primaryKey => {id}; + @override + PluginsTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PluginsTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + version: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}version'])!, + author: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}author'])!, + entryPoint: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}entry_point'])!, + apis: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}apis'])!, + abilities: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}abilities'])!, + selectedForMetadata: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}selected_for_metadata'])!, + selectedForAudioSource: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}selected_for_audio_source'])!, + repository: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}repository']), + pluginApiVersion: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}plugin_api_version'])!, + ); + } + + @override + PluginsTable createAlias(String alias) { + return PluginsTable(attachedDatabase, alias); + } +} + +class PluginsTableData extends DataClass + implements Insertable { + final int id; + final String name; + final String description; + final String version; + final String author; + final String entryPoint; + final String apis; + final String abilities; + final bool selectedForMetadata; + final bool selectedForAudioSource; + final String? repository; + final String pluginApiVersion; + const PluginsTableData( + {required this.id, + required this.name, + required this.description, + required this.version, + required this.author, + required this.entryPoint, + required this.apis, + required this.abilities, + required this.selectedForMetadata, + required this.selectedForAudioSource, + this.repository, + required this.pluginApiVersion}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['version'] = Variable(version); + map['author'] = Variable(author); + map['entry_point'] = Variable(entryPoint); + map['apis'] = Variable(apis); + map['abilities'] = Variable(abilities); + map['selected_for_metadata'] = Variable(selectedForMetadata); + map['selected_for_audio_source'] = Variable(selectedForAudioSource); + if (!nullToAbsent || repository != null) { + map['repository'] = Variable(repository); + } + map['plugin_api_version'] = Variable(pluginApiVersion); + return map; + } + + PluginsTableCompanion toCompanion(bool nullToAbsent) { + return PluginsTableCompanion( + id: Value(id), + name: Value(name), + description: Value(description), + version: Value(version), + author: Value(author), + entryPoint: Value(entryPoint), + apis: Value(apis), + abilities: Value(abilities), + selectedForMetadata: Value(selectedForMetadata), + selectedForAudioSource: Value(selectedForAudioSource), + repository: repository == null && nullToAbsent + ? const Value.absent() + : Value(repository), + pluginApiVersion: Value(pluginApiVersion), + ); + } + + factory PluginsTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PluginsTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + version: serializer.fromJson(json['version']), + author: serializer.fromJson(json['author']), + entryPoint: serializer.fromJson(json['entryPoint']), + apis: serializer.fromJson(json['apis']), + abilities: serializer.fromJson(json['abilities']), + selectedForMetadata: + serializer.fromJson(json['selectedForMetadata']), + selectedForAudioSource: + serializer.fromJson(json['selectedForAudioSource']), + repository: serializer.fromJson(json['repository']), + pluginApiVersion: serializer.fromJson(json['pluginApiVersion']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'version': serializer.toJson(version), + 'author': serializer.toJson(author), + 'entryPoint': serializer.toJson(entryPoint), + 'apis': serializer.toJson(apis), + 'abilities': serializer.toJson(abilities), + 'selectedForMetadata': serializer.toJson(selectedForMetadata), + 'selectedForAudioSource': serializer.toJson(selectedForAudioSource), + 'repository': serializer.toJson(repository), + 'pluginApiVersion': serializer.toJson(pluginApiVersion), + }; + } + + PluginsTableData copyWith( + {int? id, + String? name, + String? description, + String? version, + String? author, + String? entryPoint, + String? apis, + String? abilities, + bool? selectedForMetadata, + bool? selectedForAudioSource, + Value repository = const Value.absent(), + String? pluginApiVersion}) => + PluginsTableData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + version: version ?? this.version, + author: author ?? this.author, + entryPoint: entryPoint ?? this.entryPoint, + apis: apis ?? this.apis, + abilities: abilities ?? this.abilities, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, + repository: repository.present ? repository.value : this.repository, + pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, + ); + PluginsTableData copyWithCompanion(PluginsTableCompanion data) { + return PluginsTableData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + version: data.version.present ? data.version.value : this.version, + author: data.author.present ? data.author.value : this.author, + entryPoint: + data.entryPoint.present ? data.entryPoint.value : this.entryPoint, + apis: data.apis.present ? data.apis.value : this.apis, + abilities: data.abilities.present ? data.abilities.value : this.abilities, + selectedForMetadata: data.selectedForMetadata.present + ? data.selectedForMetadata.value + : this.selectedForMetadata, + selectedForAudioSource: data.selectedForAudioSource.present + ? data.selectedForAudioSource.value + : this.selectedForAudioSource, + repository: + data.repository.present ? data.repository.value : this.repository, + pluginApiVersion: data.pluginApiVersion.present + ? data.pluginApiVersion.value + : this.pluginApiVersion, + ); + } + + @override + String toString() { + return (StringBuffer('PluginsTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('version: $version, ') + ..write('author: $author, ') + ..write('entryPoint: $entryPoint, ') + ..write('apis: $apis, ') + ..write('abilities: $abilities, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') + ..write('repository: $repository, ') + ..write('pluginApiVersion: $pluginApiVersion') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + version, + author, + entryPoint, + apis, + abilities, + selectedForMetadata, + selectedForAudioSource, + repository, + pluginApiVersion); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PluginsTableData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.version == this.version && + other.author == this.author && + other.entryPoint == this.entryPoint && + other.apis == this.apis && + other.abilities == this.abilities && + other.selectedForMetadata == this.selectedForMetadata && + other.selectedForAudioSource == this.selectedForAudioSource && + other.repository == this.repository && + other.pluginApiVersion == this.pluginApiVersion); +} + +class PluginsTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value version; + final Value author; + final Value entryPoint; + final Value apis; + final Value abilities; + final Value selectedForMetadata; + final Value selectedForAudioSource; + final Value repository; + final Value pluginApiVersion; + const PluginsTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.version = const Value.absent(), + this.author = const Value.absent(), + this.entryPoint = const Value.absent(), + this.apis = const Value.absent(), + this.abilities = const Value.absent(), + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), + this.repository = const Value.absent(), + this.pluginApiVersion = const Value.absent(), + }); + PluginsTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required String description, + required String version, + required String author, + required String entryPoint, + required String apis, + required String abilities, + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), + this.repository = const Value.absent(), + this.pluginApiVersion = const Value.absent(), + }) : name = Value(name), + description = Value(description), + version = Value(version), + author = Value(author), + entryPoint = Value(entryPoint), + apis = Value(apis), + abilities = Value(abilities); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? version, + Expression? author, + Expression? entryPoint, + Expression? apis, + Expression? abilities, + Expression? selectedForMetadata, + Expression? selectedForAudioSource, + Expression? repository, + Expression? pluginApiVersion, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (version != null) 'version': version, + if (author != null) 'author': author, + if (entryPoint != null) 'entry_point': entryPoint, + if (apis != null) 'apis': apis, + if (abilities != null) 'abilities': abilities, + if (selectedForMetadata != null) + 'selected_for_metadata': selectedForMetadata, + if (selectedForAudioSource != null) + 'selected_for_audio_source': selectedForAudioSource, + if (repository != null) 'repository': repository, + if (pluginApiVersion != null) 'plugin_api_version': pluginApiVersion, + }); + } + + PluginsTableCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value? version, + Value? author, + Value? entryPoint, + Value? apis, + Value? abilities, + Value? selectedForMetadata, + Value? selectedForAudioSource, + Value? repository, + Value? pluginApiVersion}) { + return PluginsTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + version: version ?? this.version, + author: author ?? this.author, + entryPoint: entryPoint ?? this.entryPoint, + apis: apis ?? this.apis, + abilities: abilities ?? this.abilities, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, + repository: repository ?? this.repository, + pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (version.present) { + map['version'] = Variable(version.value); + } + if (author.present) { + map['author'] = Variable(author.value); + } + if (entryPoint.present) { + map['entry_point'] = Variable(entryPoint.value); + } + if (apis.present) { + map['apis'] = Variable(apis.value); + } + if (abilities.present) { + map['abilities'] = Variable(abilities.value); + } + if (selectedForMetadata.present) { + map['selected_for_metadata'] = Variable(selectedForMetadata.value); + } + if (selectedForAudioSource.present) { + map['selected_for_audio_source'] = + Variable(selectedForAudioSource.value); + } + if (repository.present) { + map['repository'] = Variable(repository.value); + } + if (pluginApiVersion.present) { + map['plugin_api_version'] = Variable(pluginApiVersion.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PluginsTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('version: $version, ') + ..write('author: $author, ') + ..write('entryPoint: $entryPoint, ') + ..write('apis: $apis, ') + ..write('abilities: $abilities, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') + ..write('repository: $repository, ') + ..write('pluginApiVersion: $pluginApiVersion') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV9 extends GeneratedDatabase { + DatabaseAtV9(QueryExecutor e) : super(e); + late final AuthenticationTable authenticationTable = + AuthenticationTable(this); + late final BlacklistTable blacklistTable = BlacklistTable(this); + late final PreferencesTable preferencesTable = PreferencesTable(this); + late final ScrobblerTable scrobblerTable = ScrobblerTable(this); + late final SkipSegmentTable skipSegmentTable = SkipSegmentTable(this); + late final SourceMatchTable sourceMatchTable = SourceMatchTable(this); + late final AudioPlayerStateTable audioPlayerStateTable = + AudioPlayerStateTable(this); + late final HistoryTable historyTable = HistoryTable(this); + late final LyricsTable lyricsTable = LyricsTable(this); + late final PluginsTable pluginsTable = PluginsTable(this); + late final Index uniqueBlacklist = Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); + late final Index uniqTrackMatch = Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + authenticationTable, + blacklistTable, + preferencesTable, + scrobblerTable, + skipSegmentTable, + sourceMatchTable, + audioPlayerStateTable, + historyTable, + lyricsTable, + pluginsTable, + uniqueBlacklist, + uniqTrackMatch + ]; + @override + int get schemaVersion => 9; +} diff --git a/untranslated_messages.json b/untranslated_messages.json index 618ddcf8..bcde408d 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,5 +1,9 @@ { "ar": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -8,6 +12,10 @@ ], "bn": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -16,6 +24,10 @@ ], "ca": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -24,6 +36,10 @@ ], "cs": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -32,6 +48,10 @@ ], "de": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -40,6 +60,10 @@ ], "es": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -48,6 +72,10 @@ ], "eu": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -56,6 +84,10 @@ ], "fa": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -64,6 +96,10 @@ ], "fi": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -72,6 +108,10 @@ ], "fr": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -80,6 +120,10 @@ ], "hi": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -88,6 +132,10 @@ ], "id": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -96,6 +144,10 @@ ], "it": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -104,6 +156,10 @@ ], "ja": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -112,6 +168,10 @@ ], "ka": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -120,6 +180,10 @@ ], "ko": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -128,6 +192,10 @@ ], "ne": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -137,6 +205,10 @@ "nl": [ "audio_source", + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -145,6 +217,10 @@ ], "pl": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -153,6 +229,10 @@ ], "pt": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -161,6 +241,10 @@ ], "ru": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -169,6 +253,10 @@ ], "ta": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -177,6 +265,10 @@ ], "th": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -185,6 +277,10 @@ ], "tl": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -193,6 +289,10 @@ ], "tr": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -201,6 +301,10 @@ ], "uk": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -209,6 +313,10 @@ ], "vi": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -217,6 +325,10 @@ ], "zh": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -225,6 +337,10 @@ ], "zh_TW": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", From 99a84aa6dcf463a1b814e50a0ddfabb3b8ba0a16 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Nov 2025 13:32:48 +0600 Subject: [PATCH 04/14] chore: create sourced track from active audio source plugin --- lib/models/database/database.dart | 3 +- lib/models/database/tables/preferences.dart | 25 +- lib/models/database/tables/source_match.dart | 18 +- lib/models/playback/track_sources.dart | 116 +- .../local_folder/cache_export_dialog.dart | 1 - lib/modules/player/player.dart | 2 +- lib/modules/root/sidebar/sidebar.dart | 2 +- lib/pages/settings/sections/playback.dart | 1 - lib/provider/download_manager_provider.dart | 1 - lib/provider/server/routes/playback.dart | 1 - .../user_preferences_provider.dart | 1 - lib/services/sourced_track/enums.dart | 34 - .../sourced_track/models/video_info.dart | 136 - lib/services/sourced_track/sourced_track.dart | 470 ++- .../sourced_track/sources/dab_music.dart | 303 -- .../sourced_track/sources/invidious.dart | 263 -- .../sourced_track/sources/jiosaavn.dart | 231 -- lib/services/sourced_track/sources/piped.dart | 292 -- .../sourced_track/sources/youtube.dart | 439 --- .../youtube_explode_engine.dart | 1 - pubspec.lock | 11 +- pubspec.yaml | 3 +- test/drift/app_db/generated/schema.dart | 5 +- test/drift/app_db/generated/schema_v10.dart | 3472 +++++++++++++++++ 24 files changed, 3810 insertions(+), 2021 deletions(-) delete mode 100644 lib/services/sourced_track/enums.dart delete mode 100644 lib/services/sourced_track/models/video_info.dart delete mode 100644 lib/services/sourced_track/sources/dab_music.dart delete mode 100644 lib/services/sourced_track/sources/invidious.dart delete mode 100644 lib/services/sourced_track/sources/jiosaavn.dart delete mode 100644 lib/services/sourced_track/sources/piped.dart delete mode 100644 lib/services/sourced_track/sources/youtube.dart create mode 100644 test/drift/app_db/generated/schema_v10.dart diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 2df41e9a..a03cdb8c 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -16,7 +16,6 @@ import 'package:spotube/models/metadata/market.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; @@ -65,7 +64,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override - int get schemaVersion => 9; + int get schemaVersion => 10; @override MigrationStrategy get migration { diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index 64580330..ea2f7538 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -11,17 +11,6 @@ enum CloseBehavior { close, } -enum AudioSource { - youtube("YouTube"), - piped("Piped"), - jiosaavn("JioSaavn"), - invidious("Invidious"), - dabMusic("DAB Music"); - - final String label; - const AudioSource(this.label); -} - enum YoutubeClientEngine { ytDlp("yt-dlp"), youtubeExplode("YouTubeExplode"), @@ -56,8 +45,6 @@ enum SearchMode { class PreferencesTable extends Table { IntColumn get id => integer().autoIncrement()(); - TextColumn get audioQuality => textEnum() - .withDefault(Constant(SourceQualities.high.name))(); BoolColumn get albumColorSync => boolean().withDefault(const Constant(true))(); BoolColumn get amoledDarkTheme => @@ -95,14 +82,9 @@ class PreferencesTable extends Table { text().withDefault(const Constant("https://inv.nadeko.net"))(); TextColumn get themeMode => textEnum().withDefault(Constant(ThemeMode.system.name))(); - TextColumn get audioSource => - textEnum().withDefault(Constant(AudioSource.youtube.name))(); + TextColumn get audioSourceId => text().nullable()(); TextColumn get youtubeClientEngine => textEnum() .withDefault(Constant(YoutubeClientEngine.youtubeExplode.name))(); - TextColumn get streamMusicCodec => - textEnum().withDefault(Constant(SourceCodecs.weba.name))(); - TextColumn get downloadMusicCodec => - textEnum().withDefault(Constant(SourceCodecs.m4a.name))(); BoolColumn get discordPresence => boolean().withDefault(const Constant(true))(); BoolColumn get endlessPlayback => @@ -116,7 +98,6 @@ class PreferencesTable extends Table { static PreferencesTableData defaults() { return PreferencesTableData( id: 0, - audioQuality: SourceQualities.high, albumColorSync: true, amoledDarkTheme: false, checkUpdate: true, @@ -135,10 +116,8 @@ class PreferencesTable extends Table { pipedInstance: "https://pipedapi.kavin.rocks", invidiousInstance: "https://inv.nadeko.net", themeMode: ThemeMode.system, - audioSource: AudioSource.youtube, + audioSourceId: null, youtubeClientEngine: YoutubeClientEngine.youtubeExplode, - streamMusicCodec: SourceCodecs.m4a, - downloadMusicCodec: SourceCodecs.m4a, discordPresence: true, endlessPlayback: true, enableConnect: false, diff --git a/lib/models/database/tables/source_match.dart b/lib/models/database/tables/source_match.dart index fa659287..b5661137 100644 --- a/lib/models/database/tables/source_match.dart +++ b/lib/models/database/tables/source_match.dart @@ -1,26 +1,14 @@ part of '../database.dart'; -enum SourceType { - youtube._("YouTube"), - youtubeMusic._("YouTube Music"), - jiosaavn._("JioSaavn"), - dabMusic._("DAB Music"); - - final String label; - - const SourceType._(this.label); -} - @TableIndex( name: "uniq_track_match", - columns: {#trackId, #sourceId, #sourceType}, + columns: {#trackId, #sourceInfo, #sourceType}, unique: true, ) class SourceMatchTable extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get trackId => text()(); - TextColumn get sourceId => text()(); - TextColumn get sourceType => - textEnum().withDefault(Constant(SourceType.youtube.name))(); + TextColumn get sourceInfo => text()(); + TextColumn get sourceType => text()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); } diff --git a/lib/models/playback/track_sources.dart b/lib/models/playback/track_sources.dart index 1666609c..262fcefa 100644 --- a/lib/models/playback/track_sources.dart +++ b/lib/models/playback/track_sources.dart @@ -1,122 +1,16 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/services/audio_player/audio_player.dart'; -import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; part 'track_sources.freezed.dart'; part 'track_sources.g.dart'; -@freezed -class TrackSourceQuery with _$TrackSourceQuery { - TrackSourceQuery._(); - - factory TrackSourceQuery({ - required String id, - required String title, - required List artists, - required String album, - required int durationMs, - required String isrc, - required bool explicit, - }) = _TrackSourceQuery; - - factory TrackSourceQuery.fromJson(Map json) => - _$TrackSourceQueryFromJson(json); - - factory TrackSourceQuery.fromTrack(SpotubeFullTrackObject track) { - return TrackSourceQuery( - id: track.id, - title: track.name, - artists: track.artists.map((e) => e.name).toList(), - album: track.album.name, - durationMs: track.durationMs, - isrc: track.isrc, - explicit: track.explicit, - ); - } - - /// Parses [SpotubeMedia]'s [uri] property to create a [TrackSourceQuery]. - factory TrackSourceQuery.parseUri(String url) { - final isLocal = !url.startsWith("http"); - - if (isLocal) { - try { - return TrackSourceQuery( - id: url, - title: '', - artists: [], - album: '', - durationMs: 0, - isrc: '', - explicit: false, - ); - } catch (e, stackTrace) { - AppLogger.log.e( - "Failed to parse local track URI: $url\n$e", - stackTrace: stackTrace, - ); - } - } - - final uri = Uri.parse(url); - return TrackSourceQuery( - id: uri.pathSegments.last, - title: uri.queryParameters['title'] ?? '', - artists: uri.queryParameters['artists']?.split(',') ?? [], - album: uri.queryParameters['album'] ?? '', - durationMs: int.tryParse(uri.queryParameters['durationMs'] ?? '0') ?? 0, - isrc: uri.queryParameters['isrc'] ?? '', - explicit: uri.queryParameters['explicit']?.toLowerCase() == 'true', - ); - } - - String queryString() { - return toJson() - .entries - .map((e) => - "${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value is List ? e.value.join(",") : e.value.toString())}") - .join("&"); - } -} - -@freezed -class TrackSourceInfo with _$TrackSourceInfo { - factory TrackSourceInfo({ - required String id, - required String title, - required String artists, - required String thumbnail, - required String pageUrl, - required int durationMs, - }) = _TrackSourceInfo; - - factory TrackSourceInfo.fromJson(Map json) => - _$TrackSourceInfoFromJson(json); -} - -@freezed -class TrackSource with _$TrackSource { - factory TrackSource({ - required String url, - required SourceQualities quality, - required SourceCodecs codec, - required String bitrate, - required String qualityLabel, - }) = _TrackSource; - - factory TrackSource.fromJson(Map json) => - _$TrackSourceFromJson(json); -} - @JsonSerializable() class BasicSourcedTrack { - final TrackSourceQuery query; - final AudioSource source; - final TrackSourceInfo info; - final List sources; - final List siblings; + final SpotubeFullTrackObject query; + final SpotubeAudioSourceMatchObject info; + final String source; + final List sources; + final List siblings; BasicSourcedTrack({ required this.query, required this.source, diff --git a/lib/modules/library/local_folder/cache_export_dialog.dart b/lib/modules/library/local_folder/cache_export_dialog.dart index 0f10defc..fde219c9 100644 --- a/lib/modules/library/local_folder/cache_export_dialog.dart +++ b/lib/modules/library/local_folder/cache_export_dialog.dart @@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path/path.dart' as path; import 'package:spotube/extensions/context.dart'; import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; final codecs = SourceCodecs.values.map((s) => s.name); diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 4250e153..69262641 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -49,7 +49,7 @@ class PlayerView extends HookConsumerWidget { final activeSourceCodec = useMemoized( () { return currentActiveTrackSource - ?.getSourceOfCodec(currentActiveTrackSource.codec); + ?.getStreamOfCodec(currentActiveTrackSource.codec); }, [currentActiveTrackSource?.sources, currentActiveTrackSource?.codec], ); diff --git a/lib/modules/root/sidebar/sidebar.dart b/lib/modules/root/sidebar/sidebar.dart index e4e7db3d..1538d624 100644 --- a/lib/modules/root/sidebar/sidebar.dart +++ b/lib/modules/root/sidebar/sidebar.dart @@ -22,7 +22,7 @@ class Sidebar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ThemeData(:colorScheme) = Theme.of(context); - final mediaQuery = MediaQuery.of(context); + final mediaQuery = MediaQuery.sizeOf(context); final layoutMode = ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 6d0b5dc3..77eaa0c5 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -23,7 +23,6 @@ import 'package:spotube/provider/audio_player/sources/piped_instances_provider.d import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/utils/platform.dart'; diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index d7f28b67..d0112765 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -12,7 +12,6 @@ import 'package:metadata_god/metadata_god.dart'; import 'package:path/path.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/download_manager/download_manager.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/service_utils.dart'; diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index 4bce7444..7155edca 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -20,7 +20,6 @@ import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 8e72727c..9bc64f4f 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -10,7 +10,6 @@ import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; import 'package:open_file/open_file.dart'; diff --git a/lib/services/sourced_track/enums.dart b/lib/services/sourced_track/enums.dart deleted file mode 100644 index 9a1a5040..00000000 --- a/lib/services/sourced_track/enums.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:spotube/models/playback/track_sources.dart'; - -enum SourceCodecs { - m4a._("M4a (Best for downloaded music)"), - weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"), - mp3._("MP3 (Widely supported audio format)"), - flac._("FLAC (Lossless, best quality)\nLarge file size"); - - final String label; - const SourceCodecs._(this.label); -} - -enum SourceQualities { - uncompressed(3), - high(2), - medium(1), - low(0); - - final int priority; - const SourceQualities(this.priority); - - bool operator <(SourceQualities other) { - return priority < other.priority; - } - - operator >(SourceQualities other) { - return priority > other.priority; - } -} - -typedef SiblingType = ({ - T info, - List? source -}); diff --git a/lib/services/sourced_track/models/video_info.dart b/lib/services/sourced_track/models/video_info.dart deleted file mode 100644 index e3452c61..00000000 --- a/lib/services/sourced_track/models/video_info.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:invidious/invidious.dart'; -import 'package:piped_client/piped_client.dart'; -import 'package:spotube/models/database/database.dart'; - -import 'package:youtube_explode_dart/youtube_explode_dart.dart'; - -class YoutubeVideoInfo { - final SearchMode searchMode; - final String title; - final Duration duration; - final String thumbnailUrl; - final String id; - final int likes; - final int dislikes; - final int views; - final String channelName; - final String channelId; - final DateTime publishedAt; - - YoutubeVideoInfo({ - required this.searchMode, - required this.title, - required this.duration, - required this.thumbnailUrl, - required this.id, - required this.likes, - required this.dislikes, - required this.views, - required this.channelName, - required this.publishedAt, - required this.channelId, - }); - - YoutubeVideoInfo.fromJson(Map json) - : title = json['title'], - searchMode = SearchMode.fromString(json['searchMode']), - duration = Duration(seconds: json['duration']), - thumbnailUrl = json['thumbnailUrl'], - id = json['id'], - likes = json['likes'], - dislikes = json['dislikes'], - views = json['views'], - channelName = json['channelName'], - channelId = json['channelId'], - publishedAt = DateTime.tryParse(json['publishedAt']) ?? DateTime.now(); - - Map toJson() => { - 'title': title, - 'duration': duration.inSeconds, - 'thumbnailUrl': thumbnailUrl, - 'id': id, - 'likes': likes, - 'dislikes': dislikes, - 'views': views, - 'channelName': channelName, - 'channelId': channelId, - 'publishedAt': publishedAt.toIso8601String(), - 'searchMode': searchMode.name, - }; - - factory YoutubeVideoInfo.fromVideo(Video video) { - return YoutubeVideoInfo( - searchMode: SearchMode.youtube, - title: video.title, - duration: video.duration ?? Duration.zero, - thumbnailUrl: video.thumbnails.mediumResUrl, - id: video.id.value, - likes: video.engagement.likeCount ?? 0, - dislikes: video.engagement.dislikeCount ?? 0, - views: video.engagement.viewCount, - channelName: video.author, - channelId: '/c/${video.channelId.value}', - publishedAt: video.uploadDate ?? DateTime(2003, 9, 9), - ); - } - - factory YoutubeVideoInfo.fromSearchItemStream( - PipedSearchItemStream searchItem, - SearchMode searchMode, - ) { - return YoutubeVideoInfo( - searchMode: searchMode, - title: searchItem.title, - duration: searchItem.duration, - thumbnailUrl: searchItem.thumbnail, - id: searchItem.id, - likes: 0, - dislikes: 0, - views: searchItem.views, - channelName: searchItem.uploaderName, - channelId: searchItem.uploaderUrl ?? "", - publishedAt: searchItem.uploadedDate != null - ? DateTime.tryParse(searchItem.uploadedDate!) ?? DateTime(2003, 9, 9) - : DateTime(2003, 9, 9), - ); - } - - factory YoutubeVideoInfo.fromStreamResponse( - PipedStreamResponse stream, SearchMode searchMode) { - return YoutubeVideoInfo( - searchMode: searchMode, - title: stream.title, - duration: stream.duration, - thumbnailUrl: stream.thumbnailUrl, - id: stream.id, - likes: stream.likes, - dislikes: stream.dislikes, - views: stream.views, - channelName: stream.uploader, - publishedAt: stream.uploadedDate != null - ? DateTime.tryParse(stream.uploadedDate!) ?? DateTime(2003, 9, 9) - : DateTime(2003, 9, 9), - channelId: stream.uploaderUrl, - ); - } - - factory YoutubeVideoInfo.fromSearchResponse( - InvidiousSearchResponseVideo searchResponse, - SearchMode searchMode, - ) { - return YoutubeVideoInfo( - searchMode: searchMode, - title: searchResponse.title, - duration: Duration(seconds: searchResponse.lengthSeconds), - thumbnailUrl: searchResponse.videoThumbnails.first.url, - id: searchResponse.videoId, - likes: 0, - dislikes: 0, - views: searchResponse.viewCount, - channelName: searchResponse.author, - channelId: searchResponse.authorId, - publishedAt: - DateTime.fromMillisecondsSinceEpoch(searchResponse.published * 1000), - ); - } -} diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index a5b2ae93..661a8447 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -1,18 +1,28 @@ -import 'package:collection/collection.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'dart:convert'; -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/sources/dab_music.dart'; -import 'package:spotube/services/sourced_track/sources/invidious.dart'; -import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; -import 'package:spotube/services/sourced_track/sources/piped.dart'; -import 'package:spotube/services/sourced_track/sources/youtube.dart'; +import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; +import 'package:drift/drift.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotube/extensions/string.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/models/playback/track_sources.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/dio/dio.dart'; +import 'package:spotube/services/logger/logger.dart'; + +import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/utils/service_utils.dart'; -abstract class SourcedTrack extends BasicSourcedTrack { +final officialMusicRegex = RegExp( + r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)", + caseSensitive: false, +); + +class SourcedTrack extends BasicSourcedTrack { final Ref ref; SourcedTrack({ @@ -24,72 +34,10 @@ abstract class SourcedTrack extends BasicSourcedTrack { required super.sources, }); - static SourcedTrack fromJson( - Map json, { - required Ref ref, - }) { - final preferences = ref.read(userPreferencesProvider); - - final info = TrackSourceInfo.fromJson(json["info"]); - final query = TrackSourceQuery.fromJson(json["query"]); - final source = AudioSource.values.firstWhereOrNull( - (source) => source.name == json["source"], - ) ?? - preferences.audioSource; - final siblings = (json["siblings"] as List) - .map((s) => TrackSourceInfo.fromJson(s)) - .toList(); - final sources = - (json["sources"] as List).map((s) => TrackSource.fromJson(s)).toList(); - - return switch (preferences.audioSource) { - AudioSource.youtube => YoutubeSourcedTrack( - ref: ref, - source: source, - siblings: siblings, - info: info, - query: query, - sources: sources, - ), - AudioSource.piped => PipedSourcedTrack( - ref: ref, - source: source, - siblings: siblings, - info: info, - query: query, - sources: sources, - ), - AudioSource.jiosaavn => JioSaavnSourcedTrack( - ref: ref, - source: source, - siblings: siblings, - info: info, - query: query, - sources: sources, - ), - AudioSource.invidious => InvidiousSourcedTrack( - ref: ref, - source: source, - siblings: siblings, - info: info, - query: query, - sources: sources, - ), - AudioSource.dabMusic => DABMusicSourcedTrack( - ref: ref, - source: source, - siblings: siblings, - info: info, - query: query, - sources: sources, - ), - }; - } - - static String getSearchTerm(TrackSourceQuery track) { + static String getSearchTerm(SpotubeFullTrackObject track) { final title = ServiceUtils.getTitle( - track.title, - artists: track.artists, + track.name, + artists: track.artists.map((e) => e.name).toList(), onlyCleanArtist: true, ).trim(); @@ -99,61 +47,256 @@ abstract class SourcedTrack extends BasicSourcedTrack { } static Future fetchFromQuery({ - required TrackSourceQuery query, + required SpotubeFullTrackObject query, required Ref ref, }) async { - final preferences = ref.read(userPreferencesProvider); - try { - return switch (preferences.audioSource) { - AudioSource.youtube => - await YoutubeSourcedTrack.fetchFromTrack(query: query, ref: ref), - AudioSource.piped => - await PipedSourcedTrack.fetchFromTrack(query: query, ref: ref), - AudioSource.invidious => - await InvidiousSourcedTrack.fetchFromTrack(query: query, ref: ref), - AudioSource.jiosaavn => - await JioSaavnSourcedTrack.fetchFromTrack(query: query, ref: ref), - AudioSource.dabMusic => - await DABMusicSourcedTrack.fetchFromTrack(query: query, ref: ref), - }; - } catch (e) { - if (preferences.audioSource == AudioSource.youtube) { - rethrow; + final audioSource = await ref.read(audioSourcePluginProvider.future); + final audioSourceConfig = await ref.read(metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig)); + if (audioSource == null || audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + + final database = ref.read(databaseProvider); + final cachedSource = await (database.select(database.sourceMatchTable) + ..where((s) => + s.trackId.equals(query.id) & + s.sourceType.equals(audioSourceConfig.slug)) + ..limit(1) + ..orderBy([ + (s) => + OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), + ])) + .get() + .then((s) => s.firstOrNull); + + if (cachedSource == null) { + final siblings = await fetchSiblings(ref: ref, query: query); + if (siblings.isEmpty) { + throw TrackNotFoundError(query); } - return await YoutubeSourcedTrack.fetchFromTrack(query: query, ref: ref); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: query.id, + source: jsonEncode(siblings.first), + sourceType: Value(audioSourceConfig.slug), + ), + ); + + return SourcedTrack( + ref: ref, + siblings: siblings.map((s) => s.info).skip(1).toList(), + info: siblings.first.info, + source: audioSourceConfig.slug, + sources: siblings.first.source ?? [], + query: query, + ); } + final item = + SpotubeAudioSourceMatchObject.fromJson(jsonDecode(cachedSource.source)); + final manifest = await audioSource.audioSource.streams(item); + + final sourcedTrack = SourcedTrack( + ref: ref, + siblings: [], + sources: manifest, + info: item, + query: query, + source: audioSourceConfig.slug, + ); + + AppLogger.log.i("${query.name}: ${sourcedTrack.url}"); + + return sourcedTrack; } - static Future> fetchSiblings({ - required TrackSourceQuery query, + static List rankResults( + List results, + SpotubeFullTrackObject track, + ) { + return results + .map((sibling) { + int score = 0; + + for (final artist in track.artists) { + final isSameChannelArtist = + sibling.artists.any((a) => a.toLowerCase() == artist.name); + + if (isSameChannelArtist) { + score += 1; + } + + final titleContainsArtist = + sibling.title.toLowerCase().contains(artist.name.toLowerCase()); + + if (titleContainsArtist) { + score += 1; + } + } + + final titleContainsTrackName = + sibling.title.toLowerCase().contains(track.name.toLowerCase()); + + final hasOfficialFlag = + officialMusicRegex.hasMatch(sibling.title.toLowerCase()); + + if (titleContainsTrackName) { + score += 3; + } + + if (hasOfficialFlag) { + score += 1; + } + + if (hasOfficialFlag && titleContainsTrackName) { + score += 2; + } + + return (sibling: sibling, score: score); + }) + .sorted((a, b) => b.score.compareTo(a.score)) + .map((e) => e.sibling) + .toList(); + } + + static Future> fetchSiblings({ + required SpotubeFullTrackObject query, required Ref ref, - }) { - final preferences = ref.read(userPreferencesProvider); + }) async { + final audioSource = await ref.read(audioSourcePluginProvider.future); - return switch (preferences.audioSource) { - AudioSource.piped => - PipedSourcedTrack.fetchSiblings(query: query, ref: ref), - AudioSource.youtube => - YoutubeSourcedTrack.fetchSiblings(query: query, ref: ref), - AudioSource.jiosaavn => - JioSaavnSourcedTrack.fetchSiblings(query: query, ref: ref), - AudioSource.invidious => - InvidiousSourcedTrack.fetchSiblings(query: query, ref: ref), - AudioSource.dabMusic => - DABMusicSourcedTrack.fetchSiblings(query: query, ref: ref), - }; + if (audioSource == null) { + throw Exception("Dude wat?"); + } + + final videoResults = []; + + final searchResults = await audioSource.audioSource.matches(query); + + if (ServiceUtils.onlyContainsEnglish(query.name)) { + videoResults.addAll(searchResults); + } else { + videoResults.addAll(rankResults(searchResults, query)); + } + + return videoResults.toSet().toList(); } - Future copyWithSibling(); + Future copyWithSibling() async { + if (siblings.isNotEmpty) { + return this; + } + final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - Future swapWithSibling(TrackSourceInfo sibling); + return SourcedTrack( + ref: ref, + siblings: fetchedSiblings.where((s) => s.id != info.id).toList(), + source: source, + sources: sources, + info: info, + query: query, + ); + } + + Future swapWithSibling( + SpotubeAudioSourceMatchObject sibling) async { + if (sibling.id == info.id) { + return null; + } + + final audioSource = await ref.read(audioSourcePluginProvider.future); + final audioSourceConfig = await ref.read(metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig)); + if (audioSource == null || audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + + // a sibling source that was fetched from the search results + final isStepSibling = siblings.none((s) => s.id == sibling.id); + + final newSourceInfo = isStepSibling + ? sibling + : siblings.firstWhere((s) => s.id == sibling.id); + + final newSiblings = siblings.where((s) => s.id != sibling.id).toList() + ..insert(0, info); + + final manifest = await audioSource.audioSource.streams(newSourceInfo); + + final database = ref.read(databaseProvider); + + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: query.id, + source: jsonEncode(siblings.first), + sourceType: Value(audioSourceConfig.slug), + createdAt: Value(DateTime.now()), + ), + mode: InsertMode.replace, + ); + + return SourcedTrack( + ref: ref, + source: source, + siblings: newSiblings, + sources: manifest, + info: newSourceInfo, + query: query, + ); + } Future swapWithSiblingOfIndex(int index) { return swapWithSibling(siblings[index]); } - Future refreshStream(); + Future refreshStream() async { + final audioSource = await ref.read(audioSourcePluginProvider.future); + final audioSourceConfig = await ref.read(metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig)); + if (audioSource == null || audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + + List validStreams = []; + + final stringBuffer = StringBuffer(); + for (final source in sources) { + final res = await globalDio.head( + source.url, + options: + Options(validateStatus: (status) => status != null && status < 500), + ); + + stringBuffer.writeln( + "[${query.id}] ${res.statusCode} ${source.container} ${source.codec} ${source.bitrate}", + ); + + if (res.statusCode! < 400) { + validStreams.add(source); + } + } + + AppLogger.log.d(stringBuffer.toString()); + + if (validStreams.isEmpty) { + validStreams = await audioSource.audioSource.streams(info); + } + + final sourcedTrack = SourcedTrack( + ref: ref, + siblings: siblings, + source: source, + sources: validStreams, + info: info, + query: query, + ); + + AppLogger.log.i("Refreshing ${query.name}: ${sourcedTrack.url}"); + + return sourcedTrack; + } + String? get url { final preferences = ref.read(userPreferencesProvider); @@ -170,58 +313,75 @@ abstract class SourcedTrack extends BasicSourcedTrack { /// /// If no sources match the codec, it will return the first or last source /// based on the user's audio quality preference. - TrackSource? getSourceOfCodec(SourceCodecs codec) { - final preferences = ref.read(userPreferencesProvider); + SpotubeAudioSourceStreamObject? getStreamOfQuality( + SpotubeAudioSourceContainerPreset preset, + int qualityIndex, + ) { + final quality = preset.qualities[qualityIndex]; final exactMatch = sources.firstWhereOrNull( - (source) => - source.codec == codec && source.quality == preferences.audioQuality, + (source) { + if (source.container != preset.name) return false; + + if (quality case SpotubeAudioLosslessContainerQuality()) { + return source.sampleRate == quality.sampleRate && + source.bitDepth == quality.bitDepth; + } else { + return source.bitrate == + (preset as SpotubeAudioLossyContainerQuality).bitrate; + } + }, ); if (exactMatch != null) { return exactMatch; } - final sameCodecSources = sources - .where((source) => source.codec == codec) - .toList() - .sorted((a, b) { - final aDiff = (a.quality.index - preferences.audioQuality.index).abs(); - final bDiff = (b.quality.index - preferences.audioQuality.index).abs(); - return aDiff != bDiff ? aDiff - bDiff : a.quality.index - b.quality.index; - }).toList(); + // Find the closest to preset + SpotubeAudioSourceStreamObject? closest; + for (final source in sources) { + if (source.container != preset.name) continue; - if (sameCodecSources.isNotEmpty) { - return preferences.audioQuality > SourceQualities.low - ? sameCodecSources.first - : sameCodecSources.last; + if (quality case SpotubeAudioLosslessContainerQuality()) { + final sourceBps = (source.bitDepth ?? 0) * (source.sampleRate ?? 0); + final qualityBps = quality.bitDepth * quality.sampleRate; + final closestBps = + (closest?.bitDepth ?? 0) * (closest?.sampleRate ?? 0); + + if (sourceBps == qualityBps) { + closest = source; + break; + } + final closestDiff = (closestBps - qualityBps).abs(); + final sourceDiff = (sourceBps - qualityBps).abs(); + + if (sourceDiff < closestDiff) { + closest = source; + } + } else { + final presetBitrate = + (preset as SpotubeAudioLossyContainerQuality).bitrate; + if (presetBitrate == source.bitrate) { + closest = source; + break; + } + + final closestDiff = (closest?.bitrate ?? 0) - presetBitrate; + final sourceDiff = (source.bitrate ?? 0) - presetBitrate; + + if (sourceDiff < closestDiff) { + closest = source; + } + } } - final fallbackSource = sources.sorted((a, b) { - final aDiff = (a.quality.index - preferences.audioQuality.index).abs(); - final bDiff = (b.quality.index - preferences.audioQuality.index).abs(); - return aDiff != bDiff ? aDiff - bDiff : a.quality.index - b.quality.index; - }); - - return preferences.audioQuality > SourceQualities.low - ? fallbackSource.firstOrNull - : fallbackSource.lastOrNull; + return closest; } - String? getUrlOfCodec(SourceCodecs codec) { - return getSourceOfCodec(codec)?.url; - } - - SourceCodecs get codec { - final preferences = ref.read(userPreferencesProvider); - - return switch (preferences.audioSource) { - AudioSource.dabMusic => - preferences.audioQuality == SourceQualities.uncompressed - ? SourceCodecs.flac - : SourceCodecs.mp3, - AudioSource.jiosaavn => SourceCodecs.m4a, - _ => preferences.streamMusicCodec - }; + String? getUrlOfQuality( + SpotubeAudioSourceContainerPreset preset, + int qualityIndex, + ) { + return getStreamOfQuality(preset, qualityIndex)?.url; } } diff --git a/lib/services/sourced_track/sources/dab_music.dart b/lib/services/sourced_track/sources/dab_music.dart deleted file mode 100644 index 83cc55b4..00000000 --- a/lib/services/sourced_track/sources/dab_music.dart +++ /dev/null @@ -1,303 +0,0 @@ -import 'dart:convert'; - -import 'package:collection/collection.dart'; -import 'package:dio/dio.dart'; -import 'package:drift/drift.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/exceptions.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:dab_music_api/dab_music_api.dart'; - -final dabMusicApiClient = DabMusicApiClient( - Dio(), - baseUrl: "https://dab.yeet.su/api", -); - -/// Only Music source that can't support database caching due to having no endpoint. -/// But ISRC search is 100% reliable so caching is actually not necessary. -class DABMusicSourcedTrack extends SourcedTrack { - DABMusicSourcedTrack({ - required super.ref, - required super.source, - required super.siblings, - required super.info, - required super.query, - required super.sources, - }); - - static Future fetchFromTrack({ - required TrackSourceQuery query, - required Ref ref, - }) async { - try { - final database = ref.read(databaseProvider); - final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(query.id)) - ..limit(1) - ..orderBy([ - (s) => OrderingTerm( - expression: s.createdAt, - mode: OrderingMode.desc, - ), - ])) - .get() - .then((s) => s.firstOrNull); - - if (cachedSource != null && - cachedSource.sourceType == SourceType.dabMusic) { - final json = jsonDecode(cachedSource.sourceId); - final info = TrackSourceInfo.fromJson(json["info"]); - final source = (json["sources"] as List?) - ?.map((s) => TrackSource.fromJson(s)) - .toList(); - - final [updatedSource] = await fetchSources( - info.id, - ref.read(userPreferencesProvider).audioQuality, - const AudioQuality( - isHiRes: true, - maximumBitDepth: 16, - maximumSamplingRate: 44.1, - ), - ); - - return DABMusicSourcedTrack( - ref: ref, - source: AudioSource.dabMusic, - siblings: [], - info: info, - query: query, - sources: [ - source!.first.copyWith(url: updatedSource.url), - ], - ); - } - - final siblings = await fetchSiblings(ref: ref, query: query); - - if (siblings.isEmpty) { - throw TrackNotFoundError(query); - } - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: jsonEncode({ - "info": siblings.first.info.toJson(), - "sources": (siblings.first.source ?? []) - .map((s) => s.toJson()) - .toList(), - }), - sourceType: const Value(SourceType.dabMusic), - ), - ); - - return DABMusicSourcedTrack( - ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - sources: siblings.first.source!, - info: siblings.first.info, - query: query, - source: AudioSource.dabMusic, - ); - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - rethrow; - } - } - - static Future> fetchSources( - String id, - SourceQualities quality, - AudioQuality trackMaximumQuality, - ) async { - try { - final isUncompressed = quality == SourceQualities.uncompressed; - final streamResponse = await dabMusicApiClient.music.getStream( - trackId: id, - quality: isUncompressed ? "27" : "5", - ); - if (streamResponse.url == null) { - throw Exception("No stream URL found for track ID: $id"); - } - - // kbps = (bitDepth * sampleRate * channels) / 1000 - final uncompressedBitrate = !isUncompressed - ? 0 - : ((trackMaximumQuality.maximumBitDepth ?? 0) * - ((trackMaximumQuality.maximumSamplingRate ?? 0) * 1000) * - 2) / - 1000; - return [ - TrackSource( - url: streamResponse.url!, - quality: isUncompressed - ? SourceQualities.uncompressed - : SourceQualities.high, - bitrate: - isUncompressed ? "${uncompressedBitrate.floor()}kbps" : "320kbps", - codec: isUncompressed ? SourceCodecs.flac : SourceCodecs.mp3, - qualityLabel: isUncompressed - ? "${trackMaximumQuality.maximumBitDepth}bit • ${trackMaximumQuality.maximumSamplingRate}kHz • FLAC • Stereo" - : "MP3 • 320kbps • mp3 • Stereo", - ), - ]; - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - rethrow; - } - } - - static Future toSiblingType( - Ref ref, - int index, - Track result, - ) async { - try { - List? source; - if (index == 0) { - source = await fetchSources( - result.id.toString(), - ref.read(userPreferencesProvider).audioQuality, - result.audioQuality!, - ); - } - - final SiblingType sibling = ( - info: TrackSourceInfo( - artists: result.artist!, - durationMs: Duration(seconds: result.duration!).inMilliseconds, - id: result.id.toString(), - pageUrl: "https://dab.yeet.su/music/${result.id}", - thumbnail: result.albumCover!, - title: result.title!, - ), - source: source, - ); - - return sibling; - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - rethrow; - } - } - - static Future> fetchSiblings({ - required TrackSourceQuery query, - required Ref ref, - }) async { - try { - List results = []; - - if (query.isrc.isNotEmpty) { - final res = - await dabMusicApiClient.music.getSearch(q: query.isrc, limit: 1); - results = res.tracks ?? []; - } - - if (results.isEmpty) { - final res = await dabMusicApiClient.music.getSearch( - q: SourcedTrack.getSearchTerm(query), - limit: 5, - ); - results = res.tracks ?? []; - } - - if (results.isEmpty) { - return []; - } - - final matchedResults = - results.mapIndexed((index, d) => toSiblingType(ref, index, d)); - - return Future.wait(matchedResults); - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - rethrow; - } - } - - @override - Future copyWithSibling() async { - if (siblings.isNotEmpty) { - return this; - } - final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - - return DABMusicSourcedTrack( - ref: ref, - siblings: fetchedSiblings - .where((s) => s.info.id != info.id) - .map((s) => s.info) - .toList(), - source: source, - info: info, - query: query, - sources: sources, - ); - } - - @override - Future swapWithSibling(TrackSourceInfo sibling) async { - if (sibling.id == this.info.id) { - return null; - } - - // a sibling source that was fetched from the search results - final isStepSibling = siblings.none((s) => s.id == sibling.id); - - final newSourceInfo = isStepSibling - ? sibling - : siblings.firstWhere((s) => s.id == sibling.id); - final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, this.info); - - final source = await fetchSources( - sibling.id, - ref.read(userPreferencesProvider).audioQuality, - const AudioQuality( - isHiRes: true, - maximumBitDepth: 16, - maximumSamplingRate: 44.1, - ), - ); - - final database = ref.read(databaseProvider); - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: jsonEncode({ - "info": newSourceInfo.toJson(), - "sources": source.map((s) => s.toJson()).toList(), - }), - sourceType: const Value(SourceType.dabMusic), - // Because we're sorting by createdAt in the query - // we have to update it to indicate priority - createdAt: Value(DateTime.now()), - ), - mode: InsertMode.replace, - ); - - return DABMusicSourcedTrack( - ref: ref, - siblings: newSiblings, - sources: source, - info: newSourceInfo, - query: query, - source: AudioSource.dabMusic, - ); - } - - @override - Future refreshStream() async { - // There's no need to refresh the stream for DABMusicSourcedTrack - return this; - } -} diff --git a/lib/services/sourced_track/sources/invidious.dart b/lib/services/sourced_track/sources/invidious.dart deleted file mode 100644 index c5421355..00000000 --- a/lib/services/sourced_track/sources/invidious.dart +++ /dev/null @@ -1,263 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/exceptions.dart'; -import 'package:spotube/services/sourced_track/models/video_info.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:invidious/invidious.dart'; -import 'package:spotube/services/sourced_track/sources/youtube.dart'; -import 'package:spotube/utils/service_utils.dart'; - -final invidiousProvider = Provider( - (ref) { - final invidiousInstance = ref.watch( - userPreferencesProvider.select((s) => s.invidiousInstance), - ); - return InvidiousClient(server: invidiousInstance); - }, -); - -class InvidiousSourcedTrack extends SourcedTrack { - InvidiousSourcedTrack({ - required super.ref, - required super.source, - required super.siblings, - required super.info, - required super.query, - required super.sources, - }); - - static Future fetchFromTrack({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final audioSource = ref.read(userPreferencesProvider).audioSource; - final database = ref.read(databaseProvider); - final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(query.id)) - ..limit(1) - ..orderBy([ - (s) => - OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), - ])) - .getSingleOrNull(); - final invidiousClient = ref.read(invidiousProvider); - - if (cachedSource == null) { - final siblings = await fetchSiblings(ref: ref, query: query); - if (siblings.isEmpty) { - throw TrackNotFoundError(query); - } - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: siblings.first.info.id, - sourceType: const Value(SourceType.youtube), - ), - ); - - return InvidiousSourcedTrack( - ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - sources: siblings.first.source as List, - info: siblings.first.info, - query: query, - source: audioSource, - ); - } else { - final manifest = - await invidiousClient.videos.get(cachedSource.sourceId, local: true); - - return InvidiousSourcedTrack( - ref: ref, - siblings: [], - sources: toSources(manifest), - info: TrackSourceInfo( - id: manifest.videoId, - artists: manifest.author, - pageUrl: "https://www.youtube.com/watch?v=${manifest.videoId}", - thumbnail: manifest.videoThumbnails.first.url, - title: manifest.title, - durationMs: Duration(seconds: manifest.lengthSeconds).inMilliseconds, - ), - query: query, - source: audioSource, - ); - } - } - - static List toSources(InvidiousVideoResponse manifest) { - return manifest.adaptiveFormats.map((stream) { - var isWebm = stream.type.contains("audio/webm"); - return TrackSource( - url: stream.url.toString(), - quality: switch (stream.qualityLabel) { - "high" => SourceQualities.high, - "medium" => SourceQualities.medium, - _ => SourceQualities.low, - }, - codec: isWebm ? SourceCodecs.weba : SourceCodecs.m4a, - bitrate: stream.bitrate, - qualityLabel: - "${isWebm ? "Opus" : "AAC"} • ${stream.bitrate.replaceAll("kbps", "")}kbps " - "• ${isWebm ? "weba" : "m4a"} • Stereo", - ); - }).toList(); - } - - static Future toSiblingType( - int index, - YoutubeVideoInfo item, - InvidiousClient invidiousClient, - ) async { - List? sourceMap; - if (index == 0) { - final manifest = await invidiousClient.videos.get(item.id, local: true); - sourceMap = toSources(manifest); - } - - final SiblingType sibling = ( - info: TrackSourceInfo( - id: item.id, - artists: item.channelName, - pageUrl: "https://www.youtube.com/watch?v=${item.id}", - thumbnail: item.thumbnailUrl, - title: item.title, - durationMs: item.duration.inMilliseconds, - ), - source: sourceMap, - ); - - return sibling; - } - - static Future> fetchSiblings({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final invidiousClient = ref.read(invidiousProvider); - final preference = ref.read(userPreferencesProvider); - - final searchQuery = SourcedTrack.getSearchTerm(query); - - final searchResults = await invidiousClient.search.list( - searchQuery, - type: InvidiousSearchType.video, - ); - - if (ServiceUtils.onlyContainsEnglish(searchQuery)) { - return await Future.wait( - searchResults - .whereType() - .map( - (result) => YoutubeVideoInfo.fromSearchResponse( - result, - preference.searchMode, - ), - ) - .mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)), - ); - } - - final rankedSiblings = YoutubeSourcedTrack.rankResults( - searchResults - .whereType() - .map( - (result) => YoutubeVideoInfo.fromSearchResponse( - result, - preference.searchMode, - ), - ) - .toList(), - query, - ); - - return await Future.wait( - rankedSiblings.mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)), - ); - } - - @override - Future copyWithSibling() async { - if (siblings.isNotEmpty) { - return this; - } - final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - - return InvidiousSourcedTrack( - ref: ref, - siblings: fetchedSiblings - .where((s) => s.info.id != info.id) - .map((s) => s.info) - .toList(), - source: source, - info: info, - query: query, - sources: sources, - ); - } - - @override - Future swapWithSibling(TrackSourceInfo sibling) async { - if (sibling.id == info.id) { - return null; - } - - // a sibling source that was fetched from the search results - final isStepSibling = siblings.none((s) => s.id == sibling.id); - - final newSourceInfo = isStepSibling - ? sibling - : siblings.firstWhere((s) => s.id == sibling.id); - final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, info); - - final pipedClient = ref.read(invidiousProvider); - - final manifest = - await pipedClient.videos.get(newSourceInfo.id, local: true); - - final database = ref.read(databaseProvider); - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: newSourceInfo.id, - sourceType: const Value(SourceType.youtube), - // Because we're sorting by createdAt in the query - // we have to update it to indicate priority - createdAt: Value(DateTime.now()), - ), - mode: InsertMode.replace, - ); - - return InvidiousSourcedTrack( - ref: ref, - siblings: newSiblings, - sources: toSources(manifest), - info: newSourceInfo, - query: query, - source: source, - ); - } - - @override - Future refreshStream() async { - final manifest = - await ref.read(invidiousProvider).videos.get(info.id, local: true); - - return InvidiousSourcedTrack( - ref: ref, - siblings: siblings, - sources: toSources(manifest), - info: info, - query: query, - source: source, - ); - } -} diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart deleted file mode 100644 index be78be25..00000000 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ /dev/null @@ -1,231 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/exceptions.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:jiosaavn/jiosaavn.dart'; -import 'package:spotube/extensions/string.dart'; - -final jiosaavnClient = JioSaavnClient(); - -class JioSaavnSourcedTrack extends SourcedTrack { - JioSaavnSourcedTrack({ - required super.ref, - required super.source, - required super.siblings, - required super.info, - required super.query, - required super.sources, - }); - - static Future fetchFromTrack({ - required TrackSourceQuery query, - required Ref ref, - bool weakMatch = false, - }) async { - final database = ref.read(databaseProvider); - final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(query.id)) - ..limit(1) - ..orderBy([ - (s) => - OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), - ])) - .getSingleOrNull(); - - if (cachedSource == null || - cachedSource.sourceType != SourceType.jiosaavn) { - final siblings = - await fetchSiblings(ref: ref, query: query, weakMatch: weakMatch); - - if (siblings.isEmpty) { - throw TrackNotFoundError(query); - } - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: siblings.first.info.id, - sourceType: const Value(SourceType.jiosaavn), - ), - ); - - return JioSaavnSourcedTrack( - ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - sources: siblings.first.source!, - info: siblings.first.info, - query: query, - source: AudioSource.jiosaavn, - ); - } - - final [item] = - await jiosaavnClient.songs.detailsById([cachedSource.sourceId]); - - final (:info, :source) = toSiblingType(item); - - return JioSaavnSourcedTrack( - ref: ref, - siblings: [], - sources: source!, - query: query, - info: info, - source: AudioSource.jiosaavn, - ); - } - - static SiblingType toSiblingType(SongResponse result) { - final SiblingType sibling = ( - info: TrackSourceInfo( - artists: [ - result.primaryArtists, - if (result.featuredArtists.isNotEmpty) ", ", - result.featuredArtists - ].join("").unescapeHtml(), - durationMs: - Duration(seconds: int.parse(result.duration)).inMilliseconds, - id: result.id, - pageUrl: result.url, - thumbnail: result.image?.last.link ?? "", - title: result.name!.unescapeHtml(), - ), - source: result.downloadUrl!.map((link) { - return TrackSource( - url: link.link, - quality: link.quality == "320kbps" - ? SourceQualities.high - : link.quality == "160kbps" - ? SourceQualities.medium - : SourceQualities.low, - codec: SourceCodecs.m4a, - bitrate: link.quality, - qualityLabel: "AAC • ${link.quality} • MP4 • Stereo", - ); - }).toList() - ); - - return sibling; - } - - static Future> fetchSiblings({ - required TrackSourceQuery query, - required Ref ref, - bool weakMatch = false, - }) async { - final searchQuery = SourcedTrack.getSearchTerm(query); - - final SongSearchResponse(:results) = - await jiosaavnClient.search.songs(searchQuery, limit: 20); - - final trackArtistNames = query.artists; - - final matchedResults = results - .where( - (s) { - s.name?.unescapeHtml().contains(query.title) ?? false; - - final sameName = s.name?.unescapeHtml() == query.title; - final artistNames = [ - s.primaryArtists, - if (s.featuredArtists.isNotEmpty) ", ", - s.featuredArtists - ].join("").unescapeHtml(); - final sameArtists = artistNames.split(", ").any( - (artist) => trackArtistNames.any((ar) => artist == ar), - ); - if (weakMatch) { - final containsName = - s.name?.unescapeHtml().contains(query.title) ?? false; - final containsPrimaryArtist = s.primaryArtists - .unescapeHtml() - .contains(trackArtistNames.first); - - return containsName && containsPrimaryArtist; - } - - return sameName && sameArtists; - }, - ) - .map(toSiblingType) - .toList(); - - if (weakMatch && matchedResults.isEmpty) { - return results.map(toSiblingType).toList(); - } - - return matchedResults; - } - - @override - Future copyWithSibling() async { - if (siblings.isNotEmpty) { - return this; - } - final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - - return JioSaavnSourcedTrack( - ref: ref, - siblings: fetchedSiblings - .where((s) => s.info.id != info.id) - .map((s) => s.info) - .toList(), - source: source, - info: info, - query: query, - sources: sources, - ); - } - - @override - Future swapWithSibling(TrackSourceInfo sibling) async { - if (sibling.id == this.info.id) { - return null; - } - - // a sibling source that was fetched from the search results - final isStepSibling = siblings.none((s) => s.id == sibling.id); - - final newSourceInfo = isStepSibling - ? sibling - : siblings.firstWhere((s) => s.id == sibling.id); - final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, this.info); - - final [item] = await jiosaavnClient.songs.detailsById([newSourceInfo.id]); - - final (:info, :source) = toSiblingType(item); - - final database = ref.read(databaseProvider); - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: info.id, - sourceType: const Value(SourceType.jiosaavn), - // Because we're sorting by createdAt in the query - // we have to update it to indicate priority - createdAt: Value(DateTime.now()), - ), - mode: InsertMode.replace, - ); - - return JioSaavnSourcedTrack( - ref: ref, - siblings: newSiblings, - sources: source!, - info: newSourceInfo, - query: query, - source: AudioSource.jiosaavn, - ); - } - - @override - Future refreshStream() async { - // There's no need to refresh the stream for JioSaavnSourcedTrack - return this; - } -} diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart deleted file mode 100644 index fca6c623..00000000 --- a/lib/services/sourced_track/sources/piped.dart +++ /dev/null @@ -1,292 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:piped_client/piped_client.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; - -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/exceptions.dart'; -import 'package:spotube/services/sourced_track/models/video_info.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:spotube/services/sourced_track/sources/youtube.dart'; -import 'package:spotube/utils/service_utils.dart'; - -final pipedProvider = Provider( - (ref) { - final instance = - ref.watch(userPreferencesProvider.select((s) => s.pipedInstance)); - return PipedClient(instance: instance); - }, -); - -class PipedSourcedTrack extends SourcedTrack { - PipedSourcedTrack({ - required super.ref, - required super.source, - required super.siblings, - required super.info, - required super.query, - required super.sources, - }); - - static Future fetchFromTrack({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final audioSource = ref.read(userPreferencesProvider).audioSource; - final database = ref.read(databaseProvider); - final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(query.id)) - ..limit(1) - ..orderBy([ - (s) => - OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), - ])) - .getSingleOrNull(); - final preferences = ref.read(userPreferencesProvider); - final pipedClient = ref.read(pipedProvider); - - if (cachedSource == null) { - final siblings = await fetchSiblings(ref: ref, query: query); - if (siblings.isEmpty) { - throw TrackNotFoundError(query); - } - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: siblings.first.info.id, - sourceType: Value( - preferences.searchMode == SearchMode.youtube - ? SourceType.youtube - : SourceType.youtubeMusic, - ), - ), - ); - - return PipedSourcedTrack( - ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - source: audioSource, - info: siblings.first.info, - query: query, - sources: siblings.first.source!, - ); - } else { - final manifest = await pipedClient.streams(cachedSource.sourceId); - - return PipedSourcedTrack( - ref: ref, - siblings: [], - sources: toSources(manifest), - info: TrackSourceInfo( - id: manifest.id, - artists: manifest.uploader, - pageUrl: "https://www.youtube.com/watch?v=${manifest.id}", - thumbnail: manifest.thumbnailUrl, - title: manifest.title, - durationMs: manifest.duration.inMilliseconds, - ), - query: query, - source: audioSource, - ); - } - } - - static List toSources(PipedStreamResponse manifest) { - return manifest.audioStreams.map((audio) { - final isMp4 = audio.format == PipedAudioStreamFormat.m4a; - return TrackSource( - url: audio.url.toString(), - quality: switch (audio.quality) { - "high" => SourceQualities.high, - "medium" => SourceQualities.medium, - _ => SourceQualities.low, - }, - codec: isMp4 ? SourceCodecs.m4a : SourceCodecs.weba, - bitrate: audio.bitrate.toString(), - qualityLabel: - "${isMp4 ? "AAC" : "Opus"} • ${(audio.bitrate / 1000).floor()}kbps " - "• ${isMp4 ? "m4a" : "weba"} • Stereo", - ); - }).toList(); - } - - static Future toSiblingType( - int index, - YoutubeVideoInfo item, - PipedClient pipedClient, - ) async { - List? sources; - if (index == 0) { - final manifest = await pipedClient.streams(item.id); - sources = toSources(manifest); - } - - final SiblingType sibling = ( - info: TrackSourceInfo( - id: item.id, - artists: item.channelName, - pageUrl: "https://www.youtube.com/watch?v=${item.id}", - thumbnail: item.thumbnailUrl, - title: item.title, - durationMs: item.duration.inMilliseconds, - ), - source: sources, - ); - - return sibling; - } - - static Future> fetchSiblings({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final pipedClient = ref.read(pipedProvider); - final preference = ref.read(userPreferencesProvider); - - final searchQuery = SourcedTrack.getSearchTerm(query); - - final PipedSearchResult(items: searchResults) = await pipedClient.search( - searchQuery, - preference.searchMode == SearchMode.youtube - ? PipedFilter.videos - : PipedFilter.musicSongs, - ); - - // when falling back to piped API make sure to use the YouTube mode - final isYouTubeMusic = preference.audioSource != AudioSource.piped - ? false - : preference.searchMode == SearchMode.youtubeMusic; - - if (isYouTubeMusic) { - final artists = query.artists; - - return await Future.wait( - searchResults - .map( - (result) => YoutubeVideoInfo.fromSearchItemStream( - result as PipedSearchItemStream, - preference.searchMode, - ), - ) - .sorted((a, b) => b.views.compareTo(a.views)) - .where( - (item) => artists.any( - (artist) => - artist.toLowerCase() == item.channelName.toLowerCase(), - ), - ) - .mapIndexed((i, r) => toSiblingType(i, r, pipedClient)), - ); - } - - if (ServiceUtils.onlyContainsEnglish(searchQuery)) { - return await Future.wait( - searchResults - .whereType() - .map( - (result) => YoutubeVideoInfo.fromSearchItemStream( - result, - preference.searchMode, - ), - ) - .mapIndexed((i, r) => toSiblingType(i, r, pipedClient)), - ); - } - - final rankedSiblings = YoutubeSourcedTrack.rankResults( - searchResults - .map( - (result) => YoutubeVideoInfo.fromSearchItemStream( - result as PipedSearchItemStream, - preference.searchMode, - ), - ) - .toList(), - query, - ); - - return await Future.wait( - rankedSiblings.mapIndexed((i, r) => toSiblingType(i, r, pipedClient)), - ); - } - - @override - Future copyWithSibling() async { - if (siblings.isNotEmpty) { - return this; - } - final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - - return PipedSourcedTrack( - ref: ref, - siblings: fetchedSiblings - .where((s) => s.info.id != info.id) - .map((s) => s.info) - .toList(), - source: source, - info: info, - query: query, - sources: sources, - ); - } - - @override - Future swapWithSibling(TrackSourceInfo sibling) async { - if (sibling.id == info.id) { - return null; - } - - // a sibling source that was fetched from the search results - final isStepSibling = siblings.none((s) => s.id == sibling.id); - - final newSourceInfo = isStepSibling - ? sibling - : siblings.firstWhere((s) => s.id == sibling.id); - final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, info); - - final pipedClient = ref.read(pipedProvider); - - final manifest = await pipedClient.streams(newSourceInfo.id); - - final database = ref.read(databaseProvider); - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: newSourceInfo.id, - sourceType: const Value(SourceType.youtube), - // Because we're sorting by createdAt in the query - // we have to update it to indicate priority - createdAt: Value(DateTime.now()), - ), - mode: InsertMode.replace, - ); - - return PipedSourcedTrack( - ref: ref, - siblings: newSiblings, - sources: toSources(manifest), - info: newSourceInfo, - query: query, - source: source, - ); - } - - @override - Future refreshStream() async { - final manifest = await ref.read(pipedProvider).streams(info.id); - return PipedSourcedTrack( - ref: ref, - siblings: siblings, - info: info, - source: source, - query: query, - sources: toSources(manifest), - ); - } -} diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart deleted file mode 100644 index e3e9dd39..00000000 --- a/lib/services/sourced_track/sources/youtube.dart +++ /dev/null @@ -1,439 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:dio/dio.dart'; -import 'package:drift/drift.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/youtube_engine/youtube_engine.dart'; -import 'package:spotube/services/dio/dio.dart'; -import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/song_link/song_link.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/exceptions.dart'; -import 'package:spotube/services/sourced_track/models/video_info.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:spotube/utils/service_utils.dart'; -import 'package:youtube_explode_dart/youtube_explode_dart.dart'; - -final officialMusicRegex = RegExp( - r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)", - caseSensitive: false, -); - -class YoutubeSourcedTrack extends SourcedTrack { - YoutubeSourcedTrack({ - required super.source, - required super.siblings, - required super.info, - required super.query, - required super.sources, - required super.ref, - }); - - static Future fetchFromTrack({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final audioSource = ref.read(userPreferencesProvider).audioSource; - final database = ref.read(databaseProvider); - final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(query.id)) - ..limit(1) - ..orderBy([ - (s) => - OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), - ])) - .get() - .then((s) => s.firstOrNull); - - if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) { - final siblings = await fetchSiblings(ref: ref, query: query); - if (siblings.isEmpty) { - throw TrackNotFoundError(query); - } - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: siblings.first.info.id, - sourceType: const Value(SourceType.youtube), - ), - ); - - return YoutubeSourcedTrack( - ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - info: siblings.first.info, - source: audioSource, - sources: siblings.first.source ?? [], - query: query, - ); - } - final (item, manifest) = await ref - .read(youtubeEngineProvider) - .getVideoWithStreamInfo(cachedSource.sourceId); - - final sourcedTrack = YoutubeSourcedTrack( - ref: ref, - siblings: [], - sources: toTrackSources(manifest), - info: TrackSourceInfo( - id: item.id.value, - artists: item.author, - pageUrl: item.url, - thumbnail: item.thumbnails.highResUrl, - title: item.title, - durationMs: item.duration?.inMilliseconds ?? 0, - ), - query: query, - source: audioSource, - ); - - AppLogger.log.i("${query.title}: ${sourcedTrack.url}"); - - return sourcedTrack; - } - - static List toTrackSources(StreamManifest manifest) { - return manifest.audioOnly.map((streamInfo) { - var isWebm = streamInfo.codec.mimeType == "audio/webm"; - return TrackSource( - url: streamInfo.url.toString(), - quality: switch (streamInfo.qualityLabel) { - "medium" => SourceQualities.medium, - "high" => SourceQualities.high, - "low" => SourceQualities.low, - _ => SourceQualities.high, - }, - codec: isWebm ? SourceCodecs.weba : SourceCodecs.m4a, - bitrate: streamInfo.bitrate.bitsPerSecond.toString(), - qualityLabel: - "${isWebm ? "Opus" : "AAC"} • ${(streamInfo.bitrate.kiloBitsPerSecond).floor()}kbps " - "• ${isWebm ? "weba" : "m4a"} • Stereo", - ); - }).toList(); - } - - static Future toSiblingType( - int index, - YoutubeVideoInfo item, - dynamic ref, - ) async { - assert(ref is WidgetRef || ref is Ref, "Invalid ref type"); - - List? sourceMap; - if (index == 0) { - final manifest = - await ref.read(youtubeEngineProvider).getStreamManifest(item.id); - sourceMap = toTrackSources(manifest); - } - - final SiblingType sibling = ( - info: TrackSourceInfo( - id: item.id, - artists: item.channelName, - pageUrl: "https://www.youtube.com/watch?v=${item.id}", - thumbnail: item.thumbnailUrl, - title: item.title, - durationMs: item.duration.inMilliseconds, - ), - source: sourceMap, - ); - - return sibling; - } - - static List rankResults( - List results, TrackSourceQuery track) { - return results - .sorted((a, b) => b.views.compareTo(a.views)) - .map((sibling) { - int score = 0; - - for (final artist in track.artists) { - final isSameChannelArtist = - sibling.channelName.toLowerCase() == artist.toLowerCase(); - final channelContainsArtist = sibling.channelName - .toLowerCase() - .contains(artist.toLowerCase()); - - if (isSameChannelArtist || channelContainsArtist) { - score += 1; - } - - final titleContainsArtist = - sibling.title.toLowerCase().contains(artist.toLowerCase()); - - if (titleContainsArtist) { - score += 1; - } - } - - final titleContainsTrackName = - sibling.title.toLowerCase().contains(track.title.toLowerCase()); - - final hasOfficialFlag = - officialMusicRegex.hasMatch(sibling.title.toLowerCase()); - - if (titleContainsTrackName) { - score += 3; - } - - if (hasOfficialFlag) { - score += 1; - } - - if (hasOfficialFlag && titleContainsTrackName) { - score += 2; - } - - return (sibling: sibling, score: score); - }) - .sorted((a, b) => b.score.compareTo(a.score)) - .map((e) => e.sibling) - .toList(); - } - - static Future> fetchFromIsrc({ - required TrackSourceQuery track, - required Ref ref, - }) async { - final isrcResults = []; - final isrc = track.isrc; - if (isrc.isNotEmpty) { - final searchedVideos = - await ref.read(youtubeEngineProvider).searchVideos(isrc.toString()); - if (searchedVideos.isNotEmpty) { - AppLogger.log - .d("${track.title} ISRC $isrc Total ${searchedVideos.length}"); - - final stringBuffer = StringBuffer(); - - final filteredMatches = searchedVideos - .map(YoutubeVideoInfo.fromVideo) - .map((YoutubeVideoInfo videoInfo) { - final ytWords = videoInfo.title - .toLowerCase() - .replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '') - .split(RegExp(r'\p{Z}+', unicode: true)) - .where((item) => item.isNotEmpty); - final spWords = track.title - .toLowerCase() - .replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '') - .split(RegExp(r'\p{Z}+', unicode: true)) - .where((item) => item.isNotEmpty); - // Single word and duration match with 3 second tolerance - if (ytWords.any((word) => spWords.contains(word)) && - (videoInfo.duration - - Duration(milliseconds: track.durationMs)) - .abs() - .inMilliseconds <= - 3000) { - stringBuffer.writeln( - "ISRC MATCH: ${videoInfo.id} ${videoInfo.title} by ${videoInfo.channelName} ${videoInfo.duration}", - ); - - return videoInfo; - } - return null; - }) - .nonNulls - .toList(); - - AppLogger.log.d(stringBuffer.toString()); - - isrcResults.addAll(filteredMatches); - } - } - return isrcResults; - } - - static Future> fetchSiblings({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final videoResults = []; - - if (query is! SourcedTrack) { - final isrcResults = await fetchFromIsrc( - track: query, - ref: ref, - ); - - videoResults.addAll(isrcResults); - - if (isrcResults.isEmpty) { - AppLogger.log.w("No ISRC results found, falling back to SongLink"); - - final links = await SongLinkService.links(query.id); - - final stringBuffer = links.fold( - StringBuffer(), - (previousValue, element) { - previousValue.writeln( - "SongLink ${query.id} ${element.platform} ${element.url}"); - return previousValue; - }, - ); - - AppLogger.log.d(stringBuffer.toString()); - - final ytLink = links.firstWhereOrNull( - (link) => link.platform == "youtube", - ); - if (ytLink?.url != null) { - try { - videoResults.add( - YoutubeVideoInfo.fromVideo(await ref - .read(youtubeEngineProvider) - .getVideo(Uri.parse(ytLink!.url!).queryParameters["v"]!)), - ); - } on VideoUnplayableException catch (e, stack) { - // Ignore this error and continue with the search - AppLogger.reportError(e, stack); - } - } else { - AppLogger.log.w("No YouTube link found in SongLink results"); - } - } - } - - final searchQuery = SourcedTrack.getSearchTerm(query); - - final searchResults = - await ref.read(youtubeEngineProvider).searchVideos(searchQuery); - - if (ServiceUtils.onlyContainsEnglish(searchQuery)) { - videoResults - .addAll(searchResults.map(YoutubeVideoInfo.fromVideo).toList()); - } else { - videoResults.addAll(rankResults( - searchResults.map(YoutubeVideoInfo.fromVideo).toList(), - query, - )); - } - - final seenIds = {}; - int index = 0; - return await Future.wait( - videoResults.map((videoResult) async { - // Deduplicate results - if (!seenIds.contains(videoResult.id)) { - seenIds.add(videoResult.id); - return await toSiblingType(index++, videoResult, ref); - } - return null; - }), - ).then((s) => s.whereType().toList()); - } - - @override - Future swapWithSibling(TrackSourceInfo sibling) async { - if (sibling.id == info.id) { - return null; - } - - // a sibling source that was fetched from the search results - final isStepSibling = siblings.none((s) => s.id == sibling.id); - - final newSourceInfo = isStepSibling - ? sibling - : siblings.firstWhere((s) => s.id == sibling.id); - - final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, info); - - final manifest = await ref - .read(youtubeEngineProvider) - .getStreamManifest(newSourceInfo.id); - - final database = ref.read(databaseProvider); - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: newSourceInfo.id, - sourceType: const Value(SourceType.youtube), - // Because we're sorting by createdAt in the query - // we have to update it to indicate priority - createdAt: Value(DateTime.now()), - ), - mode: InsertMode.replace, - ); - - return YoutubeSourcedTrack( - ref: ref, - source: source, - siblings: newSiblings, - sources: toTrackSources(manifest), - info: newSourceInfo, - query: query, - ); - } - - @override - Future copyWithSibling() async { - if (siblings.isNotEmpty) { - return this; - } - final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - - return YoutubeSourcedTrack( - ref: ref, - siblings: fetchedSiblings - .where((s) => s.info.id != info.id) - .map((s) => s.info) - .toList(), - source: source, - sources: sources, - info: info, - query: query, - ); - } - - @override - Future refreshStream() async { - List validStreams = []; - - final stringBuffer = StringBuffer(); - for (final source in sources) { - final res = await globalDio.head( - source.url, - options: - Options(validateStatus: (status) => status != null && status < 500), - ); - - stringBuffer.writeln( - "[${query.id}] ${res.statusCode} ${source.quality} ${source.codec} ${source.bitrate}", - ); - - if (res.statusCode! < 400) { - validStreams.add(source); - } - } - - AppLogger.log.d(stringBuffer.toString()); - - if (validStreams.isEmpty) { - final manifest = - await ref.read(youtubeEngineProvider).getStreamManifest(info.id); - - validStreams = toTrackSources(manifest); - } - - final sourcedTrack = YoutubeSourcedTrack( - ref: ref, - siblings: siblings, - source: source, - sources: validStreams, - info: info, - query: query, - ); - - AppLogger.log.i("Refreshing ${query.title}: ${sourcedTrack.url}"); - - return sourcedTrack; - } -} diff --git a/lib/services/youtube_engine/youtube_explode_engine.dart b/lib/services/youtube_engine/youtube_explode_engine.dart index fa58314c..c552f883 100644 --- a/lib/services/youtube_engine/youtube_explode_engine.dart +++ b/lib/services/youtube_engine/youtube_explode_engine.dart @@ -162,7 +162,6 @@ class YouTubeExplodeEngine implements YouTubeEngine { requireWatchPage: false, ytClients: [ YoutubeApiClient.ios, - YoutubeApiClient.android, YoutubeApiClient.androidVr, ], ); diff --git a/pubspec.lock b/pubspec.lock index 08757d74..8623af4e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2888,12 +2888,11 @@ packages: youtube_explode_dart: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: caa3023386dbc10e69c99f49f491148094874671 - url: "https://github.com/Coronon/youtube_explode_dart" - source: git - version: "2.5.2" + name: youtube_explode_dart + sha256: "947ba05e0c4f050743e480e7bca3575ff6427d86cc898c1a69f5e1d188cdc9e0" + url: "https://pub.dev" + source: hosted + version: "2.5.3" yt_dlp_dart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 46273a32..4087bc0d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -138,8 +138,7 @@ dependencies: wikipedia_api: ^0.1.0 win32_registry: ^1.1.5 window_manager: ^0.4.3 - youtube_explode_dart: - git: https://github.com/Coronon/youtube_explode_dart + youtube_explode_dart: ^2.5.3 yt_dlp_dart: git: url: https://github.com/KRTirtho/yt_dlp_dart.git diff --git a/test/drift/app_db/generated/schema.dart b/test/drift/app_db/generated/schema.dart index 413b4408..76573e49 100644 --- a/test/drift/app_db/generated/schema.dart +++ b/test/drift/app_db/generated/schema.dart @@ -12,6 +12,7 @@ import 'schema_v6.dart' as v6; import 'schema_v7.dart' as v7; import 'schema_v8.dart' as v8; import 'schema_v9.dart' as v9; +import 'schema_v10.dart' as v10; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -35,10 +36,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v8.DatabaseAtV8(db); case 9: return v9.DatabaseAtV9(db); + case 10: + return v10.DatabaseAtV10(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9]; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; } diff --git a/test/drift/app_db/generated/schema_v10.dart b/test/drift/app_db/generated/schema_v10.dart new file mode 100644 index 00000000..36cc2a6b --- /dev/null +++ b/test/drift/app_db/generated/schema_v10.dart @@ -0,0 +1,3472 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class AuthenticationTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthenticationTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn cookie = GeneratedColumn( + 'cookie', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn accessToken = GeneratedColumn( + 'access_token', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn expiration = GeneratedColumn( + 'expiration', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + @override + List get $columns => [id, cookie, accessToken, expiration]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'authentication_table'; + @override + Set get $primaryKey => {id}; + @override + AuthenticationTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthenticationTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + cookie: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}cookie'])!, + accessToken: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}access_token'])!, + expiration: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}expiration'])!, + ); + } + + @override + AuthenticationTable createAlias(String alias) { + return AuthenticationTable(attachedDatabase, alias); + } +} + +class AuthenticationTableData extends DataClass + implements Insertable { + final int id; + final String cookie; + final String accessToken; + final DateTime expiration; + const AuthenticationTableData( + {required this.id, + required this.cookie, + required this.accessToken, + required this.expiration}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['cookie'] = Variable(cookie); + map['access_token'] = Variable(accessToken); + map['expiration'] = Variable(expiration); + return map; + } + + AuthenticationTableCompanion toCompanion(bool nullToAbsent) { + return AuthenticationTableCompanion( + id: Value(id), + cookie: Value(cookie), + accessToken: Value(accessToken), + expiration: Value(expiration), + ); + } + + factory AuthenticationTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthenticationTableData( + id: serializer.fromJson(json['id']), + cookie: serializer.fromJson(json['cookie']), + accessToken: serializer.fromJson(json['accessToken']), + expiration: serializer.fromJson(json['expiration']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'cookie': serializer.toJson(cookie), + 'accessToken': serializer.toJson(accessToken), + 'expiration': serializer.toJson(expiration), + }; + } + + AuthenticationTableData copyWith( + {int? id, + String? cookie, + String? accessToken, + DateTime? expiration}) => + AuthenticationTableData( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + AuthenticationTableData copyWithCompanion(AuthenticationTableCompanion data) { + return AuthenticationTableData( + id: data.id.present ? data.id.value : this.id, + cookie: data.cookie.present ? data.cookie.value : this.cookie, + accessToken: + data.accessToken.present ? data.accessToken.value : this.accessToken, + expiration: + data.expiration.present ? data.expiration.value : this.expiration, + ); + } + + @override + String toString() { + return (StringBuffer('AuthenticationTableData(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, cookie, accessToken, expiration); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthenticationTableData && + other.id == this.id && + other.cookie == this.cookie && + other.accessToken == this.accessToken && + other.expiration == this.expiration); +} + +class AuthenticationTableCompanion + extends UpdateCompanion { + final Value id; + final Value cookie; + final Value accessToken; + final Value expiration; + const AuthenticationTableCompanion({ + this.id = const Value.absent(), + this.cookie = const Value.absent(), + this.accessToken = const Value.absent(), + this.expiration = const Value.absent(), + }); + AuthenticationTableCompanion.insert({ + this.id = const Value.absent(), + required String cookie, + required String accessToken, + required DateTime expiration, + }) : cookie = Value(cookie), + accessToken = Value(accessToken), + expiration = Value(expiration); + static Insertable custom({ + Expression? id, + Expression? cookie, + Expression? accessToken, + Expression? expiration, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (cookie != null) 'cookie': cookie, + if (accessToken != null) 'access_token': accessToken, + if (expiration != null) 'expiration': expiration, + }); + } + + AuthenticationTableCompanion copyWith( + {Value? id, + Value? cookie, + Value? accessToken, + Value? expiration}) { + return AuthenticationTableCompanion( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (cookie.present) { + map['cookie'] = Variable(cookie.value); + } + if (accessToken.present) { + map['access_token'] = Variable(accessToken.value); + } + if (expiration.present) { + map['expiration'] = Variable(expiration.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthenticationTableCompanion(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } +} + +class BlacklistTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + BlacklistTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn elementType = GeneratedColumn( + 'element_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn elementId = GeneratedColumn( + 'element_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, name, elementType, elementId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'blacklist_table'; + @override + Set get $primaryKey => {id}; + @override + BlacklistTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BlacklistTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + elementType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_type'])!, + elementId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_id'])!, + ); + } + + @override + BlacklistTable createAlias(String alias) { + return BlacklistTable(attachedDatabase, alias); + } +} + +class BlacklistTableData extends DataClass + implements Insertable { + final int id; + final String name; + final String elementType; + final String elementId; + const BlacklistTableData( + {required this.id, + required this.name, + required this.elementType, + required this.elementId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['element_type'] = Variable(elementType); + map['element_id'] = Variable(elementId); + return map; + } + + BlacklistTableCompanion toCompanion(bool nullToAbsent) { + return BlacklistTableCompanion( + id: Value(id), + name: Value(name), + elementType: Value(elementType), + elementId: Value(elementId), + ); + } + + factory BlacklistTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BlacklistTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + elementType: serializer.fromJson(json['elementType']), + elementId: serializer.fromJson(json['elementId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'elementType': serializer.toJson(elementType), + 'elementId': serializer.toJson(elementId), + }; + } + + BlacklistTableData copyWith( + {int? id, String? name, String? elementType, String? elementId}) => + BlacklistTableData( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + BlacklistTableData copyWithCompanion(BlacklistTableCompanion data) { + return BlacklistTableData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + elementType: + data.elementType.present ? data.elementType.value : this.elementType, + elementId: data.elementId.present ? data.elementId.value : this.elementId, + ); + } + + @override + String toString() { + return (StringBuffer('BlacklistTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, elementType, elementId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BlacklistTableData && + other.id == this.id && + other.name == this.name && + other.elementType == this.elementType && + other.elementId == this.elementId); +} + +class BlacklistTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value elementType; + final Value elementId; + const BlacklistTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.elementType = const Value.absent(), + this.elementId = const Value.absent(), + }); + BlacklistTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required String elementType, + required String elementId, + }) : name = Value(name), + elementType = Value(elementType), + elementId = Value(elementId); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? elementType, + Expression? elementId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (elementType != null) 'element_type': elementType, + if (elementId != null) 'element_id': elementId, + }); + } + + BlacklistTableCompanion copyWith( + {Value? id, + Value? name, + Value? elementType, + Value? elementId}) { + return BlacklistTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (elementType.present) { + map['element_type'] = Variable(elementType.value); + } + if (elementId.present) { + map['element_id'] = Variable(elementId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BlacklistTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } +} + +class PreferencesTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PreferencesTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn albumColorSync = GeneratedColumn( + 'album_color_sync', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("album_color_sync" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn amoledDarkTheme = GeneratedColumn( + 'amoled_dark_theme', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("amoled_dark_theme" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn checkUpdate = GeneratedColumn( + 'check_update', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("check_update" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn normalizeAudio = GeneratedColumn( + 'normalize_audio', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("normalize_audio" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn showSystemTrayIcon = GeneratedColumn( + 'show_system_tray_icon', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("show_system_tray_icon" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn systemTitleBar = GeneratedColumn( + 'system_title_bar', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("system_title_bar" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn skipNonMusic = GeneratedColumn( + 'skip_non_music', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("skip_non_music" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn closeBehavior = GeneratedColumn( + 'close_behavior', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(CloseBehavior.close.name)); + late final GeneratedColumn accentColorScheme = + GeneratedColumn('accent_color_scheme', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("Slate:0xff64748b")); + late final GeneratedColumn layoutMode = GeneratedColumn( + 'layout_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(LayoutMode.adaptive.name)); + late final GeneratedColumn locale = GeneratedColumn( + 'locale', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: + const Constant('{"languageCode":"system","countryCode":"system"}')); + late final GeneratedColumn market = GeneratedColumn( + 'market', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(Market.US.name)); + late final GeneratedColumn searchMode = GeneratedColumn( + 'search_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SearchMode.youtube.name)); + late final GeneratedColumn downloadLocation = GeneratedColumn( + 'download_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")); + late final GeneratedColumn localLibraryLocation = + GeneratedColumn('local_library_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")); + late final GeneratedColumn pipedInstance = GeneratedColumn( + 'piped_instance', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("https://pipedapi.kavin.rocks")); + late final GeneratedColumn invidiousInstance = + GeneratedColumn('invidious_instance', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("https://inv.nadeko.net")); + late final GeneratedColumn themeMode = GeneratedColumn( + 'theme_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(ThemeMode.system.name)); + late final GeneratedColumn audioSourceId = GeneratedColumn( + 'audio_source_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn youtubeClientEngine = + GeneratedColumn('youtube_client_engine', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name)); + late final GeneratedColumn discordPresence = GeneratedColumn( + 'discord_presence', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("discord_presence" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn endlessPlayback = GeneratedColumn( + 'endless_playback', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("endless_playback" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn enableConnect = GeneratedColumn( + 'enable_connect', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("enable_connect" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn connectPort = GeneratedColumn( + 'connect_port', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(-1)); + late final GeneratedColumn cacheMusic = GeneratedColumn( + 'cache_music', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("cache_music" IN (0, 1))'), + defaultValue: const Constant(true)); + @override + List get $columns => [ + id, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + invidiousInstance, + themeMode, + audioSourceId, + youtubeClientEngine, + discordPresence, + endlessPlayback, + enableConnect, + connectPort, + cacheMusic + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'preferences_table'; + @override + Set get $primaryKey => {id}; + @override + PreferencesTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PreferencesTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + albumColorSync: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}album_color_sync'])!, + amoledDarkTheme: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}amoled_dark_theme'])!, + checkUpdate: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}check_update'])!, + normalizeAudio: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}normalize_audio'])!, + showSystemTrayIcon: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}show_system_tray_icon'])!, + systemTitleBar: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}system_title_bar'])!, + skipNonMusic: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}skip_non_music'])!, + closeBehavior: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}close_behavior'])!, + accentColorScheme: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}accent_color_scheme'])!, + layoutMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}layout_mode'])!, + locale: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}locale'])!, + market: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}market'])!, + searchMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}search_mode'])!, + downloadLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}download_location'])!, + localLibraryLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_library_location'])!, + pipedInstance: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, + invidiousInstance: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}invidious_instance'])!, + themeMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}theme_mode'])!, + audioSourceId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}audio_source_id']), + youtubeClientEngine: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}youtube_client_engine'])!, + discordPresence: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}discord_presence'])!, + endlessPlayback: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}endless_playback'])!, + enableConnect: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}enable_connect'])!, + connectPort: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}connect_port'])!, + cacheMusic: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}cache_music'])!, + ); + } + + @override + PreferencesTable createAlias(String alias) { + return PreferencesTable(attachedDatabase, alias); + } +} + +class PreferencesTableData extends DataClass + implements Insertable { + final int id; + final bool albumColorSync; + final bool amoledDarkTheme; + final bool checkUpdate; + final bool normalizeAudio; + final bool showSystemTrayIcon; + final bool systemTitleBar; + final bool skipNonMusic; + final String closeBehavior; + final String accentColorScheme; + final String layoutMode; + final String locale; + final String market; + final String searchMode; + final String downloadLocation; + final String localLibraryLocation; + final String pipedInstance; + final String invidiousInstance; + final String themeMode; + final String? audioSourceId; + final String youtubeClientEngine; + final bool discordPresence; + final bool endlessPlayback; + final bool enableConnect; + final int connectPort; + final bool cacheMusic; + const PreferencesTableData( + {required this.id, + required this.albumColorSync, + required this.amoledDarkTheme, + required this.checkUpdate, + required this.normalizeAudio, + required this.showSystemTrayIcon, + required this.systemTitleBar, + required this.skipNonMusic, + required this.closeBehavior, + required this.accentColorScheme, + required this.layoutMode, + required this.locale, + required this.market, + required this.searchMode, + required this.downloadLocation, + required this.localLibraryLocation, + required this.pipedInstance, + required this.invidiousInstance, + required this.themeMode, + this.audioSourceId, + required this.youtubeClientEngine, + required this.discordPresence, + required this.endlessPlayback, + required this.enableConnect, + required this.connectPort, + required this.cacheMusic}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['album_color_sync'] = Variable(albumColorSync); + map['amoled_dark_theme'] = Variable(amoledDarkTheme); + map['check_update'] = Variable(checkUpdate); + map['normalize_audio'] = Variable(normalizeAudio); + map['show_system_tray_icon'] = Variable(showSystemTrayIcon); + map['system_title_bar'] = Variable(systemTitleBar); + map['skip_non_music'] = Variable(skipNonMusic); + map['close_behavior'] = Variable(closeBehavior); + map['accent_color_scheme'] = Variable(accentColorScheme); + map['layout_mode'] = Variable(layoutMode); + map['locale'] = Variable(locale); + map['market'] = Variable(market); + map['search_mode'] = Variable(searchMode); + map['download_location'] = Variable(downloadLocation); + map['local_library_location'] = Variable(localLibraryLocation); + map['piped_instance'] = Variable(pipedInstance); + map['invidious_instance'] = Variable(invidiousInstance); + map['theme_mode'] = Variable(themeMode); + if (!nullToAbsent || audioSourceId != null) { + map['audio_source_id'] = Variable(audioSourceId); + } + map['youtube_client_engine'] = Variable(youtubeClientEngine); + map['discord_presence'] = Variable(discordPresence); + map['endless_playback'] = Variable(endlessPlayback); + map['enable_connect'] = Variable(enableConnect); + map['connect_port'] = Variable(connectPort); + map['cache_music'] = Variable(cacheMusic); + return map; + } + + PreferencesTableCompanion toCompanion(bool nullToAbsent) { + return PreferencesTableCompanion( + id: Value(id), + albumColorSync: Value(albumColorSync), + amoledDarkTheme: Value(amoledDarkTheme), + checkUpdate: Value(checkUpdate), + normalizeAudio: Value(normalizeAudio), + showSystemTrayIcon: Value(showSystemTrayIcon), + systemTitleBar: Value(systemTitleBar), + skipNonMusic: Value(skipNonMusic), + closeBehavior: Value(closeBehavior), + accentColorScheme: Value(accentColorScheme), + layoutMode: Value(layoutMode), + locale: Value(locale), + market: Value(market), + searchMode: Value(searchMode), + downloadLocation: Value(downloadLocation), + localLibraryLocation: Value(localLibraryLocation), + pipedInstance: Value(pipedInstance), + invidiousInstance: Value(invidiousInstance), + themeMode: Value(themeMode), + audioSourceId: audioSourceId == null && nullToAbsent + ? const Value.absent() + : Value(audioSourceId), + youtubeClientEngine: Value(youtubeClientEngine), + discordPresence: Value(discordPresence), + endlessPlayback: Value(endlessPlayback), + enableConnect: Value(enableConnect), + connectPort: Value(connectPort), + cacheMusic: Value(cacheMusic), + ); + } + + factory PreferencesTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PreferencesTableData( + id: serializer.fromJson(json['id']), + albumColorSync: serializer.fromJson(json['albumColorSync']), + amoledDarkTheme: serializer.fromJson(json['amoledDarkTheme']), + checkUpdate: serializer.fromJson(json['checkUpdate']), + normalizeAudio: serializer.fromJson(json['normalizeAudio']), + showSystemTrayIcon: serializer.fromJson(json['showSystemTrayIcon']), + systemTitleBar: serializer.fromJson(json['systemTitleBar']), + skipNonMusic: serializer.fromJson(json['skipNonMusic']), + closeBehavior: serializer.fromJson(json['closeBehavior']), + accentColorScheme: serializer.fromJson(json['accentColorScheme']), + layoutMode: serializer.fromJson(json['layoutMode']), + locale: serializer.fromJson(json['locale']), + market: serializer.fromJson(json['market']), + searchMode: serializer.fromJson(json['searchMode']), + downloadLocation: serializer.fromJson(json['downloadLocation']), + localLibraryLocation: + serializer.fromJson(json['localLibraryLocation']), + pipedInstance: serializer.fromJson(json['pipedInstance']), + invidiousInstance: serializer.fromJson(json['invidiousInstance']), + themeMode: serializer.fromJson(json['themeMode']), + audioSourceId: serializer.fromJson(json['audioSourceId']), + youtubeClientEngine: + serializer.fromJson(json['youtubeClientEngine']), + discordPresence: serializer.fromJson(json['discordPresence']), + endlessPlayback: serializer.fromJson(json['endlessPlayback']), + enableConnect: serializer.fromJson(json['enableConnect']), + connectPort: serializer.fromJson(json['connectPort']), + cacheMusic: serializer.fromJson(json['cacheMusic']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'albumColorSync': serializer.toJson(albumColorSync), + 'amoledDarkTheme': serializer.toJson(amoledDarkTheme), + 'checkUpdate': serializer.toJson(checkUpdate), + 'normalizeAudio': serializer.toJson(normalizeAudio), + 'showSystemTrayIcon': serializer.toJson(showSystemTrayIcon), + 'systemTitleBar': serializer.toJson(systemTitleBar), + 'skipNonMusic': serializer.toJson(skipNonMusic), + 'closeBehavior': serializer.toJson(closeBehavior), + 'accentColorScheme': serializer.toJson(accentColorScheme), + 'layoutMode': serializer.toJson(layoutMode), + 'locale': serializer.toJson(locale), + 'market': serializer.toJson(market), + 'searchMode': serializer.toJson(searchMode), + 'downloadLocation': serializer.toJson(downloadLocation), + 'localLibraryLocation': serializer.toJson(localLibraryLocation), + 'pipedInstance': serializer.toJson(pipedInstance), + 'invidiousInstance': serializer.toJson(invidiousInstance), + 'themeMode': serializer.toJson(themeMode), + 'audioSourceId': serializer.toJson(audioSourceId), + 'youtubeClientEngine': serializer.toJson(youtubeClientEngine), + 'discordPresence': serializer.toJson(discordPresence), + 'endlessPlayback': serializer.toJson(endlessPlayback), + 'enableConnect': serializer.toJson(enableConnect), + 'connectPort': serializer.toJson(connectPort), + 'cacheMusic': serializer.toJson(cacheMusic), + }; + } + + PreferencesTableData copyWith( + {int? id, + bool? albumColorSync, + bool? amoledDarkTheme, + bool? checkUpdate, + bool? normalizeAudio, + bool? showSystemTrayIcon, + bool? systemTitleBar, + bool? skipNonMusic, + String? closeBehavior, + String? accentColorScheme, + String? layoutMode, + String? locale, + String? market, + String? searchMode, + String? downloadLocation, + String? localLibraryLocation, + String? pipedInstance, + String? invidiousInstance, + String? themeMode, + Value audioSourceId = const Value.absent(), + String? youtubeClientEngine, + bool? discordPresence, + bool? endlessPlayback, + bool? enableConnect, + int? connectPort, + bool? cacheMusic}) => + PreferencesTableData( + id: id ?? this.id, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + invidiousInstance: invidiousInstance ?? this.invidiousInstance, + themeMode: themeMode ?? this.themeMode, + audioSourceId: + audioSourceId.present ? audioSourceId.value : this.audioSourceId, + youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + connectPort: connectPort ?? this.connectPort, + cacheMusic: cacheMusic ?? this.cacheMusic, + ); + PreferencesTableData copyWithCompanion(PreferencesTableCompanion data) { + return PreferencesTableData( + id: data.id.present ? data.id.value : this.id, + albumColorSync: data.albumColorSync.present + ? data.albumColorSync.value + : this.albumColorSync, + amoledDarkTheme: data.amoledDarkTheme.present + ? data.amoledDarkTheme.value + : this.amoledDarkTheme, + checkUpdate: + data.checkUpdate.present ? data.checkUpdate.value : this.checkUpdate, + normalizeAudio: data.normalizeAudio.present + ? data.normalizeAudio.value + : this.normalizeAudio, + showSystemTrayIcon: data.showSystemTrayIcon.present + ? data.showSystemTrayIcon.value + : this.showSystemTrayIcon, + systemTitleBar: data.systemTitleBar.present + ? data.systemTitleBar.value + : this.systemTitleBar, + skipNonMusic: data.skipNonMusic.present + ? data.skipNonMusic.value + : this.skipNonMusic, + closeBehavior: data.closeBehavior.present + ? data.closeBehavior.value + : this.closeBehavior, + accentColorScheme: data.accentColorScheme.present + ? data.accentColorScheme.value + : this.accentColorScheme, + layoutMode: + data.layoutMode.present ? data.layoutMode.value : this.layoutMode, + locale: data.locale.present ? data.locale.value : this.locale, + market: data.market.present ? data.market.value : this.market, + searchMode: + data.searchMode.present ? data.searchMode.value : this.searchMode, + downloadLocation: data.downloadLocation.present + ? data.downloadLocation.value + : this.downloadLocation, + localLibraryLocation: data.localLibraryLocation.present + ? data.localLibraryLocation.value + : this.localLibraryLocation, + pipedInstance: data.pipedInstance.present + ? data.pipedInstance.value + : this.pipedInstance, + invidiousInstance: data.invidiousInstance.present + ? data.invidiousInstance.value + : this.invidiousInstance, + themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, + audioSourceId: data.audioSourceId.present + ? data.audioSourceId.value + : this.audioSourceId, + youtubeClientEngine: data.youtubeClientEngine.present + ? data.youtubeClientEngine.value + : this.youtubeClientEngine, + discordPresence: data.discordPresence.present + ? data.discordPresence.value + : this.discordPresence, + endlessPlayback: data.endlessPlayback.present + ? data.endlessPlayback.value + : this.endlessPlayback, + enableConnect: data.enableConnect.present + ? data.enableConnect.value + : this.enableConnect, + connectPort: + data.connectPort.present ? data.connectPort.value : this.connectPort, + cacheMusic: + data.cacheMusic.present ? data.cacheMusic.value : this.cacheMusic, + ); + } + + @override + String toString() { + return (StringBuffer('PreferencesTableData(') + ..write('id: $id, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('invidiousInstance: $invidiousInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSourceId: $audioSourceId, ') + ..write('youtubeClientEngine: $youtubeClientEngine, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect, ') + ..write('connectPort: $connectPort, ') + ..write('cacheMusic: $cacheMusic') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + id, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + invidiousInstance, + themeMode, + audioSourceId, + youtubeClientEngine, + discordPresence, + endlessPlayback, + enableConnect, + connectPort, + cacheMusic + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PreferencesTableData && + other.id == this.id && + other.albumColorSync == this.albumColorSync && + other.amoledDarkTheme == this.amoledDarkTheme && + other.checkUpdate == this.checkUpdate && + other.normalizeAudio == this.normalizeAudio && + other.showSystemTrayIcon == this.showSystemTrayIcon && + other.systemTitleBar == this.systemTitleBar && + other.skipNonMusic == this.skipNonMusic && + other.closeBehavior == this.closeBehavior && + other.accentColorScheme == this.accentColorScheme && + other.layoutMode == this.layoutMode && + other.locale == this.locale && + other.market == this.market && + other.searchMode == this.searchMode && + other.downloadLocation == this.downloadLocation && + other.localLibraryLocation == this.localLibraryLocation && + other.pipedInstance == this.pipedInstance && + other.invidiousInstance == this.invidiousInstance && + other.themeMode == this.themeMode && + other.audioSourceId == this.audioSourceId && + other.youtubeClientEngine == this.youtubeClientEngine && + other.discordPresence == this.discordPresence && + other.endlessPlayback == this.endlessPlayback && + other.enableConnect == this.enableConnect && + other.connectPort == this.connectPort && + other.cacheMusic == this.cacheMusic); +} + +class PreferencesTableCompanion extends UpdateCompanion { + final Value id; + final Value albumColorSync; + final Value amoledDarkTheme; + final Value checkUpdate; + final Value normalizeAudio; + final Value showSystemTrayIcon; + final Value systemTitleBar; + final Value skipNonMusic; + final Value closeBehavior; + final Value accentColorScheme; + final Value layoutMode; + final Value locale; + final Value market; + final Value searchMode; + final Value downloadLocation; + final Value localLibraryLocation; + final Value pipedInstance; + final Value invidiousInstance; + final Value themeMode; + final Value audioSourceId; + final Value youtubeClientEngine; + final Value discordPresence; + final Value endlessPlayback; + final Value enableConnect; + final Value connectPort; + final Value cacheMusic; + const PreferencesTableCompanion({ + this.id = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.invidiousInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSourceId = const Value.absent(), + this.youtubeClientEngine = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + this.connectPort = const Value.absent(), + this.cacheMusic = const Value.absent(), + }); + PreferencesTableCompanion.insert({ + this.id = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.invidiousInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSourceId = const Value.absent(), + this.youtubeClientEngine = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + this.connectPort = const Value.absent(), + this.cacheMusic = const Value.absent(), + }); + static Insertable custom({ + Expression? id, + Expression? albumColorSync, + Expression? amoledDarkTheme, + Expression? checkUpdate, + Expression? normalizeAudio, + Expression? showSystemTrayIcon, + Expression? systemTitleBar, + Expression? skipNonMusic, + Expression? closeBehavior, + Expression? accentColorScheme, + Expression? layoutMode, + Expression? locale, + Expression? market, + Expression? searchMode, + Expression? downloadLocation, + Expression? localLibraryLocation, + Expression? pipedInstance, + Expression? invidiousInstance, + Expression? themeMode, + Expression? audioSourceId, + Expression? youtubeClientEngine, + Expression? discordPresence, + Expression? endlessPlayback, + Expression? enableConnect, + Expression? connectPort, + Expression? cacheMusic, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (albumColorSync != null) 'album_color_sync': albumColorSync, + if (amoledDarkTheme != null) 'amoled_dark_theme': amoledDarkTheme, + if (checkUpdate != null) 'check_update': checkUpdate, + if (normalizeAudio != null) 'normalize_audio': normalizeAudio, + if (showSystemTrayIcon != null) + 'show_system_tray_icon': showSystemTrayIcon, + if (systemTitleBar != null) 'system_title_bar': systemTitleBar, + if (skipNonMusic != null) 'skip_non_music': skipNonMusic, + if (closeBehavior != null) 'close_behavior': closeBehavior, + if (accentColorScheme != null) 'accent_color_scheme': accentColorScheme, + if (layoutMode != null) 'layout_mode': layoutMode, + if (locale != null) 'locale': locale, + if (market != null) 'market': market, + if (searchMode != null) 'search_mode': searchMode, + if (downloadLocation != null) 'download_location': downloadLocation, + if (localLibraryLocation != null) + 'local_library_location': localLibraryLocation, + if (pipedInstance != null) 'piped_instance': pipedInstance, + if (invidiousInstance != null) 'invidious_instance': invidiousInstance, + if (themeMode != null) 'theme_mode': themeMode, + if (audioSourceId != null) 'audio_source_id': audioSourceId, + if (youtubeClientEngine != null) + 'youtube_client_engine': youtubeClientEngine, + if (discordPresence != null) 'discord_presence': discordPresence, + if (endlessPlayback != null) 'endless_playback': endlessPlayback, + if (enableConnect != null) 'enable_connect': enableConnect, + if (connectPort != null) 'connect_port': connectPort, + if (cacheMusic != null) 'cache_music': cacheMusic, + }); + } + + PreferencesTableCompanion copyWith( + {Value? id, + Value? albumColorSync, + Value? amoledDarkTheme, + Value? checkUpdate, + Value? normalizeAudio, + Value? showSystemTrayIcon, + Value? systemTitleBar, + Value? skipNonMusic, + Value? closeBehavior, + Value? accentColorScheme, + Value? layoutMode, + Value? locale, + Value? market, + Value? searchMode, + Value? downloadLocation, + Value? localLibraryLocation, + Value? pipedInstance, + Value? invidiousInstance, + Value? themeMode, + Value? audioSourceId, + Value? youtubeClientEngine, + Value? discordPresence, + Value? endlessPlayback, + Value? enableConnect, + Value? connectPort, + Value? cacheMusic}) { + return PreferencesTableCompanion( + id: id ?? this.id, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + invidiousInstance: invidiousInstance ?? this.invidiousInstance, + themeMode: themeMode ?? this.themeMode, + audioSourceId: audioSourceId ?? this.audioSourceId, + youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + connectPort: connectPort ?? this.connectPort, + cacheMusic: cacheMusic ?? this.cacheMusic, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (albumColorSync.present) { + map['album_color_sync'] = Variable(albumColorSync.value); + } + if (amoledDarkTheme.present) { + map['amoled_dark_theme'] = Variable(amoledDarkTheme.value); + } + if (checkUpdate.present) { + map['check_update'] = Variable(checkUpdate.value); + } + if (normalizeAudio.present) { + map['normalize_audio'] = Variable(normalizeAudio.value); + } + if (showSystemTrayIcon.present) { + map['show_system_tray_icon'] = Variable(showSystemTrayIcon.value); + } + if (systemTitleBar.present) { + map['system_title_bar'] = Variable(systemTitleBar.value); + } + if (skipNonMusic.present) { + map['skip_non_music'] = Variable(skipNonMusic.value); + } + if (closeBehavior.present) { + map['close_behavior'] = Variable(closeBehavior.value); + } + if (accentColorScheme.present) { + map['accent_color_scheme'] = Variable(accentColorScheme.value); + } + if (layoutMode.present) { + map['layout_mode'] = Variable(layoutMode.value); + } + if (locale.present) { + map['locale'] = Variable(locale.value); + } + if (market.present) { + map['market'] = Variable(market.value); + } + if (searchMode.present) { + map['search_mode'] = Variable(searchMode.value); + } + if (downloadLocation.present) { + map['download_location'] = Variable(downloadLocation.value); + } + if (localLibraryLocation.present) { + map['local_library_location'] = + Variable(localLibraryLocation.value); + } + if (pipedInstance.present) { + map['piped_instance'] = Variable(pipedInstance.value); + } + if (invidiousInstance.present) { + map['invidious_instance'] = Variable(invidiousInstance.value); + } + if (themeMode.present) { + map['theme_mode'] = Variable(themeMode.value); + } + if (audioSourceId.present) { + map['audio_source_id'] = Variable(audioSourceId.value); + } + if (youtubeClientEngine.present) { + map['youtube_client_engine'] = + Variable(youtubeClientEngine.value); + } + if (discordPresence.present) { + map['discord_presence'] = Variable(discordPresence.value); + } + if (endlessPlayback.present) { + map['endless_playback'] = Variable(endlessPlayback.value); + } + if (enableConnect.present) { + map['enable_connect'] = Variable(enableConnect.value); + } + if (connectPort.present) { + map['connect_port'] = Variable(connectPort.value); + } + if (cacheMusic.present) { + map['cache_music'] = Variable(cacheMusic.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PreferencesTableCompanion(') + ..write('id: $id, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('invidiousInstance: $invidiousInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSourceId: $audioSourceId, ') + ..write('youtubeClientEngine: $youtubeClientEngine, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect, ') + ..write('connectPort: $connectPort, ') + ..write('cacheMusic: $cacheMusic') + ..write(')')) + .toString(); + } +} + +class ScrobblerTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ScrobblerTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + late final GeneratedColumn username = GeneratedColumn( + 'username', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn passwordHash = GeneratedColumn( + 'password_hash', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, username, passwordHash]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'scrobbler_table'; + @override + Set get $primaryKey => {id}; + @override + ScrobblerTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ScrobblerTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username'])!, + passwordHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}password_hash'])!, + ); + } + + @override + ScrobblerTable createAlias(String alias) { + return ScrobblerTable(attachedDatabase, alias); + } +} + +class ScrobblerTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final String username; + final String passwordHash; + const ScrobblerTableData( + {required this.id, + required this.createdAt, + required this.username, + required this.passwordHash}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['username'] = Variable(username); + map['password_hash'] = Variable(passwordHash); + return map; + } + + ScrobblerTableCompanion toCompanion(bool nullToAbsent) { + return ScrobblerTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + username: Value(username), + passwordHash: Value(passwordHash), + ); + } + + factory ScrobblerTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ScrobblerTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + username: serializer.fromJson(json['username']), + passwordHash: serializer.fromJson(json['passwordHash']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'username': serializer.toJson(username), + 'passwordHash': serializer.toJson(passwordHash), + }; + } + + ScrobblerTableData copyWith( + {int? id, + DateTime? createdAt, + String? username, + String? passwordHash}) => + ScrobblerTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + ScrobblerTableData copyWithCompanion(ScrobblerTableCompanion data) { + return ScrobblerTableData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + username: data.username.present ? data.username.value : this.username, + passwordHash: data.passwordHash.present + ? data.passwordHash.value + : this.passwordHash, + ); + } + + @override + String toString() { + return (StringBuffer('ScrobblerTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, username, passwordHash); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScrobblerTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.username == this.username && + other.passwordHash == this.passwordHash); +} + +class ScrobblerTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value username; + final Value passwordHash; + const ScrobblerTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.username = const Value.absent(), + this.passwordHash = const Value.absent(), + }); + ScrobblerTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required String username, + required String passwordHash, + }) : username = Value(username), + passwordHash = Value(passwordHash); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? username, + Expression? passwordHash, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (username != null) 'username': username, + if (passwordHash != null) 'password_hash': passwordHash, + }); + } + + ScrobblerTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? username, + Value? passwordHash}) { + return ScrobblerTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (passwordHash.present) { + map['password_hash'] = Variable(passwordHash.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ScrobblerTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } +} + +class SkipSegmentTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SkipSegmentTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn start = GeneratedColumn( + 'start', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn end = GeneratedColumn( + 'end', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [id, start, end, trackId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'skip_segment_table'; + @override + Set get $primaryKey => {id}; + @override + SkipSegmentTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SkipSegmentTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + start: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}start'])!, + end: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}end'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SkipSegmentTable createAlias(String alias) { + return SkipSegmentTable(attachedDatabase, alias); + } +} + +class SkipSegmentTableData extends DataClass + implements Insertable { + final int id; + final int start; + final int end; + final String trackId; + final DateTime createdAt; + const SkipSegmentTableData( + {required this.id, + required this.start, + required this.end, + required this.trackId, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['start'] = Variable(start); + map['end'] = Variable(end); + map['track_id'] = Variable(trackId); + map['created_at'] = Variable(createdAt); + return map; + } + + SkipSegmentTableCompanion toCompanion(bool nullToAbsent) { + return SkipSegmentTableCompanion( + id: Value(id), + start: Value(start), + end: Value(end), + trackId: Value(trackId), + createdAt: Value(createdAt), + ); + } + + factory SkipSegmentTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SkipSegmentTableData( + id: serializer.fromJson(json['id']), + start: serializer.fromJson(json['start']), + end: serializer.fromJson(json['end']), + trackId: serializer.fromJson(json['trackId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'start': serializer.toJson(start), + 'end': serializer.toJson(end), + 'trackId': serializer.toJson(trackId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SkipSegmentTableData copyWith( + {int? id, + int? start, + int? end, + String? trackId, + DateTime? createdAt}) => + SkipSegmentTableData( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + SkipSegmentTableData copyWithCompanion(SkipSegmentTableCompanion data) { + return SkipSegmentTableData( + id: data.id.present ? data.id.value : this.id, + start: data.start.present ? data.start.value : this.start, + end: data.end.present ? data.end.value : this.end, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SkipSegmentTableData(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, start, end, trackId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SkipSegmentTableData && + other.id == this.id && + other.start == this.start && + other.end == this.end && + other.trackId == this.trackId && + other.createdAt == this.createdAt); +} + +class SkipSegmentTableCompanion extends UpdateCompanion { + final Value id; + final Value start; + final Value end; + final Value trackId; + final Value createdAt; + const SkipSegmentTableCompanion({ + this.id = const Value.absent(), + this.start = const Value.absent(), + this.end = const Value.absent(), + this.trackId = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SkipSegmentTableCompanion.insert({ + this.id = const Value.absent(), + required int start, + required int end, + required String trackId, + this.createdAt = const Value.absent(), + }) : start = Value(start), + end = Value(end), + trackId = Value(trackId); + static Insertable custom({ + Expression? id, + Expression? start, + Expression? end, + Expression? trackId, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (start != null) 'start': start, + if (end != null) 'end': end, + if (trackId != null) 'track_id': trackId, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SkipSegmentTableCompanion copyWith( + {Value? id, + Value? start, + Value? end, + Value? trackId, + Value? createdAt}) { + return SkipSegmentTableCompanion( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (start.present) { + map['start'] = Variable(start.value); + } + if (end.present) { + map['end'] = Variable(end.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SkipSegmentTableCompanion(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class SourceMatchTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SourceMatchTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn sourceInfo = GeneratedColumn( + 'source_info', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [id, trackId, sourceInfo, sourceType, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'source_match_table'; + @override + Set get $primaryKey => {id}; + @override + SourceMatchTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SourceMatchTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + sourceInfo: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_info'])!, + sourceType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_type'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SourceMatchTable createAlias(String alias) { + return SourceMatchTable(attachedDatabase, alias); + } +} + +class SourceMatchTableData extends DataClass + implements Insertable { + final int id; + final String trackId; + final String sourceInfo; + final String sourceType; + final DateTime createdAt; + const SourceMatchTableData( + {required this.id, + required this.trackId, + required this.sourceInfo, + required this.sourceType, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + map['source_info'] = Variable(sourceInfo); + map['source_type'] = Variable(sourceType); + map['created_at'] = Variable(createdAt); + return map; + } + + SourceMatchTableCompanion toCompanion(bool nullToAbsent) { + return SourceMatchTableCompanion( + id: Value(id), + trackId: Value(trackId), + sourceInfo: Value(sourceInfo), + sourceType: Value(sourceType), + createdAt: Value(createdAt), + ); + } + + factory SourceMatchTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SourceMatchTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + sourceInfo: serializer.fromJson(json['sourceInfo']), + sourceType: serializer.fromJson(json['sourceType']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'sourceInfo': serializer.toJson(sourceInfo), + 'sourceType': serializer.toJson(sourceType), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SourceMatchTableData copyWith( + {int? id, + String? trackId, + String? sourceInfo, + String? sourceType, + DateTime? createdAt}) => + SourceMatchTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceInfo: sourceInfo ?? this.sourceInfo, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + SourceMatchTableData copyWithCompanion(SourceMatchTableCompanion data) { + return SourceMatchTableData( + id: data.id.present ? data.id.value : this.id, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + sourceInfo: + data.sourceInfo.present ? data.sourceInfo.value : this.sourceInfo, + sourceType: + data.sourceType.present ? data.sourceType.value : this.sourceType, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SourceMatchTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceInfo: $sourceInfo, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, trackId, sourceInfo, sourceType, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SourceMatchTableData && + other.id == this.id && + other.trackId == this.trackId && + other.sourceInfo == this.sourceInfo && + other.sourceType == this.sourceType && + other.createdAt == this.createdAt); +} + +class SourceMatchTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value sourceInfo; + final Value sourceType; + final Value createdAt; + const SourceMatchTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.sourceInfo = const Value.absent(), + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SourceMatchTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required String sourceInfo, + required String sourceType, + this.createdAt = const Value.absent(), + }) : trackId = Value(trackId), + sourceInfo = Value(sourceInfo), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? sourceInfo, + Expression? sourceType, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (sourceInfo != null) 'source_info': sourceInfo, + if (sourceType != null) 'source_type': sourceType, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SourceMatchTableCompanion copyWith( + {Value? id, + Value? trackId, + Value? sourceInfo, + Value? sourceType, + Value? createdAt}) { + return SourceMatchTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceInfo: sourceInfo ?? this.sourceInfo, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (sourceInfo.present) { + map['source_info'] = Variable(sourceInfo.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SourceMatchTableCompanion(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceInfo: $sourceInfo, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class AudioPlayerStateTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AudioPlayerStateTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn playing = GeneratedColumn( + 'playing', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))')); + late final GeneratedColumn loopMode = GeneratedColumn( + 'loop_mode', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn shuffled = GeneratedColumn( + 'shuffled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))')); + late final GeneratedColumn collections = GeneratedColumn( + 'collections', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn tracks = GeneratedColumn( + 'tracks', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("[]")); + late final GeneratedColumn currentIndex = GeneratedColumn( + 'current_index', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + @override + List get $columns => + [id, playing, loopMode, shuffled, collections, tracks, currentIndex]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'audio_player_state_table'; + @override + Set get $primaryKey => {id}; + @override + AudioPlayerStateTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AudioPlayerStateTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + playing: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}playing'])!, + loopMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}loop_mode'])!, + shuffled: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}shuffled'])!, + collections: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}collections'])!, + tracks: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}tracks'])!, + currentIndex: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}current_index'])!, + ); + } + + @override + AudioPlayerStateTable createAlias(String alias) { + return AudioPlayerStateTable(attachedDatabase, alias); + } +} + +class AudioPlayerStateTableData extends DataClass + implements Insertable { + final int id; + final bool playing; + final String loopMode; + final bool shuffled; + final String collections; + final String tracks; + final int currentIndex; + const AudioPlayerStateTableData( + {required this.id, + required this.playing, + required this.loopMode, + required this.shuffled, + required this.collections, + required this.tracks, + required this.currentIndex}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['playing'] = Variable(playing); + map['loop_mode'] = Variable(loopMode); + map['shuffled'] = Variable(shuffled); + map['collections'] = Variable(collections); + map['tracks'] = Variable(tracks); + map['current_index'] = Variable(currentIndex); + return map; + } + + AudioPlayerStateTableCompanion toCompanion(bool nullToAbsent) { + return AudioPlayerStateTableCompanion( + id: Value(id), + playing: Value(playing), + loopMode: Value(loopMode), + shuffled: Value(shuffled), + collections: Value(collections), + tracks: Value(tracks), + currentIndex: Value(currentIndex), + ); + } + + factory AudioPlayerStateTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AudioPlayerStateTableData( + id: serializer.fromJson(json['id']), + playing: serializer.fromJson(json['playing']), + loopMode: serializer.fromJson(json['loopMode']), + shuffled: serializer.fromJson(json['shuffled']), + collections: serializer.fromJson(json['collections']), + tracks: serializer.fromJson(json['tracks']), + currentIndex: serializer.fromJson(json['currentIndex']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'playing': serializer.toJson(playing), + 'loopMode': serializer.toJson(loopMode), + 'shuffled': serializer.toJson(shuffled), + 'collections': serializer.toJson(collections), + 'tracks': serializer.toJson(tracks), + 'currentIndex': serializer.toJson(currentIndex), + }; + } + + AudioPlayerStateTableData copyWith( + {int? id, + bool? playing, + String? loopMode, + bool? shuffled, + String? collections, + String? tracks, + int? currentIndex}) => + AudioPlayerStateTableData( + id: id ?? this.id, + playing: playing ?? this.playing, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, + tracks: tracks ?? this.tracks, + currentIndex: currentIndex ?? this.currentIndex, + ); + AudioPlayerStateTableData copyWithCompanion( + AudioPlayerStateTableCompanion data) { + return AudioPlayerStateTableData( + id: data.id.present ? data.id.value : this.id, + playing: data.playing.present ? data.playing.value : this.playing, + loopMode: data.loopMode.present ? data.loopMode.value : this.loopMode, + shuffled: data.shuffled.present ? data.shuffled.value : this.shuffled, + collections: + data.collections.present ? data.collections.value : this.collections, + tracks: data.tracks.present ? data.tracks.value : this.tracks, + currentIndex: data.currentIndex.present + ? data.currentIndex.value + : this.currentIndex, + ); + } + + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableData(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections, ') + ..write('tracks: $tracks, ') + ..write('currentIndex: $currentIndex') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, playing, loopMode, shuffled, collections, tracks, currentIndex); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AudioPlayerStateTableData && + other.id == this.id && + other.playing == this.playing && + other.loopMode == this.loopMode && + other.shuffled == this.shuffled && + other.collections == this.collections && + other.tracks == this.tracks && + other.currentIndex == this.currentIndex); +} + +class AudioPlayerStateTableCompanion + extends UpdateCompanion { + final Value id; + final Value playing; + final Value loopMode; + final Value shuffled; + final Value collections; + final Value tracks; + final Value currentIndex; + const AudioPlayerStateTableCompanion({ + this.id = const Value.absent(), + this.playing = const Value.absent(), + this.loopMode = const Value.absent(), + this.shuffled = const Value.absent(), + this.collections = const Value.absent(), + this.tracks = const Value.absent(), + this.currentIndex = const Value.absent(), + }); + AudioPlayerStateTableCompanion.insert({ + this.id = const Value.absent(), + required bool playing, + required String loopMode, + required bool shuffled, + required String collections, + this.tracks = const Value.absent(), + this.currentIndex = const Value.absent(), + }) : playing = Value(playing), + loopMode = Value(loopMode), + shuffled = Value(shuffled), + collections = Value(collections); + static Insertable custom({ + Expression? id, + Expression? playing, + Expression? loopMode, + Expression? shuffled, + Expression? collections, + Expression? tracks, + Expression? currentIndex, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (playing != null) 'playing': playing, + if (loopMode != null) 'loop_mode': loopMode, + if (shuffled != null) 'shuffled': shuffled, + if (collections != null) 'collections': collections, + if (tracks != null) 'tracks': tracks, + if (currentIndex != null) 'current_index': currentIndex, + }); + } + + AudioPlayerStateTableCompanion copyWith( + {Value? id, + Value? playing, + Value? loopMode, + Value? shuffled, + Value? collections, + Value? tracks, + Value? currentIndex}) { + return AudioPlayerStateTableCompanion( + id: id ?? this.id, + playing: playing ?? this.playing, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, + tracks: tracks ?? this.tracks, + currentIndex: currentIndex ?? this.currentIndex, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (playing.present) { + map['playing'] = Variable(playing.value); + } + if (loopMode.present) { + map['loop_mode'] = Variable(loopMode.value); + } + if (shuffled.present) { + map['shuffled'] = Variable(shuffled.value); + } + if (collections.present) { + map['collections'] = Variable(collections.value); + } + if (tracks.present) { + map['tracks'] = Variable(tracks.value); + } + if (currentIndex.present) { + map['current_index'] = Variable(currentIndex.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableCompanion(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections, ') + ..write('tracks: $tracks, ') + ..write('currentIndex: $currentIndex') + ..write(')')) + .toString(); + } +} + +class HistoryTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + HistoryTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn itemId = GeneratedColumn( + 'item_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, type, itemId, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'history_table'; + @override + Set get $primaryKey => {id}; + @override + HistoryTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return HistoryTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + itemId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}item_id'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + ); + } + + @override + HistoryTable createAlias(String alias) { + return HistoryTable(attachedDatabase, alias); + } +} + +class HistoryTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final String type; + final String itemId; + final String data; + const HistoryTableData( + {required this.id, + required this.createdAt, + required this.type, + required this.itemId, + required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['type'] = Variable(type); + map['item_id'] = Variable(itemId); + map['data'] = Variable(data); + return map; + } + + HistoryTableCompanion toCompanion(bool nullToAbsent) { + return HistoryTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + type: Value(type), + itemId: Value(itemId), + data: Value(data), + ); + } + + factory HistoryTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return HistoryTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + type: serializer.fromJson(json['type']), + itemId: serializer.fromJson(json['itemId']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'type': serializer.toJson(type), + 'itemId': serializer.toJson(itemId), + 'data': serializer.toJson(data), + }; + } + + HistoryTableData copyWith( + {int? id, + DateTime? createdAt, + String? type, + String? itemId, + String? data}) => + HistoryTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + HistoryTableData copyWithCompanion(HistoryTableCompanion data) { + return HistoryTableData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + type: data.type.present ? data.type.value : this.type, + itemId: data.itemId.present ? data.itemId.value : this.itemId, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('HistoryTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, type, itemId, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is HistoryTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.type == this.type && + other.itemId == this.itemId && + other.data == this.data); +} + +class HistoryTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value type; + final Value itemId; + final Value data; + const HistoryTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.type = const Value.absent(), + this.itemId = const Value.absent(), + this.data = const Value.absent(), + }); + HistoryTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required String type, + required String itemId, + required String data, + }) : type = Value(type), + itemId = Value(itemId), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? type, + Expression? itemId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (type != null) 'type': type, + if (itemId != null) 'item_id': itemId, + if (data != null) 'data': data, + }); + } + + HistoryTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? type, + Value? itemId, + Value? data}) { + return HistoryTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (itemId.present) { + map['item_id'] = Variable(itemId.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('HistoryTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + +class LyricsTable extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LyricsTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, trackId, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'lyrics_table'; + @override + Set get $primaryKey => {id}; + @override + LyricsTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LyricsTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + ); + } + + @override + LyricsTable createAlias(String alias) { + return LyricsTable(attachedDatabase, alias); + } +} + +class LyricsTableData extends DataClass implements Insertable { + final int id; + final String trackId; + final String data; + const LyricsTableData( + {required this.id, required this.trackId, required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + map['data'] = Variable(data); + return map; + } + + LyricsTableCompanion toCompanion(bool nullToAbsent) { + return LyricsTableCompanion( + id: Value(id), + trackId: Value(trackId), + data: Value(data), + ); + } + + factory LyricsTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LyricsTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'data': serializer.toJson(data), + }; + } + + LyricsTableData copyWith({int? id, String? trackId, String? data}) => + LyricsTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + data: data ?? this.data, + ); + LyricsTableData copyWithCompanion(LyricsTableCompanion data) { + return LyricsTableData( + id: data.id.present ? data.id.value : this.id, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('LyricsTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, trackId, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LyricsTableData && + other.id == this.id && + other.trackId == this.trackId && + other.data == this.data); +} + +class LyricsTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value data; + const LyricsTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.data = const Value.absent(), + }); + LyricsTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required String data, + }) : trackId = Value(trackId), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (data != null) 'data': data, + }); + } + + LyricsTableCompanion copyWith( + {Value? id, Value? trackId, Value? data}) { + return LyricsTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LyricsTableCompanion(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + +class PluginsTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PluginsTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + additionalChecks: + GeneratedColumn.checkTextLength(minTextLength: 1, maxTextLength: 50), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn version = GeneratedColumn( + 'version', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn author = GeneratedColumn( + 'author', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn entryPoint = GeneratedColumn( + 'entry_point', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn apis = GeneratedColumn( + 'apis', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn abilities = GeneratedColumn( + 'abilities', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn selectedForMetadata = GeneratedColumn( + 'selected_for_metadata', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_metadata" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn selectedForAudioSource = + GeneratedColumn('selected_for_audio_source', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_audio_source" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn repository = GeneratedColumn( + 'repository', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn pluginApiVersion = GeneratedColumn( + 'plugin_api_version', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('2.0.0')); + @override + List get $columns => [ + id, + name, + description, + version, + author, + entryPoint, + apis, + abilities, + selectedForMetadata, + selectedForAudioSource, + repository, + pluginApiVersion + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'plugins_table'; + @override + Set get $primaryKey => {id}; + @override + PluginsTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PluginsTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + version: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}version'])!, + author: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}author'])!, + entryPoint: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}entry_point'])!, + apis: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}apis'])!, + abilities: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}abilities'])!, + selectedForMetadata: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}selected_for_metadata'])!, + selectedForAudioSource: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}selected_for_audio_source'])!, + repository: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}repository']), + pluginApiVersion: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}plugin_api_version'])!, + ); + } + + @override + PluginsTable createAlias(String alias) { + return PluginsTable(attachedDatabase, alias); + } +} + +class PluginsTableData extends DataClass + implements Insertable { + final int id; + final String name; + final String description; + final String version; + final String author; + final String entryPoint; + final String apis; + final String abilities; + final bool selectedForMetadata; + final bool selectedForAudioSource; + final String? repository; + final String pluginApiVersion; + const PluginsTableData( + {required this.id, + required this.name, + required this.description, + required this.version, + required this.author, + required this.entryPoint, + required this.apis, + required this.abilities, + required this.selectedForMetadata, + required this.selectedForAudioSource, + this.repository, + required this.pluginApiVersion}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['version'] = Variable(version); + map['author'] = Variable(author); + map['entry_point'] = Variable(entryPoint); + map['apis'] = Variable(apis); + map['abilities'] = Variable(abilities); + map['selected_for_metadata'] = Variable(selectedForMetadata); + map['selected_for_audio_source'] = Variable(selectedForAudioSource); + if (!nullToAbsent || repository != null) { + map['repository'] = Variable(repository); + } + map['plugin_api_version'] = Variable(pluginApiVersion); + return map; + } + + PluginsTableCompanion toCompanion(bool nullToAbsent) { + return PluginsTableCompanion( + id: Value(id), + name: Value(name), + description: Value(description), + version: Value(version), + author: Value(author), + entryPoint: Value(entryPoint), + apis: Value(apis), + abilities: Value(abilities), + selectedForMetadata: Value(selectedForMetadata), + selectedForAudioSource: Value(selectedForAudioSource), + repository: repository == null && nullToAbsent + ? const Value.absent() + : Value(repository), + pluginApiVersion: Value(pluginApiVersion), + ); + } + + factory PluginsTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PluginsTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + version: serializer.fromJson(json['version']), + author: serializer.fromJson(json['author']), + entryPoint: serializer.fromJson(json['entryPoint']), + apis: serializer.fromJson(json['apis']), + abilities: serializer.fromJson(json['abilities']), + selectedForMetadata: + serializer.fromJson(json['selectedForMetadata']), + selectedForAudioSource: + serializer.fromJson(json['selectedForAudioSource']), + repository: serializer.fromJson(json['repository']), + pluginApiVersion: serializer.fromJson(json['pluginApiVersion']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'version': serializer.toJson(version), + 'author': serializer.toJson(author), + 'entryPoint': serializer.toJson(entryPoint), + 'apis': serializer.toJson(apis), + 'abilities': serializer.toJson(abilities), + 'selectedForMetadata': serializer.toJson(selectedForMetadata), + 'selectedForAudioSource': serializer.toJson(selectedForAudioSource), + 'repository': serializer.toJson(repository), + 'pluginApiVersion': serializer.toJson(pluginApiVersion), + }; + } + + PluginsTableData copyWith( + {int? id, + String? name, + String? description, + String? version, + String? author, + String? entryPoint, + String? apis, + String? abilities, + bool? selectedForMetadata, + bool? selectedForAudioSource, + Value repository = const Value.absent(), + String? pluginApiVersion}) => + PluginsTableData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + version: version ?? this.version, + author: author ?? this.author, + entryPoint: entryPoint ?? this.entryPoint, + apis: apis ?? this.apis, + abilities: abilities ?? this.abilities, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, + repository: repository.present ? repository.value : this.repository, + pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, + ); + PluginsTableData copyWithCompanion(PluginsTableCompanion data) { + return PluginsTableData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + version: data.version.present ? data.version.value : this.version, + author: data.author.present ? data.author.value : this.author, + entryPoint: + data.entryPoint.present ? data.entryPoint.value : this.entryPoint, + apis: data.apis.present ? data.apis.value : this.apis, + abilities: data.abilities.present ? data.abilities.value : this.abilities, + selectedForMetadata: data.selectedForMetadata.present + ? data.selectedForMetadata.value + : this.selectedForMetadata, + selectedForAudioSource: data.selectedForAudioSource.present + ? data.selectedForAudioSource.value + : this.selectedForAudioSource, + repository: + data.repository.present ? data.repository.value : this.repository, + pluginApiVersion: data.pluginApiVersion.present + ? data.pluginApiVersion.value + : this.pluginApiVersion, + ); + } + + @override + String toString() { + return (StringBuffer('PluginsTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('version: $version, ') + ..write('author: $author, ') + ..write('entryPoint: $entryPoint, ') + ..write('apis: $apis, ') + ..write('abilities: $abilities, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') + ..write('repository: $repository, ') + ..write('pluginApiVersion: $pluginApiVersion') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + version, + author, + entryPoint, + apis, + abilities, + selectedForMetadata, + selectedForAudioSource, + repository, + pluginApiVersion); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PluginsTableData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.version == this.version && + other.author == this.author && + other.entryPoint == this.entryPoint && + other.apis == this.apis && + other.abilities == this.abilities && + other.selectedForMetadata == this.selectedForMetadata && + other.selectedForAudioSource == this.selectedForAudioSource && + other.repository == this.repository && + other.pluginApiVersion == this.pluginApiVersion); +} + +class PluginsTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value version; + final Value author; + final Value entryPoint; + final Value apis; + final Value abilities; + final Value selectedForMetadata; + final Value selectedForAudioSource; + final Value repository; + final Value pluginApiVersion; + const PluginsTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.version = const Value.absent(), + this.author = const Value.absent(), + this.entryPoint = const Value.absent(), + this.apis = const Value.absent(), + this.abilities = const Value.absent(), + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), + this.repository = const Value.absent(), + this.pluginApiVersion = const Value.absent(), + }); + PluginsTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required String description, + required String version, + required String author, + required String entryPoint, + required String apis, + required String abilities, + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), + this.repository = const Value.absent(), + this.pluginApiVersion = const Value.absent(), + }) : name = Value(name), + description = Value(description), + version = Value(version), + author = Value(author), + entryPoint = Value(entryPoint), + apis = Value(apis), + abilities = Value(abilities); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? version, + Expression? author, + Expression? entryPoint, + Expression? apis, + Expression? abilities, + Expression? selectedForMetadata, + Expression? selectedForAudioSource, + Expression? repository, + Expression? pluginApiVersion, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (version != null) 'version': version, + if (author != null) 'author': author, + if (entryPoint != null) 'entry_point': entryPoint, + if (apis != null) 'apis': apis, + if (abilities != null) 'abilities': abilities, + if (selectedForMetadata != null) + 'selected_for_metadata': selectedForMetadata, + if (selectedForAudioSource != null) + 'selected_for_audio_source': selectedForAudioSource, + if (repository != null) 'repository': repository, + if (pluginApiVersion != null) 'plugin_api_version': pluginApiVersion, + }); + } + + PluginsTableCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value? version, + Value? author, + Value? entryPoint, + Value? apis, + Value? abilities, + Value? selectedForMetadata, + Value? selectedForAudioSource, + Value? repository, + Value? pluginApiVersion}) { + return PluginsTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + version: version ?? this.version, + author: author ?? this.author, + entryPoint: entryPoint ?? this.entryPoint, + apis: apis ?? this.apis, + abilities: abilities ?? this.abilities, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, + repository: repository ?? this.repository, + pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (version.present) { + map['version'] = Variable(version.value); + } + if (author.present) { + map['author'] = Variable(author.value); + } + if (entryPoint.present) { + map['entry_point'] = Variable(entryPoint.value); + } + if (apis.present) { + map['apis'] = Variable(apis.value); + } + if (abilities.present) { + map['abilities'] = Variable(abilities.value); + } + if (selectedForMetadata.present) { + map['selected_for_metadata'] = Variable(selectedForMetadata.value); + } + if (selectedForAudioSource.present) { + map['selected_for_audio_source'] = + Variable(selectedForAudioSource.value); + } + if (repository.present) { + map['repository'] = Variable(repository.value); + } + if (pluginApiVersion.present) { + map['plugin_api_version'] = Variable(pluginApiVersion.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PluginsTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('version: $version, ') + ..write('author: $author, ') + ..write('entryPoint: $entryPoint, ') + ..write('apis: $apis, ') + ..write('abilities: $abilities, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') + ..write('repository: $repository, ') + ..write('pluginApiVersion: $pluginApiVersion') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV10 extends GeneratedDatabase { + DatabaseAtV10(QueryExecutor e) : super(e); + late final AuthenticationTable authenticationTable = + AuthenticationTable(this); + late final BlacklistTable blacklistTable = BlacklistTable(this); + late final PreferencesTable preferencesTable = PreferencesTable(this); + late final ScrobblerTable scrobblerTable = ScrobblerTable(this); + late final SkipSegmentTable skipSegmentTable = SkipSegmentTable(this); + late final SourceMatchTable sourceMatchTable = SourceMatchTable(this); + late final AudioPlayerStateTable audioPlayerStateTable = + AudioPlayerStateTable(this); + late final HistoryTable historyTable = HistoryTable(this); + late final LyricsTable lyricsTable = LyricsTable(this); + late final PluginsTable pluginsTable = PluginsTable(this); + late final Index uniqueBlacklist = Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); + late final Index uniqTrackMatch = Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_info, source_type)'); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + authenticationTable, + blacklistTable, + preferencesTable, + scrobblerTable, + skipSegmentTable, + sourceMatchTable, + audioPlayerStateTable, + historyTable, + lyricsTable, + pluginsTable, + uniqueBlacklist, + uniqTrackMatch + ]; + @override + int get schemaVersion => 10; +} From 63118319021e5b9e35b023792122345aad74e628 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Nov 2025 19:33:47 +0600 Subject: [PATCH 05/14] feat: move away from track source query and preferences audio quality and codec --- drift_schemas/app_db/drift_schema_v10.json | 1 + .../dialogs/track_details_dialog.dart | 11 +- .../presentation_actions.dart | 19 +- lib/models/database/database.dart | 9 + lib/models/database/database.g.dart | 602 +++---------- lib/models/database/database.steps.dart | 307 ++++++- lib/models/database/tables/preferences.dart | 6 - lib/models/database/tables/source_match.dart | 2 +- lib/models/metadata/audio_source.dart | 22 +- lib/models/metadata/metadata.dart | 1 + lib/models/metadata/metadata.freezed.dart | 62 +- lib/models/metadata/metadata.g.dart | 4 +- lib/models/playback/track_sources.dart | 1 - .../playback/track_sources.freezed.dart | 800 ------------------ lib/models/playback/track_sources.g.dart | 99 +-- .../local_folder/cache_export_dialog.dart | 5 +- lib/modules/player/player.dart | 56 +- lib/modules/player/sibling_tracks_sheet.dart | 347 ++------ .../getting_started/sections/playback.dart | 123 ++- lib/pages/settings/sections/playback.dart | 439 ++-------- lib/provider/audio_player/audio_player.dart | 11 +- .../audio_player/audio_player_streams.dart | 7 +- .../audio_player/querying_track_info.dart | 12 +- .../sources/invidious_instances_provider.dart | 12 - .../sources/piped_instances_provider.dart | 17 - lib/provider/download_manager_provider.dart | 71 +- .../audio_source/quality_label.dart | 12 + .../audio_source/quality_presets.dart | 120 +++ .../audio_source/quality_presets.freezed.dart | 289 +++++++ .../audio_source/quality_presets.g.dart | 38 + lib/provider/server/active_track_sources.dart | 17 +- lib/provider/server/routes/playback.dart | 49 +- .../server/sourced_track_provider.dart | 49 ++ lib/provider/server/track_sources.dart | 48 -- lib/provider/skip_segments/skip_segments.dart | 14 +- .../user_preferences_provider.dart | 61 -- lib/services/audio_player/audio_player.dart | 25 +- lib/services/sourced_track/exceptions.dart | 6 +- lib/services/sourced_track/sourced_track.dart | 55 +- lib/utils/service_utils.dart | 91 -- pubspec.lock | 32 - pubspec.yaml | 3 - test/drift/app_db/generated/schema_v10.dart | 73 +- 43 files changed, 1352 insertions(+), 2676 deletions(-) create mode 100644 drift_schemas/app_db/drift_schema_v10.json delete mode 100644 lib/models/playback/track_sources.freezed.dart delete mode 100644 lib/provider/audio_player/sources/invidious_instances_provider.dart delete mode 100644 lib/provider/audio_player/sources/piped_instances_provider.dart create mode 100644 lib/provider/metadata_plugin/audio_source/quality_label.dart create mode 100644 lib/provider/metadata_plugin/audio_source/quality_presets.dart create mode 100644 lib/provider/metadata_plugin/audio_source/quality_presets.freezed.dart create mode 100644 lib/provider/metadata_plugin/audio_source/quality_presets.g.dart create mode 100644 lib/provider/server/sourced_track_provider.dart delete mode 100644 lib/provider/server/track_sources.dart diff --git a/drift_schemas/app_db/drift_schema_v10.json b/drift_schemas/app_db/drift_schema_v10.json new file mode 100644 index 00000000..5fb86d25 --- /dev/null +++ b/drift_schemas/app_db/drift_schema_v10.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"authentication_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"cookie","getter_name":"cookie","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"access_token","getter_name":"accessToken","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"expiration","getter_name":"expiration","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"blacklist_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"element_type","getter_name":"elementType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BlacklistedType.values)","dart_type_name":"BlacklistedType"}},{"name":"element_id","getter_name":"elementId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"preferences_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"album_color_sync","getter_name":"albumColorSync","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"album_color_sync\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"album_color_sync\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"amoled_dark_theme","getter_name":"amoledDarkTheme","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"amoled_dark_theme\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"amoled_dark_theme\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"check_update","getter_name":"checkUpdate","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"check_update\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"check_update\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"normalize_audio","getter_name":"normalizeAudio","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"normalize_audio\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"normalize_audio\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"show_system_tray_icon","getter_name":"showSystemTrayIcon","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"show_system_tray_icon\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"show_system_tray_icon\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"system_title_bar","getter_name":"systemTitleBar","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"system_title_bar\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"system_title_bar\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"skip_non_music","getter_name":"skipNonMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"skip_non_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"skip_non_music\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"close_behavior","getter_name":"closeBehavior","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(CloseBehavior.close.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(CloseBehavior.values)","dart_type_name":"CloseBehavior"}},{"name":"accent_color_scheme","getter_name":"accentColorScheme","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"Slate:0xff64748b\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeColorConverter()","dart_type_name":"SpotubeColor"}},{"name":"layout_mode","getter_name":"layoutMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(LayoutMode.adaptive.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(LayoutMode.values)","dart_type_name":"LayoutMode"}},{"name":"locale","getter_name":"locale","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('{\"languageCode\":\"system\",\"countryCode\":\"system\"}')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const LocaleConverter()","dart_type_name":"Locale"}},{"name":"market","getter_name":"market","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(Market.US.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(Market.values)","dart_type_name":"Market"}},{"name":"search_mode","getter_name":"searchMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SearchMode.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SearchMode.values)","dart_type_name":"SearchMode"}},{"name":"download_location","getter_name":"downloadLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[]},{"name":"local_library_location","getter_name":"localLibraryLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"theme_mode","getter_name":"themeMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(ThemeMode.system.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ThemeMode.values)","dart_type_name":"ThemeMode"}},{"name":"audio_source_id","getter_name":"audioSourceId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"youtube_client_engine","getter_name":"youtubeClientEngine","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(YoutubeClientEngine.youtubeExplode.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(YoutubeClientEngine.values)","dart_type_name":"YoutubeClientEngine"}},{"name":"discord_presence","getter_name":"discordPresence","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"discord_presence\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"discord_presence\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"endless_playback","getter_name":"endlessPlayback","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"endless_playback\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"endless_playback\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"enable_connect","getter_name":"enableConnect","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"enable_connect\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"enable_connect\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"connect_port","getter_name":"connectPort","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(-1)","default_client_dart":null,"dsl_features":[]},{"name":"cache_music","getter_name":"cacheMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"cache_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"cache_music\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":3,"references":[],"type":"table","data":{"name":"scrobbler_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"username","getter_name":"username","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"password_hash","getter_name":"passwordHash","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":4,"references":[],"type":"table","data":{"name":"skip_segment_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"start","getter_name":"start","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"end","getter_name":"end","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":5,"references":[],"type":"table","data":{"name":"source_match_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_info","getter_name":"sourceInfo","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"{}\")","default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":6,"references":[],"type":"table","data":{"name":"audio_player_state_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"playing","getter_name":"playing","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"playing\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"playing\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"loop_mode","getter_name":"loopMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(PlaylistMode.values)","dart_type_name":"PlaylistMode"}},{"name":"shuffled","getter_name":"shuffled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"shuffled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"shuffled\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"collections","getter_name":"collections","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"tracks","getter_name":"tracks","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"[]\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeTrackObjectListConverter()","dart_type_name":"List"}},{"name":"current_index","getter_name":"currentIndex","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":7,"references":[],"type":"table","data":{"name":"history_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(HistoryEntryType.values)","dart_type_name":"HistoryEntryType"}},{"name":"item_id","getter_name":"itemId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MapTypeConverter()","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":8,"references":[],"type":"table","data":{"name":"lyrics_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"SubtitleTypeConverter()","dart_type_name":"SubtitleSimple"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":9,"references":[],"type":"table","data":{"name":"plugins_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":50}}]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"version","getter_name":"version","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"author","getter_name":"author","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"entry_point","getter_name":"entryPoint","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"apis","getter_name":"apis","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"abilities","getter_name":"abilities","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"selected_for_metadata","getter_name":"selectedForMetadata","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"selected_for_metadata\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"selected_for_metadata\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"selected_for_audio_source","getter_name":"selectedForAudioSource","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"selected_for_audio_source\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"selected_for_audio_source\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"repository","getter_name":"repository","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"plugin_api_version","getter_name":"pluginApiVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('2.0.0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"unique_blacklist","sql":null,"unique":true,"columns":["element_type","element_id"]}},{"id":11,"references":[5],"type":"index","data":{"on":5,"name":"uniq_track_match","sql":null,"unique":true,"columns":["track_id","source_info","source_type"]}}]} \ No newline at end of file diff --git a/lib/components/dialogs/track_details_dialog.dart b/lib/components/dialogs/track_details_dialog.dart index 3d3fd7e9..9d35a6fb 100644 --- a/lib/components/dialogs/track_details_dialog.dart +++ b/lib/components/dialogs/track_details_dialog.dart @@ -7,8 +7,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; class TrackDetailsDialog extends HookConsumerWidget { final SpotubeFullTrackObject track; @@ -21,8 +20,7 @@ class TrackDetailsDialog extends HookConsumerWidget { Widget build(BuildContext context, ref) { final theme = Theme.of(context); final mediaQuery = MediaQuery.of(context); - final sourcedTrack = - ref.read(trackSourcesProvider(TrackSourceQuery.fromTrack(track))); + final sourcedTrack = ref.read(sourcedTrackProvider(track)); final detailsMap = { context.l10n.title: track.name, @@ -39,8 +37,7 @@ class TrackDetailsDialog extends HookConsumerWidget { // style: const TextStyle(color: Colors.blue), // ), context.l10n.duration: sourcedTrack.asData != null - ? Duration(milliseconds: sourcedTrack.asData!.value.info.durationMs) - .toHumanReadableString() + ? sourcedTrack.asData!.value.info.duration.toHumanReadableString() : Duration(milliseconds: track.durationMs).toHumanReadableString(), if (track.album.releaseDate != null) context.l10n.released: track.album.releaseDate, @@ -57,7 +54,7 @@ class TrackDetailsDialog extends HookConsumerWidget { maxLines: 2, overflow: TextOverflow.ellipsis, ), - context.l10n.channel: Text(sourceInfo.artists), + context.l10n.channel: Text(sourceInfo.artists.join(", ")), if (sourcedTrack.asData?.value.url != null) context.l10n.streamUrl: Hyperlink( sourcedTrack.asData!.value.url ?? "", diff --git a/lib/components/track_presentation/presentation_actions.dart b/lib/components/track_presentation/presentation_actions.dart index 735a4514..54aa3428 100644 --- a/lib/components/track_presentation/presentation_actions.dart +++ b/lib/components/track_presentation/presentation_actions.dart @@ -8,12 +8,10 @@ import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_state.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; ToastOverlay showToastForAction( BuildContext context, @@ -70,8 +68,6 @@ class TrackPresentationActionsSection extends HookConsumerWidget { final downloader = ref.watch(downloadManagerProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.watch(playbackHistoryActionsProvider); - final audioSource = - ref.watch(userPreferencesProvider.select((s) => s.audioSource)); final state = ref.watch(presentationStateProvider(options.collection)); final notifier = @@ -85,14 +81,13 @@ class TrackPresentationActionsSection extends HookConsumerWidget { }) async { final fullTrackObjects = tracks.whereType().toList(); - final confirmed = audioSource == AudioSource.piped || - (await showDialog( - context: context, - builder: (context) { - return const ConfirmDownloadDialog(); - }, - ) ?? - false); + final confirmed = await showDialog( + context: context, + builder: (context) { + return const ConfirmDownloadDialog(); + }, + ) ?? + false; if (confirmed != true) return; downloader.batchAddToQueue(fullTrackObjects); notifier.deselectAllTracks(); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index a03cdb8c..786b813f 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -210,6 +210,15 @@ class AppDatabase extends _$AppDatabase { pluginsTable.selectedForAudioSource, ); }, + from9To10: (m, schema) async { + await m.dropColumn(schema.preferencesTable, "piped_instance"); + await m.dropColumn(schema.preferencesTable, "invidious_instance"); + await m.addColumn( + schema.sourceMatchTable, + sourceMatchTable.sourceInfo, + ); + await m.dropColumn(schema.sourceMatchTable, "source_id"); + }, ), ); } diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 70f6aa26..4a9a7eba 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -558,15 +558,6 @@ class $PreferencesTableTable extends PreferencesTable requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - @override - late final GeneratedColumnWithTypeConverter - audioQuality = GeneratedColumn( - 'audio_quality', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SourceQualities.high.name)) - .withConverter( - $PreferencesTableTable.$converteraudioQuality); static const VerificationMeta _albumColorSyncMeta = const VerificationMeta('albumColorSync'); @override @@ -703,22 +694,6 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: const Constant("")) .withConverter>( $PreferencesTableTable.$converterlocalLibraryLocation); - static const VerificationMeta _pipedInstanceMeta = - const VerificationMeta('pipedInstance'); - @override - late final GeneratedColumn pipedInstance = GeneratedColumn( - 'piped_instance', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("https://pipedapi.kavin.rocks")); - static const VerificationMeta _invidiousInstanceMeta = - const VerificationMeta('invidiousInstance'); - @override - late final GeneratedColumn invidiousInstance = - GeneratedColumn('invidious_instance', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("https://inv.nadeko.net")); @override late final GeneratedColumnWithTypeConverter themeMode = GeneratedColumn('theme_mode', aliasedName, false, @@ -726,14 +701,12 @@ class $PreferencesTableTable extends PreferencesTable requiredDuringInsert: false, defaultValue: Constant(ThemeMode.system.name)) .withConverter($PreferencesTableTable.$converterthemeMode); + static const VerificationMeta _audioSourceIdMeta = + const VerificationMeta('audioSourceId'); @override - late final GeneratedColumnWithTypeConverter audioSource = - GeneratedColumn('audio_source', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(AudioSource.youtube.name)) - .withConverter( - $PreferencesTableTable.$converteraudioSource); + late final GeneratedColumn audioSourceId = GeneratedColumn( + 'audio_source_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); @override late final GeneratedColumnWithTypeConverter youtubeClientEngine = GeneratedColumn( @@ -743,24 +716,6 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name)) .withConverter( $PreferencesTableTable.$converteryoutubeClientEngine); - @override - late final GeneratedColumnWithTypeConverter - streamMusicCodec = GeneratedColumn( - 'stream_music_codec', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SourceCodecs.weba.name)) - .withConverter( - $PreferencesTableTable.$converterstreamMusicCodec); - @override - late final GeneratedColumnWithTypeConverter - downloadMusicCodec = GeneratedColumn( - 'download_music_codec', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SourceCodecs.m4a.name)) - .withConverter( - $PreferencesTableTable.$converterdownloadMusicCodec); static const VerificationMeta _discordPresenceMeta = const VerificationMeta('discordPresence'); @override @@ -812,7 +767,6 @@ class $PreferencesTableTable extends PreferencesTable @override List get $columns => [ id, - audioQuality, albumColorSync, amoledDarkTheme, checkUpdate, @@ -828,13 +782,9 @@ class $PreferencesTableTable extends PreferencesTable searchMode, downloadLocation, localLibraryLocation, - pipedInstance, - invidiousInstance, themeMode, - audioSource, + audioSourceId, youtubeClientEngine, - streamMusicCodec, - downloadMusicCodec, discordPresence, endlessPlayback, enableConnect, @@ -903,17 +853,11 @@ class $PreferencesTableTable extends PreferencesTable downloadLocation.isAcceptableOrUnknown( data['download_location']!, _downloadLocationMeta)); } - if (data.containsKey('piped_instance')) { + if (data.containsKey('audio_source_id')) { context.handle( - _pipedInstanceMeta, - pipedInstance.isAcceptableOrUnknown( - data['piped_instance']!, _pipedInstanceMeta)); - } - if (data.containsKey('invidious_instance')) { - context.handle( - _invidiousInstanceMeta, - invidiousInstance.isAcceptableOrUnknown( - data['invidious_instance']!, _invidiousInstanceMeta)); + _audioSourceIdMeta, + audioSourceId.isAcceptableOrUnknown( + data['audio_source_id']!, _audioSourceIdMeta)); } if (data.containsKey('discord_presence')) { context.handle( @@ -956,9 +900,6 @@ class $PreferencesTableTable extends PreferencesTable return PreferencesTableData( id: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - audioQuality: $PreferencesTableTable.$converteraudioQuality.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}audio_quality'])!), albumColorSync: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}album_color_sync'])!, amoledDarkTheme: attachedDatabase.typeMapping.read( @@ -997,25 +938,14 @@ class $PreferencesTableTable extends PreferencesTable .$converterlocalLibraryLocation .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}local_library_location'])!), - pipedInstance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, - invidiousInstance: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}invidious_instance'])!, themeMode: $PreferencesTableTable.$converterthemeMode.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}theme_mode'])!), - audioSource: $PreferencesTableTable.$converteraudioSource.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}audio_source'])!), + audioSourceId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}audio_source_id']), youtubeClientEngine: $PreferencesTableTable.$converteryoutubeClientEngine .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}youtube_client_engine'])!), - streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec - .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}stream_music_codec'])!), - downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec - .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}download_music_codec'])!), discordPresence: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}discord_presence'])!, endlessPlayback: attachedDatabase.typeMapping @@ -1034,9 +964,6 @@ class $PreferencesTableTable extends PreferencesTable return $PreferencesTableTable(attachedDatabase, alias); } - static JsonTypeConverter2 - $converteraudioQuality = - const EnumNameConverter(SourceQualities.values); static JsonTypeConverter2 $convertercloseBehavior = const EnumNameConverter(CloseBehavior.values); @@ -1054,23 +981,14 @@ class $PreferencesTableTable extends PreferencesTable const StringListConverter(); static JsonTypeConverter2 $converterthemeMode = const EnumNameConverter(ThemeMode.values); - static JsonTypeConverter2 $converteraudioSource = - const EnumNameConverter(AudioSource.values); static JsonTypeConverter2 $converteryoutubeClientEngine = const EnumNameConverter(YoutubeClientEngine.values); - static JsonTypeConverter2 - $converterstreamMusicCodec = - const EnumNameConverter(SourceCodecs.values); - static JsonTypeConverter2 - $converterdownloadMusicCodec = - const EnumNameConverter(SourceCodecs.values); } class PreferencesTableData extends DataClass implements Insertable { final int id; - final SourceQualities audioQuality; final bool albumColorSync; final bool amoledDarkTheme; final bool checkUpdate; @@ -1086,13 +1004,9 @@ class PreferencesTableData extends DataClass final SearchMode searchMode; final String downloadLocation; final List localLibraryLocation; - final String pipedInstance; - final String invidiousInstance; final ThemeMode themeMode; - final AudioSource audioSource; + final String? audioSourceId; final YoutubeClientEngine youtubeClientEngine; - final SourceCodecs streamMusicCodec; - final SourceCodecs downloadMusicCodec; final bool discordPresence; final bool endlessPlayback; final bool enableConnect; @@ -1100,7 +1014,6 @@ class PreferencesTableData extends DataClass final bool cacheMusic; const PreferencesTableData( {required this.id, - required this.audioQuality, required this.albumColorSync, required this.amoledDarkTheme, required this.checkUpdate, @@ -1116,13 +1029,9 @@ class PreferencesTableData extends DataClass required this.searchMode, required this.downloadLocation, required this.localLibraryLocation, - required this.pipedInstance, - required this.invidiousInstance, required this.themeMode, - required this.audioSource, + this.audioSourceId, required this.youtubeClientEngine, - required this.streamMusicCodec, - required this.downloadMusicCodec, required this.discordPresence, required this.endlessPlayback, required this.enableConnect, @@ -1132,10 +1041,6 @@ class PreferencesTableData extends DataClass Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); - { - map['audio_quality'] = Variable( - $PreferencesTableTable.$converteraudioQuality.toSql(audioQuality)); - } map['album_color_sync'] = Variable(albumColorSync); map['amoled_dark_theme'] = Variable(amoledDarkTheme); map['check_update'] = Variable(checkUpdate); @@ -1174,31 +1079,18 @@ class PreferencesTableData extends DataClass .$converterlocalLibraryLocation .toSql(localLibraryLocation)); } - map['piped_instance'] = Variable(pipedInstance); - map['invidious_instance'] = Variable(invidiousInstance); { map['theme_mode'] = Variable( $PreferencesTableTable.$converterthemeMode.toSql(themeMode)); } - { - map['audio_source'] = Variable( - $PreferencesTableTable.$converteraudioSource.toSql(audioSource)); + if (!nullToAbsent || audioSourceId != null) { + map['audio_source_id'] = Variable(audioSourceId); } { map['youtube_client_engine'] = Variable($PreferencesTableTable .$converteryoutubeClientEngine .toSql(youtubeClientEngine)); } - { - map['stream_music_codec'] = Variable($PreferencesTableTable - .$converterstreamMusicCodec - .toSql(streamMusicCodec)); - } - { - map['download_music_codec'] = Variable($PreferencesTableTable - .$converterdownloadMusicCodec - .toSql(downloadMusicCodec)); - } map['discord_presence'] = Variable(discordPresence); map['endless_playback'] = Variable(endlessPlayback); map['enable_connect'] = Variable(enableConnect); @@ -1210,7 +1102,6 @@ class PreferencesTableData extends DataClass PreferencesTableCompanion toCompanion(bool nullToAbsent) { return PreferencesTableCompanion( id: Value(id), - audioQuality: Value(audioQuality), albumColorSync: Value(albumColorSync), amoledDarkTheme: Value(amoledDarkTheme), checkUpdate: Value(checkUpdate), @@ -1226,13 +1117,11 @@ class PreferencesTableData extends DataClass searchMode: Value(searchMode), downloadLocation: Value(downloadLocation), localLibraryLocation: Value(localLibraryLocation), - pipedInstance: Value(pipedInstance), - invidiousInstance: Value(invidiousInstance), themeMode: Value(themeMode), - audioSource: Value(audioSource), + audioSourceId: audioSourceId == null && nullToAbsent + ? const Value.absent() + : Value(audioSourceId), youtubeClientEngine: Value(youtubeClientEngine), - streamMusicCodec: Value(streamMusicCodec), - downloadMusicCodec: Value(downloadMusicCodec), discordPresence: Value(discordPresence), endlessPlayback: Value(endlessPlayback), enableConnect: Value(enableConnect), @@ -1246,8 +1135,6 @@ class PreferencesTableData extends DataClass serializer ??= driftRuntimeOptions.defaultSerializer; return PreferencesTableData( id: serializer.fromJson(json['id']), - audioQuality: $PreferencesTableTable.$converteraudioQuality - .fromJson(serializer.fromJson(json['audioQuality'])), albumColorSync: serializer.fromJson(json['albumColorSync']), amoledDarkTheme: serializer.fromJson(json['amoledDarkTheme']), checkUpdate: serializer.fromJson(json['checkUpdate']), @@ -1269,18 +1156,11 @@ class PreferencesTableData extends DataClass downloadLocation: serializer.fromJson(json['downloadLocation']), localLibraryLocation: serializer.fromJson>(json['localLibraryLocation']), - pipedInstance: serializer.fromJson(json['pipedInstance']), - invidiousInstance: serializer.fromJson(json['invidiousInstance']), themeMode: $PreferencesTableTable.$converterthemeMode .fromJson(serializer.fromJson(json['themeMode'])), - audioSource: $PreferencesTableTable.$converteraudioSource - .fromJson(serializer.fromJson(json['audioSource'])), + audioSourceId: serializer.fromJson(json['audioSourceId']), youtubeClientEngine: $PreferencesTableTable.$converteryoutubeClientEngine .fromJson(serializer.fromJson(json['youtubeClientEngine'])), - streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec - .fromJson(serializer.fromJson(json['streamMusicCodec'])), - downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec - .fromJson(serializer.fromJson(json['downloadMusicCodec'])), discordPresence: serializer.fromJson(json['discordPresence']), endlessPlayback: serializer.fromJson(json['endlessPlayback']), enableConnect: serializer.fromJson(json['enableConnect']), @@ -1293,8 +1173,6 @@ class PreferencesTableData extends DataClass serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), - 'audioQuality': serializer.toJson( - $PreferencesTableTable.$converteraudioQuality.toJson(audioQuality)), 'albumColorSync': serializer.toJson(albumColorSync), 'amoledDarkTheme': serializer.toJson(amoledDarkTheme), 'checkUpdate': serializer.toJson(checkUpdate), @@ -1315,21 +1193,12 @@ class PreferencesTableData extends DataClass 'downloadLocation': serializer.toJson(downloadLocation), 'localLibraryLocation': serializer.toJson>(localLibraryLocation), - 'pipedInstance': serializer.toJson(pipedInstance), - 'invidiousInstance': serializer.toJson(invidiousInstance), 'themeMode': serializer.toJson( $PreferencesTableTable.$converterthemeMode.toJson(themeMode)), - 'audioSource': serializer.toJson( - $PreferencesTableTable.$converteraudioSource.toJson(audioSource)), + 'audioSourceId': serializer.toJson(audioSourceId), 'youtubeClientEngine': serializer.toJson($PreferencesTableTable .$converteryoutubeClientEngine .toJson(youtubeClientEngine)), - 'streamMusicCodec': serializer.toJson($PreferencesTableTable - .$converterstreamMusicCodec - .toJson(streamMusicCodec)), - 'downloadMusicCodec': serializer.toJson($PreferencesTableTable - .$converterdownloadMusicCodec - .toJson(downloadMusicCodec)), 'discordPresence': serializer.toJson(discordPresence), 'endlessPlayback': serializer.toJson(endlessPlayback), 'enableConnect': serializer.toJson(enableConnect), @@ -1340,7 +1209,6 @@ class PreferencesTableData extends DataClass PreferencesTableData copyWith( {int? id, - SourceQualities? audioQuality, bool? albumColorSync, bool? amoledDarkTheme, bool? checkUpdate, @@ -1356,13 +1224,9 @@ class PreferencesTableData extends DataClass SearchMode? searchMode, String? downloadLocation, List? localLibraryLocation, - String? pipedInstance, - String? invidiousInstance, ThemeMode? themeMode, - AudioSource? audioSource, + Value audioSourceId = const Value.absent(), YoutubeClientEngine? youtubeClientEngine, - SourceCodecs? streamMusicCodec, - SourceCodecs? downloadMusicCodec, bool? discordPresence, bool? endlessPlayback, bool? enableConnect, @@ -1370,7 +1234,6 @@ class PreferencesTableData extends DataClass bool? cacheMusic}) => PreferencesTableData( id: id ?? this.id, - audioQuality: audioQuality ?? this.audioQuality, albumColorSync: albumColorSync ?? this.albumColorSync, amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, checkUpdate: checkUpdate ?? this.checkUpdate, @@ -1386,13 +1249,10 @@ class PreferencesTableData extends DataClass searchMode: searchMode ?? this.searchMode, downloadLocation: downloadLocation ?? this.downloadLocation, localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, - pipedInstance: pipedInstance ?? this.pipedInstance, - invidiousInstance: invidiousInstance ?? this.invidiousInstance, themeMode: themeMode ?? this.themeMode, - audioSource: audioSource ?? this.audioSource, + audioSourceId: + audioSourceId.present ? audioSourceId.value : this.audioSourceId, youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, - streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, - downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, discordPresence: discordPresence ?? this.discordPresence, endlessPlayback: endlessPlayback ?? this.endlessPlayback, enableConnect: enableConnect ?? this.enableConnect, @@ -1402,9 +1262,6 @@ class PreferencesTableData extends DataClass PreferencesTableData copyWithCompanion(PreferencesTableCompanion data) { return PreferencesTableData( id: data.id.present ? data.id.value : this.id, - audioQuality: data.audioQuality.present - ? data.audioQuality.value - : this.audioQuality, albumColorSync: data.albumColorSync.present ? data.albumColorSync.value : this.albumColorSync, @@ -1443,24 +1300,13 @@ class PreferencesTableData extends DataClass localLibraryLocation: data.localLibraryLocation.present ? data.localLibraryLocation.value : this.localLibraryLocation, - pipedInstance: data.pipedInstance.present - ? data.pipedInstance.value - : this.pipedInstance, - invidiousInstance: data.invidiousInstance.present - ? data.invidiousInstance.value - : this.invidiousInstance, themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, - audioSource: - data.audioSource.present ? data.audioSource.value : this.audioSource, + audioSourceId: data.audioSourceId.present + ? data.audioSourceId.value + : this.audioSourceId, youtubeClientEngine: data.youtubeClientEngine.present ? data.youtubeClientEngine.value : this.youtubeClientEngine, - streamMusicCodec: data.streamMusicCodec.present - ? data.streamMusicCodec.value - : this.streamMusicCodec, - downloadMusicCodec: data.downloadMusicCodec.present - ? data.downloadMusicCodec.value - : this.downloadMusicCodec, discordPresence: data.discordPresence.present ? data.discordPresence.value : this.discordPresence, @@ -1481,7 +1327,6 @@ class PreferencesTableData extends DataClass String toString() { return (StringBuffer('PreferencesTableData(') ..write('id: $id, ') - ..write('audioQuality: $audioQuality, ') ..write('albumColorSync: $albumColorSync, ') ..write('amoledDarkTheme: $amoledDarkTheme, ') ..write('checkUpdate: $checkUpdate, ') @@ -1497,13 +1342,9 @@ class PreferencesTableData extends DataClass ..write('searchMode: $searchMode, ') ..write('downloadLocation: $downloadLocation, ') ..write('localLibraryLocation: $localLibraryLocation, ') - ..write('pipedInstance: $pipedInstance, ') - ..write('invidiousInstance: $invidiousInstance, ') ..write('themeMode: $themeMode, ') - ..write('audioSource: $audioSource, ') + ..write('audioSourceId: $audioSourceId, ') ..write('youtubeClientEngine: $youtubeClientEngine, ') - ..write('streamMusicCodec: $streamMusicCodec, ') - ..write('downloadMusicCodec: $downloadMusicCodec, ') ..write('discordPresence: $discordPresence, ') ..write('endlessPlayback: $endlessPlayback, ') ..write('enableConnect: $enableConnect, ') @@ -1516,7 +1357,6 @@ class PreferencesTableData extends DataClass @override int get hashCode => Object.hashAll([ id, - audioQuality, albumColorSync, amoledDarkTheme, checkUpdate, @@ -1532,13 +1372,9 @@ class PreferencesTableData extends DataClass searchMode, downloadLocation, localLibraryLocation, - pipedInstance, - invidiousInstance, themeMode, - audioSource, + audioSourceId, youtubeClientEngine, - streamMusicCodec, - downloadMusicCodec, discordPresence, endlessPlayback, enableConnect, @@ -1550,7 +1386,6 @@ class PreferencesTableData extends DataClass identical(this, other) || (other is PreferencesTableData && other.id == this.id && - other.audioQuality == this.audioQuality && other.albumColorSync == this.albumColorSync && other.amoledDarkTheme == this.amoledDarkTheme && other.checkUpdate == this.checkUpdate && @@ -1566,13 +1401,9 @@ class PreferencesTableData extends DataClass other.searchMode == this.searchMode && other.downloadLocation == this.downloadLocation && other.localLibraryLocation == this.localLibraryLocation && - other.pipedInstance == this.pipedInstance && - other.invidiousInstance == this.invidiousInstance && other.themeMode == this.themeMode && - other.audioSource == this.audioSource && + other.audioSourceId == this.audioSourceId && other.youtubeClientEngine == this.youtubeClientEngine && - other.streamMusicCodec == this.streamMusicCodec && - other.downloadMusicCodec == this.downloadMusicCodec && other.discordPresence == this.discordPresence && other.endlessPlayback == this.endlessPlayback && other.enableConnect == this.enableConnect && @@ -1582,7 +1413,6 @@ class PreferencesTableData extends DataClass class PreferencesTableCompanion extends UpdateCompanion { final Value id; - final Value audioQuality; final Value albumColorSync; final Value amoledDarkTheme; final Value checkUpdate; @@ -1598,13 +1428,9 @@ class PreferencesTableCompanion extends UpdateCompanion { final Value searchMode; final Value downloadLocation; final Value> localLibraryLocation; - final Value pipedInstance; - final Value invidiousInstance; final Value themeMode; - final Value audioSource; + final Value audioSourceId; final Value youtubeClientEngine; - final Value streamMusicCodec; - final Value downloadMusicCodec; final Value discordPresence; final Value endlessPlayback; final Value enableConnect; @@ -1612,7 +1438,6 @@ class PreferencesTableCompanion extends UpdateCompanion { final Value cacheMusic; const PreferencesTableCompanion({ this.id = const Value.absent(), - this.audioQuality = const Value.absent(), this.albumColorSync = const Value.absent(), this.amoledDarkTheme = const Value.absent(), this.checkUpdate = const Value.absent(), @@ -1628,13 +1453,9 @@ class PreferencesTableCompanion extends UpdateCompanion { this.searchMode = const Value.absent(), this.downloadLocation = const Value.absent(), this.localLibraryLocation = const Value.absent(), - this.pipedInstance = const Value.absent(), - this.invidiousInstance = const Value.absent(), this.themeMode = const Value.absent(), - this.audioSource = const Value.absent(), + this.audioSourceId = const Value.absent(), this.youtubeClientEngine = const Value.absent(), - this.streamMusicCodec = const Value.absent(), - this.downloadMusicCodec = const Value.absent(), this.discordPresence = const Value.absent(), this.endlessPlayback = const Value.absent(), this.enableConnect = const Value.absent(), @@ -1643,7 +1464,6 @@ class PreferencesTableCompanion extends UpdateCompanion { }); PreferencesTableCompanion.insert({ this.id = const Value.absent(), - this.audioQuality = const Value.absent(), this.albumColorSync = const Value.absent(), this.amoledDarkTheme = const Value.absent(), this.checkUpdate = const Value.absent(), @@ -1659,13 +1479,9 @@ class PreferencesTableCompanion extends UpdateCompanion { this.searchMode = const Value.absent(), this.downloadLocation = const Value.absent(), this.localLibraryLocation = const Value.absent(), - this.pipedInstance = const Value.absent(), - this.invidiousInstance = const Value.absent(), this.themeMode = const Value.absent(), - this.audioSource = const Value.absent(), + this.audioSourceId = const Value.absent(), this.youtubeClientEngine = const Value.absent(), - this.streamMusicCodec = const Value.absent(), - this.downloadMusicCodec = const Value.absent(), this.discordPresence = const Value.absent(), this.endlessPlayback = const Value.absent(), this.enableConnect = const Value.absent(), @@ -1674,7 +1490,6 @@ class PreferencesTableCompanion extends UpdateCompanion { }); static Insertable custom({ Expression? id, - Expression? audioQuality, Expression? albumColorSync, Expression? amoledDarkTheme, Expression? checkUpdate, @@ -1690,13 +1505,9 @@ class PreferencesTableCompanion extends UpdateCompanion { Expression? searchMode, Expression? downloadLocation, Expression? localLibraryLocation, - Expression? pipedInstance, - Expression? invidiousInstance, Expression? themeMode, - Expression? audioSource, + Expression? audioSourceId, Expression? youtubeClientEngine, - Expression? streamMusicCodec, - Expression? downloadMusicCodec, Expression? discordPresence, Expression? endlessPlayback, Expression? enableConnect, @@ -1705,7 +1516,6 @@ class PreferencesTableCompanion extends UpdateCompanion { }) { return RawValuesInsertable({ if (id != null) 'id': id, - if (audioQuality != null) 'audio_quality': audioQuality, if (albumColorSync != null) 'album_color_sync': albumColorSync, if (amoledDarkTheme != null) 'amoled_dark_theme': amoledDarkTheme, if (checkUpdate != null) 'check_update': checkUpdate, @@ -1723,15 +1533,10 @@ class PreferencesTableCompanion extends UpdateCompanion { if (downloadLocation != null) 'download_location': downloadLocation, if (localLibraryLocation != null) 'local_library_location': localLibraryLocation, - if (pipedInstance != null) 'piped_instance': pipedInstance, - if (invidiousInstance != null) 'invidious_instance': invidiousInstance, if (themeMode != null) 'theme_mode': themeMode, - if (audioSource != null) 'audio_source': audioSource, + if (audioSourceId != null) 'audio_source_id': audioSourceId, if (youtubeClientEngine != null) 'youtube_client_engine': youtubeClientEngine, - if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec, - if (downloadMusicCodec != null) - 'download_music_codec': downloadMusicCodec, if (discordPresence != null) 'discord_presence': discordPresence, if (endlessPlayback != null) 'endless_playback': endlessPlayback, if (enableConnect != null) 'enable_connect': enableConnect, @@ -1742,7 +1547,6 @@ class PreferencesTableCompanion extends UpdateCompanion { PreferencesTableCompanion copyWith( {Value? id, - Value? audioQuality, Value? albumColorSync, Value? amoledDarkTheme, Value? checkUpdate, @@ -1758,13 +1562,9 @@ class PreferencesTableCompanion extends UpdateCompanion { Value? searchMode, Value? downloadLocation, Value>? localLibraryLocation, - Value? pipedInstance, - Value? invidiousInstance, Value? themeMode, - Value? audioSource, + Value? audioSourceId, Value? youtubeClientEngine, - Value? streamMusicCodec, - Value? downloadMusicCodec, Value? discordPresence, Value? endlessPlayback, Value? enableConnect, @@ -1772,7 +1572,6 @@ class PreferencesTableCompanion extends UpdateCompanion { Value? cacheMusic}) { return PreferencesTableCompanion( id: id ?? this.id, - audioQuality: audioQuality ?? this.audioQuality, albumColorSync: albumColorSync ?? this.albumColorSync, amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, checkUpdate: checkUpdate ?? this.checkUpdate, @@ -1788,13 +1587,9 @@ class PreferencesTableCompanion extends UpdateCompanion { searchMode: searchMode ?? this.searchMode, downloadLocation: downloadLocation ?? this.downloadLocation, localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, - pipedInstance: pipedInstance ?? this.pipedInstance, - invidiousInstance: invidiousInstance ?? this.invidiousInstance, themeMode: themeMode ?? this.themeMode, - audioSource: audioSource ?? this.audioSource, + audioSourceId: audioSourceId ?? this.audioSourceId, youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, - streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, - downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, discordPresence: discordPresence ?? this.discordPresence, endlessPlayback: endlessPlayback ?? this.endlessPlayback, enableConnect: enableConnect ?? this.enableConnect, @@ -1809,11 +1604,6 @@ class PreferencesTableCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } - if (audioQuality.present) { - map['audio_quality'] = Variable($PreferencesTableTable - .$converteraudioQuality - .toSql(audioQuality.value)); - } if (albumColorSync.present) { map['album_color_sync'] = Variable(albumColorSync.value); } @@ -1869,36 +1659,18 @@ class PreferencesTableCompanion extends UpdateCompanion { .$converterlocalLibraryLocation .toSql(localLibraryLocation.value)); } - if (pipedInstance.present) { - map['piped_instance'] = Variable(pipedInstance.value); - } - if (invidiousInstance.present) { - map['invidious_instance'] = Variable(invidiousInstance.value); - } if (themeMode.present) { map['theme_mode'] = Variable( $PreferencesTableTable.$converterthemeMode.toSql(themeMode.value)); } - if (audioSource.present) { - map['audio_source'] = Variable($PreferencesTableTable - .$converteraudioSource - .toSql(audioSource.value)); + if (audioSourceId.present) { + map['audio_source_id'] = Variable(audioSourceId.value); } if (youtubeClientEngine.present) { map['youtube_client_engine'] = Variable($PreferencesTableTable .$converteryoutubeClientEngine .toSql(youtubeClientEngine.value)); } - if (streamMusicCodec.present) { - map['stream_music_codec'] = Variable($PreferencesTableTable - .$converterstreamMusicCodec - .toSql(streamMusicCodec.value)); - } - if (downloadMusicCodec.present) { - map['download_music_codec'] = Variable($PreferencesTableTable - .$converterdownloadMusicCodec - .toSql(downloadMusicCodec.value)); - } if (discordPresence.present) { map['discord_presence'] = Variable(discordPresence.value); } @@ -1921,7 +1693,6 @@ class PreferencesTableCompanion extends UpdateCompanion { String toString() { return (StringBuffer('PreferencesTableCompanion(') ..write('id: $id, ') - ..write('audioQuality: $audioQuality, ') ..write('albumColorSync: $albumColorSync, ') ..write('amoledDarkTheme: $amoledDarkTheme, ') ..write('checkUpdate: $checkUpdate, ') @@ -1937,13 +1708,9 @@ class PreferencesTableCompanion extends UpdateCompanion { ..write('searchMode: $searchMode, ') ..write('downloadLocation: $downloadLocation, ') ..write('localLibraryLocation: $localLibraryLocation, ') - ..write('pipedInstance: $pipedInstance, ') - ..write('invidiousInstance: $invidiousInstance, ') ..write('themeMode: $themeMode, ') - ..write('audioSource: $audioSource, ') + ..write('audioSourceId: $audioSourceId, ') ..write('youtubeClientEngine: $youtubeClientEngine, ') - ..write('streamMusicCodec: $streamMusicCodec, ') - ..write('downloadMusicCodec: $downloadMusicCodec, ') ..write('discordPresence: $discordPresence, ') ..write('endlessPlayback: $endlessPlayback, ') ..write('enableConnect: $enableConnect, ') @@ -2539,20 +2306,20 @@ class $SourceMatchTableTable extends SourceMatchTable late final GeneratedColumn trackId = GeneratedColumn( 'track_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _sourceIdMeta = - const VerificationMeta('sourceId'); + static const VerificationMeta _sourceInfoMeta = + const VerificationMeta('sourceInfo'); @override - late final GeneratedColumn sourceId = GeneratedColumn( - 'source_id', aliasedName, false, + late final GeneratedColumn sourceInfo = GeneratedColumn( + 'source_info', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("{}")); + static const VerificationMeta _sourceTypeMeta = + const VerificationMeta('sourceType'); + @override + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - @override - late final GeneratedColumnWithTypeConverter sourceType = - GeneratedColumn('source_type', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SourceType.youtube.name)) - .withConverter( - $SourceMatchTableTable.$convertersourceType); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override @@ -2563,7 +2330,7 @@ class $SourceMatchTableTable extends SourceMatchTable defaultValue: currentDateAndTime); @override List get $columns => - [id, trackId, sourceId, sourceType, createdAt]; + [id, trackId, sourceInfo, sourceType, createdAt]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -2584,11 +2351,19 @@ class $SourceMatchTableTable extends SourceMatchTable } else if (isInserting) { context.missing(_trackIdMeta); } - if (data.containsKey('source_id')) { - context.handle(_sourceIdMeta, - sourceId.isAcceptableOrUnknown(data['source_id']!, _sourceIdMeta)); + if (data.containsKey('source_info')) { + context.handle( + _sourceInfoMeta, + sourceInfo.isAcceptableOrUnknown( + data['source_info']!, _sourceInfoMeta)); + } + if (data.containsKey('source_type')) { + context.handle( + _sourceTypeMeta, + sourceType.isAcceptableOrUnknown( + data['source_type']!, _sourceTypeMeta)); } else if (isInserting) { - context.missing(_sourceIdMeta); + context.missing(_sourceTypeMeta); } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, @@ -2607,11 +2382,10 @@ class $SourceMatchTableTable extends SourceMatchTable .read(DriftSqlType.int, data['${effectivePrefix}id'])!, trackId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, - sourceId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}source_id'])!, - sourceType: $SourceMatchTableTable.$convertersourceType.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}source_type'])!), + sourceInfo: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_info'])!, + sourceType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_type'])!, createdAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, ); @@ -2621,22 +2395,19 @@ class $SourceMatchTableTable extends SourceMatchTable $SourceMatchTableTable createAlias(String alias) { return $SourceMatchTableTable(attachedDatabase, alias); } - - static JsonTypeConverter2 $convertersourceType = - const EnumNameConverter(SourceType.values); } class SourceMatchTableData extends DataClass implements Insertable { final int id; final String trackId; - final String sourceId; - final SourceType sourceType; + final String sourceInfo; + final String sourceType; final DateTime createdAt; const SourceMatchTableData( {required this.id, required this.trackId, - required this.sourceId, + required this.sourceInfo, required this.sourceType, required this.createdAt}); @override @@ -2644,11 +2415,8 @@ class SourceMatchTableData extends DataClass final map = {}; map['id'] = Variable(id); map['track_id'] = Variable(trackId); - map['source_id'] = Variable(sourceId); - { - map['source_type'] = Variable( - $SourceMatchTableTable.$convertersourceType.toSql(sourceType)); - } + map['source_info'] = Variable(sourceInfo); + map['source_type'] = Variable(sourceType); map['created_at'] = Variable(createdAt); return map; } @@ -2657,7 +2425,7 @@ class SourceMatchTableData extends DataClass return SourceMatchTableCompanion( id: Value(id), trackId: Value(trackId), - sourceId: Value(sourceId), + sourceInfo: Value(sourceInfo), sourceType: Value(sourceType), createdAt: Value(createdAt), ); @@ -2669,9 +2437,8 @@ class SourceMatchTableData extends DataClass return SourceMatchTableData( id: serializer.fromJson(json['id']), trackId: serializer.fromJson(json['trackId']), - sourceId: serializer.fromJson(json['sourceId']), - sourceType: $SourceMatchTableTable.$convertersourceType - .fromJson(serializer.fromJson(json['sourceType'])), + sourceInfo: serializer.fromJson(json['sourceInfo']), + sourceType: serializer.fromJson(json['sourceType']), createdAt: serializer.fromJson(json['createdAt']), ); } @@ -2681,9 +2448,8 @@ class SourceMatchTableData extends DataClass return { 'id': serializer.toJson(id), 'trackId': serializer.toJson(trackId), - 'sourceId': serializer.toJson(sourceId), - 'sourceType': serializer.toJson( - $SourceMatchTableTable.$convertersourceType.toJson(sourceType)), + 'sourceInfo': serializer.toJson(sourceInfo), + 'sourceType': serializer.toJson(sourceType), 'createdAt': serializer.toJson(createdAt), }; } @@ -2691,13 +2457,13 @@ class SourceMatchTableData extends DataClass SourceMatchTableData copyWith( {int? id, String? trackId, - String? sourceId, - SourceType? sourceType, + String? sourceInfo, + String? sourceType, DateTime? createdAt}) => SourceMatchTableData( id: id ?? this.id, trackId: trackId ?? this.trackId, - sourceId: sourceId ?? this.sourceId, + sourceInfo: sourceInfo ?? this.sourceInfo, sourceType: sourceType ?? this.sourceType, createdAt: createdAt ?? this.createdAt, ); @@ -2705,7 +2471,8 @@ class SourceMatchTableData extends DataClass return SourceMatchTableData( id: data.id.present ? data.id.value : this.id, trackId: data.trackId.present ? data.trackId.value : this.trackId, - sourceId: data.sourceId.present ? data.sourceId.value : this.sourceId, + sourceInfo: + data.sourceInfo.present ? data.sourceInfo.value : this.sourceInfo, sourceType: data.sourceType.present ? data.sourceType.value : this.sourceType, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, @@ -2717,7 +2484,7 @@ class SourceMatchTableData extends DataClass return (StringBuffer('SourceMatchTableData(') ..write('id: $id, ') ..write('trackId: $trackId, ') - ..write('sourceId: $sourceId, ') + ..write('sourceInfo: $sourceInfo, ') ..write('sourceType: $sourceType, ') ..write('createdAt: $createdAt') ..write(')')) @@ -2725,14 +2492,15 @@ class SourceMatchTableData extends DataClass } @override - int get hashCode => Object.hash(id, trackId, sourceId, sourceType, createdAt); + int get hashCode => + Object.hash(id, trackId, sourceInfo, sourceType, createdAt); @override bool operator ==(Object other) => identical(this, other) || (other is SourceMatchTableData && other.id == this.id && other.trackId == this.trackId && - other.sourceId == this.sourceId && + other.sourceInfo == this.sourceInfo && other.sourceType == this.sourceType && other.createdAt == this.createdAt); } @@ -2740,35 +2508,35 @@ class SourceMatchTableData extends DataClass class SourceMatchTableCompanion extends UpdateCompanion { final Value id; final Value trackId; - final Value sourceId; - final Value sourceType; + final Value sourceInfo; + final Value sourceType; final Value createdAt; const SourceMatchTableCompanion({ this.id = const Value.absent(), this.trackId = const Value.absent(), - this.sourceId = const Value.absent(), + this.sourceInfo = const Value.absent(), this.sourceType = const Value.absent(), this.createdAt = const Value.absent(), }); SourceMatchTableCompanion.insert({ this.id = const Value.absent(), required String trackId, - required String sourceId, - this.sourceType = const Value.absent(), + this.sourceInfo = const Value.absent(), + required String sourceType, this.createdAt = const Value.absent(), }) : trackId = Value(trackId), - sourceId = Value(sourceId); + sourceType = Value(sourceType); static Insertable custom({ Expression? id, Expression? trackId, - Expression? sourceId, + Expression? sourceInfo, Expression? sourceType, Expression? createdAt, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (trackId != null) 'track_id': trackId, - if (sourceId != null) 'source_id': sourceId, + if (sourceInfo != null) 'source_info': sourceInfo, if (sourceType != null) 'source_type': sourceType, if (createdAt != null) 'created_at': createdAt, }); @@ -2777,13 +2545,13 @@ class SourceMatchTableCompanion extends UpdateCompanion { SourceMatchTableCompanion copyWith( {Value? id, Value? trackId, - Value? sourceId, - Value? sourceType, + Value? sourceInfo, + Value? sourceType, Value? createdAt}) { return SourceMatchTableCompanion( id: id ?? this.id, trackId: trackId ?? this.trackId, - sourceId: sourceId ?? this.sourceId, + sourceInfo: sourceInfo ?? this.sourceInfo, sourceType: sourceType ?? this.sourceType, createdAt: createdAt ?? this.createdAt, ); @@ -2798,12 +2566,11 @@ class SourceMatchTableCompanion extends UpdateCompanion { if (trackId.present) { map['track_id'] = Variable(trackId.value); } - if (sourceId.present) { - map['source_id'] = Variable(sourceId.value); + if (sourceInfo.present) { + map['source_info'] = Variable(sourceInfo.value); } if (sourceType.present) { - map['source_type'] = Variable( - $SourceMatchTableTable.$convertersourceType.toSql(sourceType.value)); + map['source_type'] = Variable(sourceType.value); } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); @@ -2816,7 +2583,7 @@ class SourceMatchTableCompanion extends UpdateCompanion { return (StringBuffer('SourceMatchTableCompanion(') ..write('id: $id, ') ..write('trackId: $trackId, ') - ..write('sourceId: $sourceId, ') + ..write('sourceInfo: $sourceInfo, ') ..write('sourceType: $sourceType, ') ..write('createdAt: $createdAt') ..write(')')) @@ -4377,7 +4144,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final Index uniqueBlacklist = Index('unique_blacklist', 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); late final Index uniqTrackMatch = Index('uniq_track_match', - 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_info, source_type)'); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -4719,7 +4486,6 @@ typedef $$BlacklistTableTableProcessedTableManager = ProcessedTableManager< typedef $$PreferencesTableTableCreateCompanionBuilder = PreferencesTableCompanion Function({ Value id, - Value audioQuality, Value albumColorSync, Value amoledDarkTheme, Value checkUpdate, @@ -4735,13 +4501,9 @@ typedef $$PreferencesTableTableCreateCompanionBuilder Value searchMode, Value downloadLocation, Value> localLibraryLocation, - Value pipedInstance, - Value invidiousInstance, Value themeMode, - Value audioSource, + Value audioSourceId, Value youtubeClientEngine, - Value streamMusicCodec, - Value downloadMusicCodec, Value discordPresence, Value endlessPlayback, Value enableConnect, @@ -4751,7 +4513,6 @@ typedef $$PreferencesTableTableCreateCompanionBuilder typedef $$PreferencesTableTableUpdateCompanionBuilder = PreferencesTableCompanion Function({ Value id, - Value audioQuality, Value albumColorSync, Value amoledDarkTheme, Value checkUpdate, @@ -4767,13 +4528,9 @@ typedef $$PreferencesTableTableUpdateCompanionBuilder Value searchMode, Value downloadLocation, Value> localLibraryLocation, - Value pipedInstance, - Value invidiousInstance, Value themeMode, - Value audioSource, + Value audioSourceId, Value youtubeClientEngine, - Value streamMusicCodec, - Value downloadMusicCodec, Value discordPresence, Value endlessPlayback, Value enableConnect, @@ -4793,11 +4550,6 @@ class $$PreferencesTableTableFilterComposer ColumnFilters get id => $composableBuilder( column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters - get audioQuality => $composableBuilder( - column: $table.audioQuality, - builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get albumColorSync => $composableBuilder( column: $table.albumColorSync, builder: (column) => ColumnFilters(column)); @@ -4863,22 +4615,13 @@ class $$PreferencesTableTableFilterComposer column: $table.localLibraryLocation, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get pipedInstance => $composableBuilder( - column: $table.pipedInstance, builder: (column) => ColumnFilters(column)); - - ColumnFilters get invidiousInstance => $composableBuilder( - column: $table.invidiousInstance, - builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters get themeMode => $composableBuilder( column: $table.themeMode, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnWithTypeConverterFilters - get audioSource => $composableBuilder( - column: $table.audioSource, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get audioSourceId => $composableBuilder( + column: $table.audioSourceId, builder: (column) => ColumnFilters(column)); ColumnWithTypeConverterFilters @@ -4886,16 +4629,6 @@ class $$PreferencesTableTableFilterComposer column: $table.youtubeClientEngine, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnWithTypeConverterFilters - get streamMusicCodec => $composableBuilder( - column: $table.streamMusicCodec, - builder: (column) => ColumnWithTypeConverterFilters(column)); - - ColumnWithTypeConverterFilters - get downloadMusicCodec => $composableBuilder( - column: $table.downloadMusicCodec, - builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get discordPresence => $composableBuilder( column: $table.discordPresence, builder: (column) => ColumnFilters(column)); @@ -4926,10 +4659,6 @@ class $$PreferencesTableTableOrderingComposer ColumnOrderings get id => $composableBuilder( column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get audioQuality => $composableBuilder( - column: $table.audioQuality, - builder: (column) => ColumnOrderings(column)); - ColumnOrderings get albumColorSync => $composableBuilder( column: $table.albumColorSync, builder: (column) => ColumnOrderings(column)); @@ -4985,32 +4714,17 @@ class $$PreferencesTableTableOrderingComposer column: $table.localLibraryLocation, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pipedInstance => $composableBuilder( - column: $table.pipedInstance, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get invidiousInstance => $composableBuilder( - column: $table.invidiousInstance, - builder: (column) => ColumnOrderings(column)); - ColumnOrderings get themeMode => $composableBuilder( column: $table.themeMode, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get audioSource => $composableBuilder( - column: $table.audioSource, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get audioSourceId => $composableBuilder( + column: $table.audioSourceId, + builder: (column) => ColumnOrderings(column)); ColumnOrderings get youtubeClientEngine => $composableBuilder( column: $table.youtubeClientEngine, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get streamMusicCodec => $composableBuilder( - column: $table.streamMusicCodec, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get downloadMusicCodec => $composableBuilder( - column: $table.downloadMusicCodec, - builder: (column) => ColumnOrderings(column)); - ColumnOrderings get discordPresence => $composableBuilder( column: $table.discordPresence, builder: (column) => ColumnOrderings(column)); @@ -5042,10 +4756,6 @@ class $$PreferencesTableTableAnnotationComposer GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumnWithTypeConverter get audioQuality => - $composableBuilder( - column: $table.audioQuality, builder: (column) => column); - GeneratedColumn get albumColorSync => $composableBuilder( column: $table.albumColorSync, builder: (column) => column); @@ -5096,31 +4806,16 @@ class $$PreferencesTableTableAnnotationComposer get localLibraryLocation => $composableBuilder( column: $table.localLibraryLocation, builder: (column) => column); - GeneratedColumn get pipedInstance => $composableBuilder( - column: $table.pipedInstance, builder: (column) => column); - - GeneratedColumn get invidiousInstance => $composableBuilder( - column: $table.invidiousInstance, builder: (column) => column); - GeneratedColumnWithTypeConverter get themeMode => $composableBuilder(column: $table.themeMode, builder: (column) => column); - GeneratedColumnWithTypeConverter get audioSource => - $composableBuilder( - column: $table.audioSource, builder: (column) => column); + GeneratedColumn get audioSourceId => $composableBuilder( + column: $table.audioSourceId, builder: (column) => column); GeneratedColumnWithTypeConverter get youtubeClientEngine => $composableBuilder( column: $table.youtubeClientEngine, builder: (column) => column); - GeneratedColumnWithTypeConverter get streamMusicCodec => - $composableBuilder( - column: $table.streamMusicCodec, builder: (column) => column); - - GeneratedColumnWithTypeConverter - get downloadMusicCodec => $composableBuilder( - column: $table.downloadMusicCodec, builder: (column) => column); - GeneratedColumn get discordPresence => $composableBuilder( column: $table.discordPresence, builder: (column) => column); @@ -5166,7 +4861,6 @@ class $$PreferencesTableTableTableManager extends RootTableManager< $$PreferencesTableTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), - Value audioQuality = const Value.absent(), Value albumColorSync = const Value.absent(), Value amoledDarkTheme = const Value.absent(), Value checkUpdate = const Value.absent(), @@ -5182,14 +4876,10 @@ class $$PreferencesTableTableTableManager extends RootTableManager< Value searchMode = const Value.absent(), Value downloadLocation = const Value.absent(), Value> localLibraryLocation = const Value.absent(), - Value pipedInstance = const Value.absent(), - Value invidiousInstance = const Value.absent(), Value themeMode = const Value.absent(), - Value audioSource = const Value.absent(), + Value audioSourceId = const Value.absent(), Value youtubeClientEngine = const Value.absent(), - Value streamMusicCodec = const Value.absent(), - Value downloadMusicCodec = const Value.absent(), Value discordPresence = const Value.absent(), Value endlessPlayback = const Value.absent(), Value enableConnect = const Value.absent(), @@ -5198,7 +4888,6 @@ class $$PreferencesTableTableTableManager extends RootTableManager< }) => PreferencesTableCompanion( id: id, - audioQuality: audioQuality, albumColorSync: albumColorSync, amoledDarkTheme: amoledDarkTheme, checkUpdate: checkUpdate, @@ -5214,13 +4903,9 @@ class $$PreferencesTableTableTableManager extends RootTableManager< searchMode: searchMode, downloadLocation: downloadLocation, localLibraryLocation: localLibraryLocation, - pipedInstance: pipedInstance, - invidiousInstance: invidiousInstance, themeMode: themeMode, - audioSource: audioSource, + audioSourceId: audioSourceId, youtubeClientEngine: youtubeClientEngine, - streamMusicCodec: streamMusicCodec, - downloadMusicCodec: downloadMusicCodec, discordPresence: discordPresence, endlessPlayback: endlessPlayback, enableConnect: enableConnect, @@ -5229,7 +4914,6 @@ class $$PreferencesTableTableTableManager extends RootTableManager< ), createCompanionCallback: ({ Value id = const Value.absent(), - Value audioQuality = const Value.absent(), Value albumColorSync = const Value.absent(), Value amoledDarkTheme = const Value.absent(), Value checkUpdate = const Value.absent(), @@ -5245,14 +4929,10 @@ class $$PreferencesTableTableTableManager extends RootTableManager< Value searchMode = const Value.absent(), Value downloadLocation = const Value.absent(), Value> localLibraryLocation = const Value.absent(), - Value pipedInstance = const Value.absent(), - Value invidiousInstance = const Value.absent(), Value themeMode = const Value.absent(), - Value audioSource = const Value.absent(), + Value audioSourceId = const Value.absent(), Value youtubeClientEngine = const Value.absent(), - Value streamMusicCodec = const Value.absent(), - Value downloadMusicCodec = const Value.absent(), Value discordPresence = const Value.absent(), Value endlessPlayback = const Value.absent(), Value enableConnect = const Value.absent(), @@ -5261,7 +4941,6 @@ class $$PreferencesTableTableTableManager extends RootTableManager< }) => PreferencesTableCompanion.insert( id: id, - audioQuality: audioQuality, albumColorSync: albumColorSync, amoledDarkTheme: amoledDarkTheme, checkUpdate: checkUpdate, @@ -5277,13 +4956,9 @@ class $$PreferencesTableTableTableManager extends RootTableManager< searchMode: searchMode, downloadLocation: downloadLocation, localLibraryLocation: localLibraryLocation, - pipedInstance: pipedInstance, - invidiousInstance: invidiousInstance, themeMode: themeMode, - audioSource: audioSource, + audioSourceId: audioSourceId, youtubeClientEngine: youtubeClientEngine, - streamMusicCodec: streamMusicCodec, - downloadMusicCodec: downloadMusicCodec, discordPresence: discordPresence, endlessPlayback: endlessPlayback, enableConnect: enableConnect, @@ -5644,16 +5319,16 @@ typedef $$SourceMatchTableTableCreateCompanionBuilder = SourceMatchTableCompanion Function({ Value id, required String trackId, - required String sourceId, - Value sourceType, + Value sourceInfo, + required String sourceType, Value createdAt, }); typedef $$SourceMatchTableTableUpdateCompanionBuilder = SourceMatchTableCompanion Function({ Value id, Value trackId, - Value sourceId, - Value sourceType, + Value sourceInfo, + Value sourceType, Value createdAt, }); @@ -5672,13 +5347,11 @@ class $$SourceMatchTableTableFilterComposer ColumnFilters get trackId => $composableBuilder( column: $table.trackId, builder: (column) => ColumnFilters(column)); - ColumnFilters get sourceId => $composableBuilder( - column: $table.sourceId, builder: (column) => ColumnFilters(column)); + ColumnFilters get sourceInfo => $composableBuilder( + column: $table.sourceInfo, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters - get sourceType => $composableBuilder( - column: $table.sourceType, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get sourceType => $composableBuilder( + column: $table.sourceType, builder: (column) => ColumnFilters(column)); ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); @@ -5699,8 +5372,8 @@ class $$SourceMatchTableTableOrderingComposer ColumnOrderings get trackId => $composableBuilder( column: $table.trackId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get sourceId => $composableBuilder( - column: $table.sourceId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get sourceInfo => $composableBuilder( + column: $table.sourceInfo, builder: (column) => ColumnOrderings(column)); ColumnOrderings get sourceType => $composableBuilder( column: $table.sourceType, builder: (column) => ColumnOrderings(column)); @@ -5724,12 +5397,11 @@ class $$SourceMatchTableTableAnnotationComposer GeneratedColumn get trackId => $composableBuilder(column: $table.trackId, builder: (column) => column); - GeneratedColumn get sourceId => - $composableBuilder(column: $table.sourceId, builder: (column) => column); + GeneratedColumn get sourceInfo => $composableBuilder( + column: $table.sourceInfo, builder: (column) => column); - GeneratedColumnWithTypeConverter get sourceType => - $composableBuilder( - column: $table.sourceType, builder: (column) => column); + GeneratedColumn get sourceType => $composableBuilder( + column: $table.sourceType, builder: (column) => column); GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); @@ -5765,28 +5437,28 @@ class $$SourceMatchTableTableTableManager extends RootTableManager< updateCompanionCallback: ({ Value id = const Value.absent(), Value trackId = const Value.absent(), - Value sourceId = const Value.absent(), - Value sourceType = const Value.absent(), + Value sourceInfo = const Value.absent(), + Value sourceType = const Value.absent(), Value createdAt = const Value.absent(), }) => SourceMatchTableCompanion( id: id, trackId: trackId, - sourceId: sourceId, + sourceInfo: sourceInfo, sourceType: sourceType, createdAt: createdAt, ), createCompanionCallback: ({ Value id = const Value.absent(), required String trackId, - required String sourceId, - Value sourceType = const Value.absent(), + Value sourceInfo = const Value.absent(), + required String sourceType, Value createdAt = const Value.absent(), }) => SourceMatchTableCompanion.insert( id: id, trackId: trackId, - sourceId: sourceId, + sourceInfo: sourceInfo, sourceType: sourceType, createdAt: createdAt, ), diff --git a/lib/models/database/database.steps.dart b/lib/models/database/database.steps.dart index babe71b9..42cbdf6d 100644 --- a/lib/models/database/database.steps.dart +++ b/lib/models/database/database.steps.dart @@ -5,7 +5,6 @@ import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import import 'package:flutter/material.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/market.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; // GENERATED BY drift_dev, DO NOT MODIFY. final class Schema2 extends i0.VersionedSchema { @@ -330,8 +329,7 @@ class Shape2 extends i0.VersionedTable { i1.GeneratedColumn _column_7(String aliasedName) => i1.GeneratedColumn('audio_quality', aliasedName, false, - type: i1.DriftSqlType.string, - defaultValue: Constant(SourceQualities.high.name)); + type: i1.DriftSqlType.string, defaultValue: Constant("high")); i1.GeneratedColumn _column_8(String aliasedName) => i1.GeneratedColumn('album_color_sync', aliasedName, false, type: i1.DriftSqlType.bool, @@ -418,16 +416,13 @@ i1.GeneratedColumn _column_25(String aliasedName) => defaultValue: Constant(ThemeMode.system.name)); i1.GeneratedColumn _column_26(String aliasedName) => i1.GeneratedColumn('audio_source', aliasedName, false, - type: i1.DriftSqlType.string, - defaultValue: Constant(AudioSource.youtube.name)); + type: i1.DriftSqlType.string, defaultValue: Constant("youtube")); i1.GeneratedColumn _column_27(String aliasedName) => i1.GeneratedColumn('stream_music_codec', aliasedName, false, - type: i1.DriftSqlType.string, - defaultValue: Constant(SourceCodecs.weba.name)); + type: i1.DriftSqlType.string, defaultValue: Constant("weba")); i1.GeneratedColumn _column_28(String aliasedName) => i1.GeneratedColumn('download_music_codec', aliasedName, false, - type: i1.DriftSqlType.string, - defaultValue: Constant(SourceCodecs.m4a.name)); + type: i1.DriftSqlType.string, defaultValue: Constant("m4a")); i1.GeneratedColumn _column_29(String aliasedName) => i1.GeneratedColumn('discord_presence', aliasedName, false, type: i1.DriftSqlType.bool, @@ -512,8 +507,7 @@ i1.GeneratedColumn _column_38(String aliasedName) => type: i1.DriftSqlType.string); i1.GeneratedColumn _column_39(String aliasedName) => i1.GeneratedColumn('source_type', aliasedName, false, - type: i1.DriftSqlType.string, - defaultValue: Constant(SourceType.youtube.name)); + type: i1.DriftSqlType.string, defaultValue: Constant("youtube")); class Shape6 extends i0.VersionedTable { Shape6({required super.source, required super.alias}) : super.aliased(); @@ -2462,6 +2456,289 @@ i1.GeneratedColumn _column_72(String aliasedName) => i1.GeneratedColumn _column_73(String aliasedName) => i1.GeneratedColumn('plugin_api_version', aliasedName, false, type: i1.DriftSqlType.string, defaultValue: const Constant('2.0.0')); + +final class Schema10 extends i0.VersionedSchema { + Schema10({required super.database}) : super(version: 10); + @override + late final List entities = [ + authenticationTable, + blacklistTable, + preferencesTable, + scrobblerTable, + skipSegmentTable, + sourceMatchTable, + audioPlayerStateTable, + historyTable, + lyricsTable, + pluginsTable, + uniqueBlacklist, + uniqTrackMatch, + ]; + late final Shape0 authenticationTable = Shape0( + source: i0.VersionedTable( + entityName: 'authentication_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 blacklistTable = Shape1( + source: i0.VersionedTable( + entityName: 'blacklist_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_4, + _column_5, + _column_6, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape17 preferencesTable = Shape17( + source: i0.VersionedTable( + entityName: 'preferences_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_8, + _column_9, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + _column_15, + _column_69, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_22, + _column_25, + _column_74, + _column_54, + _column_29, + _column_30, + _column_31, + _column_56, + _column_53, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 scrobblerTable = Shape3( + source: i0.VersionedTable( + entityName: 'scrobbler_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_32, + _column_33, + _column_34, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape4 skipSegmentTable = Shape4( + source: i0.VersionedTable( + entityName: 'skip_segment_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_35, + _column_36, + _column_37, + _column_32, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape18 sourceMatchTable = Shape18( + source: i0.VersionedTable( + entityName: 'source_match_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_37, + _column_75, + _column_76, + _column_32, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape14 audioPlayerStateTable = Shape14( + source: i0.VersionedTable( + entityName: 'audio_player_state_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_40, + _column_41, + _column_42, + _column_43, + _column_57, + _column_58, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 historyTable = Shape9( + source: i0.VersionedTable( + entityName: 'history_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_32, + _column_50, + _column_51, + _column_52, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 lyricsTable = Shape10( + source: i0.VersionedTable( + entityName: 'lyrics_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_37, + _column_52, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape16 pluginsTable = Shape16( + source: i0.VersionedTable( + entityName: 'plugins_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_59, + _column_60, + _column_61, + _column_62, + _column_63, + _column_64, + _column_65, + _column_71, + _column_72, + _column_67, + _column_73, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Index uniqueBlacklist = i1.Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); + final i1.Index uniqTrackMatch = i1.Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_info, source_type)'); +} + +class Shape17 extends i0.VersionedTable { + Shape17({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get albumColorSync => + columnsByName['album_color_sync']! as i1.GeneratedColumn; + i1.GeneratedColumn get amoledDarkTheme => + columnsByName['amoled_dark_theme']! as i1.GeneratedColumn; + i1.GeneratedColumn get checkUpdate => + columnsByName['check_update']! as i1.GeneratedColumn; + i1.GeneratedColumn get normalizeAudio => + columnsByName['normalize_audio']! as i1.GeneratedColumn; + i1.GeneratedColumn get showSystemTrayIcon => + columnsByName['show_system_tray_icon']! as i1.GeneratedColumn; + i1.GeneratedColumn get systemTitleBar => + columnsByName['system_title_bar']! as i1.GeneratedColumn; + i1.GeneratedColumn get skipNonMusic => + columnsByName['skip_non_music']! as i1.GeneratedColumn; + i1.GeneratedColumn get closeBehavior => + columnsByName['close_behavior']! as i1.GeneratedColumn; + i1.GeneratedColumn get accentColorScheme => + columnsByName['accent_color_scheme']! as i1.GeneratedColumn; + i1.GeneratedColumn get layoutMode => + columnsByName['layout_mode']! as i1.GeneratedColumn; + i1.GeneratedColumn get locale => + columnsByName['locale']! as i1.GeneratedColumn; + i1.GeneratedColumn get market => + columnsByName['market']! as i1.GeneratedColumn; + i1.GeneratedColumn get searchMode => + columnsByName['search_mode']! as i1.GeneratedColumn; + i1.GeneratedColumn get downloadLocation => + columnsByName['download_location']! as i1.GeneratedColumn; + i1.GeneratedColumn get localLibraryLocation => + columnsByName['local_library_location']! as i1.GeneratedColumn; + i1.GeneratedColumn get themeMode => + columnsByName['theme_mode']! as i1.GeneratedColumn; + i1.GeneratedColumn get audioSourceId => + columnsByName['audio_source_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get youtubeClientEngine => + columnsByName['youtube_client_engine']! as i1.GeneratedColumn; + i1.GeneratedColumn get discordPresence => + columnsByName['discord_presence']! as i1.GeneratedColumn; + i1.GeneratedColumn get endlessPlayback => + columnsByName['endless_playback']! as i1.GeneratedColumn; + i1.GeneratedColumn get enableConnect => + columnsByName['enable_connect']! as i1.GeneratedColumn; + i1.GeneratedColumn get connectPort => + columnsByName['connect_port']! as i1.GeneratedColumn; + i1.GeneratedColumn get cacheMusic => + columnsByName['cache_music']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_74(String aliasedName) => + i1.GeneratedColumn('audio_source_id', aliasedName, true, + type: i1.DriftSqlType.string); + +class Shape18 extends i0.VersionedTable { + Shape18({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get trackId => + columnsByName['track_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get sourceInfo => + columnsByName['source_info']! as i1.GeneratedColumn; + i1.GeneratedColumn get sourceType => + columnsByName['source_type']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_75(String aliasedName) => + i1.GeneratedColumn('source_info', aliasedName, false, + type: i1.DriftSqlType.string, defaultValue: const Constant("{}")); +i1.GeneratedColumn _column_76(String aliasedName) => + i1.GeneratedColumn('source_type', aliasedName, false, + type: i1.DriftSqlType.string); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -2471,6 +2748,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, required Future Function(i1.Migrator m, Schema9 schema) from8To9, + required Future Function(i1.Migrator m, Schema10 schema) from9To10, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -2514,6 +2792,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from8To9(migrator, schema); return 9; + case 9: + final schema = Schema10(database: database); + final migrator = i1.Migrator(database, schema); + await from9To10(migrator, schema); + return 10; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -2529,6 +2812,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, required Future Function(i1.Migrator m, Schema9 schema) from8To9, + required Future Function(i1.Migrator m, Schema10 schema) from9To10, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( @@ -2540,4 +2824,5 @@ i1.OnUpgrade stepByStep({ from6To7: from6To7, from7To8: from7To8, from8To9: from8To9, + from9To10: from9To10, )); diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index ea2f7538..cc810ae7 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -76,10 +76,6 @@ class PreferencesTable extends Table { TextColumn get downloadLocation => text().withDefault(const Constant(""))(); TextColumn get localLibraryLocation => text().withDefault(const Constant("")).map(const StringListConverter())(); - TextColumn get pipedInstance => - text().withDefault(const Constant("https://pipedapi.kavin.rocks"))(); - TextColumn get invidiousInstance => - text().withDefault(const Constant("https://inv.nadeko.net"))(); TextColumn get themeMode => textEnum().withDefault(Constant(ThemeMode.system.name))(); TextColumn get audioSourceId => text().nullable()(); @@ -113,8 +109,6 @@ class PreferencesTable extends Table { searchMode: SearchMode.youtube, downloadLocation: "", localLibraryLocation: [], - pipedInstance: "https://pipedapi.kavin.rocks", - invidiousInstance: "https://inv.nadeko.net", themeMode: ThemeMode.system, audioSourceId: null, youtubeClientEngine: YoutubeClientEngine.youtubeExplode, diff --git a/lib/models/database/tables/source_match.dart b/lib/models/database/tables/source_match.dart index b5661137..9ef79e9b 100644 --- a/lib/models/database/tables/source_match.dart +++ b/lib/models/database/tables/source_match.dart @@ -8,7 +8,7 @@ part of '../database.dart'; class SourceMatchTable extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get trackId => text()(); - TextColumn get sourceInfo => text()(); + TextColumn get sourceInfo => text().withDefault(const Constant("{}"))(); TextColumn get sourceType => text()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); } diff --git a/lib/models/metadata/audio_source.dart b/lib/models/metadata/audio_source.dart index c429ec74..44804285 100644 --- a/lib/models/metadata/audio_source.dart +++ b/lib/models/metadata/audio_source.dart @@ -1,5 +1,7 @@ part of 'metadata.dart'; +final oneOptionalDecimalFormatter = NumberFormat('0.#', 'en_US'); + enum SpotubeMediaCompressionType { lossy, lossless, @@ -30,26 +32,40 @@ class SpotubeAudioSourceContainerPreset @freezed class SpotubeAudioLossyContainerQuality with _$SpotubeAudioLossyContainerQuality { + const SpotubeAudioLossyContainerQuality._(); + factory SpotubeAudioLossyContainerQuality({ - required double bitrate, + required int bitrate, // bits per second }) = _SpotubeAudioLossyContainerQuality; factory SpotubeAudioLossyContainerQuality.fromJson( Map json) => _$SpotubeAudioLossyContainerQualityFromJson(json); + + @override + toString() { + return "${oneOptionalDecimalFormatter.format(bitrate)}kbps"; + } } @freezed class SpotubeAudioLosslessContainerQuality with _$SpotubeAudioLosslessContainerQuality { + const SpotubeAudioLosslessContainerQuality._(); + factory SpotubeAudioLosslessContainerQuality({ - required int bitDepth, - required double sampleRate, + required int bitDepth, // bit + required int sampleRate, // hz }) = _SpotubeAudioLosslessContainerQuality; factory SpotubeAudioLosslessContainerQuality.fromJson( Map json) => _$SpotubeAudioLosslessContainerQualityFromJson(json); + + @override + toString() { + return "${bitDepth}bit • ${oneOptionalDecimalFormatter.format(sampleRate / 1000)}kHz"; + } } @freezed diff --git a/lib/models/metadata/metadata.dart b/lib/models/metadata/metadata.dart index 4c6eb2ac..e68bcd14 100644 --- a/lib/models/metadata/metadata.dart +++ b/lib/models/metadata/metadata.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; import 'package:metadata_god/metadata_god.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart'; diff --git a/lib/models/metadata/metadata.freezed.dart b/lib/models/metadata/metadata.freezed.dart index f54ee379..301929a5 100644 --- a/lib/models/metadata/metadata.freezed.dart +++ b/lib/models/metadata/metadata.freezed.dart @@ -595,7 +595,7 @@ SpotubeAudioLossyContainerQuality _$SpotubeAudioLossyContainerQualityFromJson( /// @nodoc mixin _$SpotubeAudioLossyContainerQuality { - double get bitrate => throw _privateConstructorUsedError; + int get bitrate => throw _privateConstructorUsedError; /// Serializes this SpotubeAudioLossyContainerQuality to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -615,7 +615,7 @@ abstract class $SpotubeAudioLossyContainerQualityCopyWith<$Res> { _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res, SpotubeAudioLossyContainerQuality>; @useResult - $Res call({double bitrate}); + $Res call({int bitrate}); } /// @nodoc @@ -640,7 +640,7 @@ class _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res, bitrate: null == bitrate ? _value.bitrate : bitrate // ignore: cast_nullable_to_non_nullable - as double, + as int, ) as $Val); } } @@ -654,7 +654,7 @@ abstract class _$$SpotubeAudioLossyContainerQualityImplCopyWith<$Res> __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res>; @override @useResult - $Res call({double bitrate}); + $Res call({int bitrate}); } /// @nodoc @@ -678,7 +678,7 @@ class __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res> bitrate: null == bitrate ? _value.bitrate : bitrate // ignore: cast_nullable_to_non_nullable - as double, + as int, )); } } @@ -686,20 +686,15 @@ class __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$SpotubeAudioLossyContainerQualityImpl - implements _SpotubeAudioLossyContainerQuality { - _$SpotubeAudioLossyContainerQualityImpl({required this.bitrate}); + extends _SpotubeAudioLossyContainerQuality { + _$SpotubeAudioLossyContainerQualityImpl({required this.bitrate}) : super._(); factory _$SpotubeAudioLossyContainerQualityImpl.fromJson( Map json) => _$$SpotubeAudioLossyContainerQualityImplFromJson(json); @override - final double bitrate; - - @override - String toString() { - return 'SpotubeAudioLossyContainerQuality(bitrate: $bitrate)'; - } + final int bitrate; @override bool operator ==(Object other) { @@ -732,16 +727,17 @@ class _$SpotubeAudioLossyContainerQualityImpl } abstract class _SpotubeAudioLossyContainerQuality - implements SpotubeAudioLossyContainerQuality { - factory _SpotubeAudioLossyContainerQuality({required final double bitrate}) = + extends SpotubeAudioLossyContainerQuality { + factory _SpotubeAudioLossyContainerQuality({required final int bitrate}) = _$SpotubeAudioLossyContainerQualityImpl; + _SpotubeAudioLossyContainerQuality._() : super._(); factory _SpotubeAudioLossyContainerQuality.fromJson( Map json) = _$SpotubeAudioLossyContainerQualityImpl.fromJson; @override - double get bitrate; + int get bitrate; /// Create a copy of SpotubeAudioLossyContainerQuality /// with the given fields replaced by the non-null parameter values. @@ -759,8 +755,8 @@ SpotubeAudioLosslessContainerQuality /// @nodoc mixin _$SpotubeAudioLosslessContainerQuality { - int get bitDepth => throw _privateConstructorUsedError; - double get sampleRate => throw _privateConstructorUsedError; + int get bitDepth => throw _privateConstructorUsedError; // bit + int get sampleRate => throw _privateConstructorUsedError; /// Serializes this SpotubeAudioLosslessContainerQuality to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -781,7 +777,7 @@ abstract class $SpotubeAudioLosslessContainerQualityCopyWith<$Res> { _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res, SpotubeAudioLosslessContainerQuality>; @useResult - $Res call({int bitDepth, double sampleRate}); + $Res call({int bitDepth, int sampleRate}); } /// @nodoc @@ -811,7 +807,7 @@ class _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res, sampleRate: null == sampleRate ? _value.sampleRate : sampleRate // ignore: cast_nullable_to_non_nullable - as double, + as int, ) as $Val); } } @@ -825,7 +821,7 @@ abstract class _$$SpotubeAudioLosslessContainerQualityImplCopyWith<$Res> __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res>; @override @useResult - $Res call({int bitDepth, double sampleRate}); + $Res call({int bitDepth, int sampleRate}); } /// @nodoc @@ -854,7 +850,7 @@ class __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res> sampleRate: null == sampleRate ? _value.sampleRate : sampleRate // ignore: cast_nullable_to_non_nullable - as double, + as int, )); } } @@ -862,9 +858,10 @@ class __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$SpotubeAudioLosslessContainerQualityImpl - implements _SpotubeAudioLosslessContainerQuality { + extends _SpotubeAudioLosslessContainerQuality { _$SpotubeAudioLosslessContainerQualityImpl( - {required this.bitDepth, required this.sampleRate}); + {required this.bitDepth, required this.sampleRate}) + : super._(); factory _$SpotubeAudioLosslessContainerQualityImpl.fromJson( Map json) => @@ -872,13 +869,9 @@ class _$SpotubeAudioLosslessContainerQualityImpl @override final int bitDepth; +// bit @override - final double sampleRate; - - @override - String toString() { - return 'SpotubeAudioLosslessContainerQuality(bitDepth: $bitDepth, sampleRate: $sampleRate)'; - } + final int sampleRate; @override bool operator ==(Object other) { @@ -914,19 +907,20 @@ class _$SpotubeAudioLosslessContainerQualityImpl } abstract class _SpotubeAudioLosslessContainerQuality - implements SpotubeAudioLosslessContainerQuality { + extends SpotubeAudioLosslessContainerQuality { factory _SpotubeAudioLosslessContainerQuality( - {required final int bitDepth, required final double sampleRate}) = + {required final int bitDepth, required final int sampleRate}) = _$SpotubeAudioLosslessContainerQualityImpl; + _SpotubeAudioLosslessContainerQuality._() : super._(); factory _SpotubeAudioLosslessContainerQuality.fromJson( Map json) = _$SpotubeAudioLosslessContainerQualityImpl.fromJson; @override - int get bitDepth; + int get bitDepth; // bit @override - double get sampleRate; + int get sampleRate; /// Create a copy of SpotubeAudioLosslessContainerQuality /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/metadata/metadata.g.dart b/lib/models/metadata/metadata.g.dart index 7497053c..56783d80 100644 --- a/lib/models/metadata/metadata.g.dart +++ b/lib/models/metadata/metadata.g.dart @@ -52,7 +52,7 @@ Map _$$SpotubeAudioSourceContainerPresetLosslessImplToJson( _$SpotubeAudioLossyContainerQualityImpl _$$SpotubeAudioLossyContainerQualityImplFromJson(Map json) => _$SpotubeAudioLossyContainerQualityImpl( - bitrate: (json['bitrate'] as num).toDouble(), + bitrate: (json['bitrate'] as num).toInt(), ); Map _$$SpotubeAudioLossyContainerQualityImplToJson( @@ -65,7 +65,7 @@ _$SpotubeAudioLosslessContainerQualityImpl _$$SpotubeAudioLosslessContainerQualityImplFromJson(Map json) => _$SpotubeAudioLosslessContainerQualityImpl( bitDepth: (json['bitDepth'] as num).toInt(), - sampleRate: (json['sampleRate'] as num).toDouble(), + sampleRate: (json['sampleRate'] as num).toInt(), ); Map _$$SpotubeAudioLosslessContainerQualityImplToJson( diff --git a/lib/models/playback/track_sources.dart b/lib/models/playback/track_sources.dart index 262fcefa..677b34b8 100644 --- a/lib/models/playback/track_sources.dart +++ b/lib/models/playback/track_sources.dart @@ -1,7 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:spotube/models/metadata/metadata.dart'; -part 'track_sources.freezed.dart'; part 'track_sources.g.dart'; @JsonSerializable() diff --git a/lib/models/playback/track_sources.freezed.dart b/lib/models/playback/track_sources.freezed.dart deleted file mode 100644 index 09ceb399..00000000 --- a/lib/models/playback/track_sources.freezed.dart +++ /dev/null @@ -1,800 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'track_sources.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -TrackSourceQuery _$TrackSourceQueryFromJson(Map json) { - return _TrackSourceQuery.fromJson(json); -} - -/// @nodoc -mixin _$TrackSourceQuery { - String get id => throw _privateConstructorUsedError; - String get title => throw _privateConstructorUsedError; - List get artists => throw _privateConstructorUsedError; - String get album => throw _privateConstructorUsedError; - int get durationMs => throw _privateConstructorUsedError; - String get isrc => throw _privateConstructorUsedError; - bool get explicit => throw _privateConstructorUsedError; - - /// Serializes this TrackSourceQuery to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of TrackSourceQuery - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $TrackSourceQueryCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $TrackSourceQueryCopyWith<$Res> { - factory $TrackSourceQueryCopyWith( - TrackSourceQuery value, $Res Function(TrackSourceQuery) then) = - _$TrackSourceQueryCopyWithImpl<$Res, TrackSourceQuery>; - @useResult - $Res call( - {String id, - String title, - List artists, - String album, - int durationMs, - String isrc, - bool explicit}); -} - -/// @nodoc -class _$TrackSourceQueryCopyWithImpl<$Res, $Val extends TrackSourceQuery> - implements $TrackSourceQueryCopyWith<$Res> { - _$TrackSourceQueryCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of TrackSourceQuery - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? title = null, - Object? artists = null, - Object? album = null, - Object? durationMs = null, - Object? isrc = null, - Object? explicit = null, - }) { - return _then(_value.copyWith( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String, - title: null == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String, - artists: null == artists - ? _value.artists - : artists // ignore: cast_nullable_to_non_nullable - as List, - album: null == album - ? _value.album - : album // ignore: cast_nullable_to_non_nullable - as String, - durationMs: null == durationMs - ? _value.durationMs - : durationMs // ignore: cast_nullable_to_non_nullable - as int, - isrc: null == isrc - ? _value.isrc - : isrc // ignore: cast_nullable_to_non_nullable - as String, - explicit: null == explicit - ? _value.explicit - : explicit // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$TrackSourceQueryImplCopyWith<$Res> - implements $TrackSourceQueryCopyWith<$Res> { - factory _$$TrackSourceQueryImplCopyWith(_$TrackSourceQueryImpl value, - $Res Function(_$TrackSourceQueryImpl) then) = - __$$TrackSourceQueryImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String id, - String title, - List artists, - String album, - int durationMs, - String isrc, - bool explicit}); -} - -/// @nodoc -class __$$TrackSourceQueryImplCopyWithImpl<$Res> - extends _$TrackSourceQueryCopyWithImpl<$Res, _$TrackSourceQueryImpl> - implements _$$TrackSourceQueryImplCopyWith<$Res> { - __$$TrackSourceQueryImplCopyWithImpl(_$TrackSourceQueryImpl _value, - $Res Function(_$TrackSourceQueryImpl) _then) - : super(_value, _then); - - /// Create a copy of TrackSourceQuery - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? title = null, - Object? artists = null, - Object? album = null, - Object? durationMs = null, - Object? isrc = null, - Object? explicit = null, - }) { - return _then(_$TrackSourceQueryImpl( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String, - title: null == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String, - artists: null == artists - ? _value._artists - : artists // ignore: cast_nullable_to_non_nullable - as List, - album: null == album - ? _value.album - : album // ignore: cast_nullable_to_non_nullable - as String, - durationMs: null == durationMs - ? _value.durationMs - : durationMs // ignore: cast_nullable_to_non_nullable - as int, - isrc: null == isrc - ? _value.isrc - : isrc // ignore: cast_nullable_to_non_nullable - as String, - explicit: null == explicit - ? _value.explicit - : explicit // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$TrackSourceQueryImpl extends _TrackSourceQuery { - _$TrackSourceQueryImpl( - {required this.id, - required this.title, - required final List artists, - required this.album, - required this.durationMs, - required this.isrc, - required this.explicit}) - : _artists = artists, - super._(); - - factory _$TrackSourceQueryImpl.fromJson(Map json) => - _$$TrackSourceQueryImplFromJson(json); - - @override - final String id; - @override - final String title; - final List _artists; - @override - List get artists { - if (_artists is EqualUnmodifiableListView) return _artists; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_artists); - } - - @override - final String album; - @override - final int durationMs; - @override - final String isrc; - @override - final bool explicit; - - @override - String toString() { - return 'TrackSourceQuery(id: $id, title: $title, artists: $artists, album: $album, durationMs: $durationMs, isrc: $isrc, explicit: $explicit)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$TrackSourceQueryImpl && - (identical(other.id, id) || other.id == id) && - (identical(other.title, title) || other.title == title) && - const DeepCollectionEquality().equals(other._artists, _artists) && - (identical(other.album, album) || other.album == album) && - (identical(other.durationMs, durationMs) || - other.durationMs == durationMs) && - (identical(other.isrc, isrc) || other.isrc == isrc) && - (identical(other.explicit, explicit) || - other.explicit == explicit)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - id, - title, - const DeepCollectionEquality().hash(_artists), - album, - durationMs, - isrc, - explicit); - - /// Create a copy of TrackSourceQuery - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith => - __$$TrackSourceQueryImplCopyWithImpl<_$TrackSourceQueryImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$TrackSourceQueryImplToJson( - this, - ); - } -} - -abstract class _TrackSourceQuery extends TrackSourceQuery { - factory _TrackSourceQuery( - {required final String id, - required final String title, - required final List artists, - required final String album, - required final int durationMs, - required final String isrc, - required final bool explicit}) = _$TrackSourceQueryImpl; - _TrackSourceQuery._() : super._(); - - factory _TrackSourceQuery.fromJson(Map json) = - _$TrackSourceQueryImpl.fromJson; - - @override - String get id; - @override - String get title; - @override - List get artists; - @override - String get album; - @override - int get durationMs; - @override - String get isrc; - @override - bool get explicit; - - /// Create a copy of TrackSourceQuery - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith => - throw _privateConstructorUsedError; -} - -TrackSourceInfo _$TrackSourceInfoFromJson(Map json) { - return _TrackSourceInfo.fromJson(json); -} - -/// @nodoc -mixin _$TrackSourceInfo { - String get id => throw _privateConstructorUsedError; - String get title => throw _privateConstructorUsedError; - String get artists => throw _privateConstructorUsedError; - String get thumbnail => throw _privateConstructorUsedError; - String get pageUrl => throw _privateConstructorUsedError; - int get durationMs => throw _privateConstructorUsedError; - - /// Serializes this TrackSourceInfo to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of TrackSourceInfo - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $TrackSourceInfoCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $TrackSourceInfoCopyWith<$Res> { - factory $TrackSourceInfoCopyWith( - TrackSourceInfo value, $Res Function(TrackSourceInfo) then) = - _$TrackSourceInfoCopyWithImpl<$Res, TrackSourceInfo>; - @useResult - $Res call( - {String id, - String title, - String artists, - String thumbnail, - String pageUrl, - int durationMs}); -} - -/// @nodoc -class _$TrackSourceInfoCopyWithImpl<$Res, $Val extends TrackSourceInfo> - implements $TrackSourceInfoCopyWith<$Res> { - _$TrackSourceInfoCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of TrackSourceInfo - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? title = null, - Object? artists = null, - Object? thumbnail = null, - Object? pageUrl = null, - Object? durationMs = null, - }) { - return _then(_value.copyWith( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String, - title: null == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String, - artists: null == artists - ? _value.artists - : artists // ignore: cast_nullable_to_non_nullable - as String, - thumbnail: null == thumbnail - ? _value.thumbnail - : thumbnail // ignore: cast_nullable_to_non_nullable - as String, - pageUrl: null == pageUrl - ? _value.pageUrl - : pageUrl // ignore: cast_nullable_to_non_nullable - as String, - durationMs: null == durationMs - ? _value.durationMs - : durationMs // ignore: cast_nullable_to_non_nullable - as int, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$TrackSourceInfoImplCopyWith<$Res> - implements $TrackSourceInfoCopyWith<$Res> { - factory _$$TrackSourceInfoImplCopyWith(_$TrackSourceInfoImpl value, - $Res Function(_$TrackSourceInfoImpl) then) = - __$$TrackSourceInfoImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String id, - String title, - String artists, - String thumbnail, - String pageUrl, - int durationMs}); -} - -/// @nodoc -class __$$TrackSourceInfoImplCopyWithImpl<$Res> - extends _$TrackSourceInfoCopyWithImpl<$Res, _$TrackSourceInfoImpl> - implements _$$TrackSourceInfoImplCopyWith<$Res> { - __$$TrackSourceInfoImplCopyWithImpl( - _$TrackSourceInfoImpl _value, $Res Function(_$TrackSourceInfoImpl) _then) - : super(_value, _then); - - /// Create a copy of TrackSourceInfo - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? title = null, - Object? artists = null, - Object? thumbnail = null, - Object? pageUrl = null, - Object? durationMs = null, - }) { - return _then(_$TrackSourceInfoImpl( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String, - title: null == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String, - artists: null == artists - ? _value.artists - : artists // ignore: cast_nullable_to_non_nullable - as String, - thumbnail: null == thumbnail - ? _value.thumbnail - : thumbnail // ignore: cast_nullable_to_non_nullable - as String, - pageUrl: null == pageUrl - ? _value.pageUrl - : pageUrl // ignore: cast_nullable_to_non_nullable - as String, - durationMs: null == durationMs - ? _value.durationMs - : durationMs // ignore: cast_nullable_to_non_nullable - as int, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$TrackSourceInfoImpl implements _TrackSourceInfo { - _$TrackSourceInfoImpl( - {required this.id, - required this.title, - required this.artists, - required this.thumbnail, - required this.pageUrl, - required this.durationMs}); - - factory _$TrackSourceInfoImpl.fromJson(Map json) => - _$$TrackSourceInfoImplFromJson(json); - - @override - final String id; - @override - final String title; - @override - final String artists; - @override - final String thumbnail; - @override - final String pageUrl; - @override - final int durationMs; - - @override - String toString() { - return 'TrackSourceInfo(id: $id, title: $title, artists: $artists, thumbnail: $thumbnail, pageUrl: $pageUrl, durationMs: $durationMs)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$TrackSourceInfoImpl && - (identical(other.id, id) || other.id == id) && - (identical(other.title, title) || other.title == title) && - (identical(other.artists, artists) || other.artists == artists) && - (identical(other.thumbnail, thumbnail) || - other.thumbnail == thumbnail) && - (identical(other.pageUrl, pageUrl) || other.pageUrl == pageUrl) && - (identical(other.durationMs, durationMs) || - other.durationMs == durationMs)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, id, title, artists, thumbnail, pageUrl, durationMs); - - /// Create a copy of TrackSourceInfo - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith => - __$$TrackSourceInfoImplCopyWithImpl<_$TrackSourceInfoImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$TrackSourceInfoImplToJson( - this, - ); - } -} - -abstract class _TrackSourceInfo implements TrackSourceInfo { - factory _TrackSourceInfo( - {required final String id, - required final String title, - required final String artists, - required final String thumbnail, - required final String pageUrl, - required final int durationMs}) = _$TrackSourceInfoImpl; - - factory _TrackSourceInfo.fromJson(Map json) = - _$TrackSourceInfoImpl.fromJson; - - @override - String get id; - @override - String get title; - @override - String get artists; - @override - String get thumbnail; - @override - String get pageUrl; - @override - int get durationMs; - - /// Create a copy of TrackSourceInfo - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith => - throw _privateConstructorUsedError; -} - -TrackSource _$TrackSourceFromJson(Map json) { - return _TrackSource.fromJson(json); -} - -/// @nodoc -mixin _$TrackSource { - String get url => throw _privateConstructorUsedError; - SourceQualities get quality => throw _privateConstructorUsedError; - SourceCodecs get codec => throw _privateConstructorUsedError; - String get bitrate => throw _privateConstructorUsedError; - String get qualityLabel => throw _privateConstructorUsedError; - - /// Serializes this TrackSource to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of TrackSource - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $TrackSourceCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $TrackSourceCopyWith<$Res> { - factory $TrackSourceCopyWith( - TrackSource value, $Res Function(TrackSource) then) = - _$TrackSourceCopyWithImpl<$Res, TrackSource>; - @useResult - $Res call( - {String url, - SourceQualities quality, - SourceCodecs codec, - String bitrate, - String qualityLabel}); -} - -/// @nodoc -class _$TrackSourceCopyWithImpl<$Res, $Val extends TrackSource> - implements $TrackSourceCopyWith<$Res> { - _$TrackSourceCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of TrackSource - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? url = null, - Object? quality = null, - Object? codec = null, - Object? bitrate = null, - Object? qualityLabel = null, - }) { - return _then(_value.copyWith( - url: null == url - ? _value.url - : url // ignore: cast_nullable_to_non_nullable - as String, - quality: null == quality - ? _value.quality - : quality // ignore: cast_nullable_to_non_nullable - as SourceQualities, - codec: null == codec - ? _value.codec - : codec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - bitrate: null == bitrate - ? _value.bitrate - : bitrate // ignore: cast_nullable_to_non_nullable - as String, - qualityLabel: null == qualityLabel - ? _value.qualityLabel - : qualityLabel // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$TrackSourceImplCopyWith<$Res> - implements $TrackSourceCopyWith<$Res> { - factory _$$TrackSourceImplCopyWith( - _$TrackSourceImpl value, $Res Function(_$TrackSourceImpl) then) = - __$$TrackSourceImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String url, - SourceQualities quality, - SourceCodecs codec, - String bitrate, - String qualityLabel}); -} - -/// @nodoc -class __$$TrackSourceImplCopyWithImpl<$Res> - extends _$TrackSourceCopyWithImpl<$Res, _$TrackSourceImpl> - implements _$$TrackSourceImplCopyWith<$Res> { - __$$TrackSourceImplCopyWithImpl( - _$TrackSourceImpl _value, $Res Function(_$TrackSourceImpl) _then) - : super(_value, _then); - - /// Create a copy of TrackSource - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? url = null, - Object? quality = null, - Object? codec = null, - Object? bitrate = null, - Object? qualityLabel = null, - }) { - return _then(_$TrackSourceImpl( - url: null == url - ? _value.url - : url // ignore: cast_nullable_to_non_nullable - as String, - quality: null == quality - ? _value.quality - : quality // ignore: cast_nullable_to_non_nullable - as SourceQualities, - codec: null == codec - ? _value.codec - : codec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - bitrate: null == bitrate - ? _value.bitrate - : bitrate // ignore: cast_nullable_to_non_nullable - as String, - qualityLabel: null == qualityLabel - ? _value.qualityLabel - : qualityLabel // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$TrackSourceImpl implements _TrackSource { - _$TrackSourceImpl( - {required this.url, - required this.quality, - required this.codec, - required this.bitrate, - required this.qualityLabel}); - - factory _$TrackSourceImpl.fromJson(Map json) => - _$$TrackSourceImplFromJson(json); - - @override - final String url; - @override - final SourceQualities quality; - @override - final SourceCodecs codec; - @override - final String bitrate; - @override - final String qualityLabel; - - @override - String toString() { - return 'TrackSource(url: $url, quality: $quality, codec: $codec, bitrate: $bitrate, qualityLabel: $qualityLabel)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$TrackSourceImpl && - (identical(other.url, url) || other.url == url) && - (identical(other.quality, quality) || other.quality == quality) && - (identical(other.codec, codec) || other.codec == codec) && - (identical(other.bitrate, bitrate) || other.bitrate == bitrate) && - (identical(other.qualityLabel, qualityLabel) || - other.qualityLabel == qualityLabel)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => - Object.hash(runtimeType, url, quality, codec, bitrate, qualityLabel); - - /// Create a copy of TrackSource - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith => - __$$TrackSourceImplCopyWithImpl<_$TrackSourceImpl>(this, _$identity); - - @override - Map toJson() { - return _$$TrackSourceImplToJson( - this, - ); - } -} - -abstract class _TrackSource implements TrackSource { - factory _TrackSource( - {required final String url, - required final SourceQualities quality, - required final SourceCodecs codec, - required final String bitrate, - required final String qualityLabel}) = _$TrackSourceImpl; - - factory _TrackSource.fromJson(Map json) = - _$TrackSourceImpl.fromJson; - - @override - String get url; - @override - SourceQualities get quality; - @override - SourceCodecs get codec; - @override - String get bitrate; - @override - String get qualityLabel; - - /// Create a copy of TrackSource - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/models/playback/track_sources.g.dart b/lib/models/playback/track_sources.g.dart index dd63aebb..3088493a 100644 --- a/lib/models/playback/track_sources.g.dart +++ b/lib/models/playback/track_sources.g.dart @@ -7,17 +7,18 @@ part of 'track_sources.dart'; // ************************************************************************** BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack( - query: TrackSourceQuery.fromJson( + query: SpotubeFullTrackObject.fromJson( Map.from(json['query'] as Map)), - source: $enumDecode(_$AudioSourceEnumMap, json['source']), - info: TrackSourceInfo.fromJson( + source: json['source'] as String, + info: SpotubeAudioSourceMatchObject.fromJson( Map.from(json['info'] as Map)), sources: (json['sources'] as List) - .map((e) => TrackSource.fromJson(Map.from(e as Map))) + .map((e) => SpotubeAudioSourceStreamObject.fromJson( + Map.from(e as Map))) .toList(), siblings: (json['siblings'] as List?) - ?.map((e) => - TrackSourceInfo.fromJson(Map.from(e as Map))) + ?.map((e) => SpotubeAudioSourceMatchObject.fromJson( + Map.from(e as Map))) .toList() ?? const [], ); @@ -25,92 +26,8 @@ BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack( Map _$BasicSourcedTrackToJson(BasicSourcedTrack instance) => { 'query': instance.query.toJson(), - 'source': _$AudioSourceEnumMap[instance.source]!, 'info': instance.info.toJson(), + 'source': instance.source, 'sources': instance.sources.map((e) => e.toJson()).toList(), 'siblings': instance.siblings.map((e) => e.toJson()).toList(), }; - -const _$AudioSourceEnumMap = { - AudioSource.youtube: 'youtube', - AudioSource.piped: 'piped', - AudioSource.jiosaavn: 'jiosaavn', - AudioSource.invidious: 'invidious', - AudioSource.dabMusic: 'dabMusic', -}; - -_$TrackSourceQueryImpl _$$TrackSourceQueryImplFromJson(Map json) => - _$TrackSourceQueryImpl( - id: json['id'] as String, - title: json['title'] as String, - artists: - (json['artists'] as List).map((e) => e as String).toList(), - album: json['album'] as String, - durationMs: (json['durationMs'] as num).toInt(), - isrc: json['isrc'] as String, - explicit: json['explicit'] as bool, - ); - -Map _$$TrackSourceQueryImplToJson( - _$TrackSourceQueryImpl instance) => - { - 'id': instance.id, - 'title': instance.title, - 'artists': instance.artists, - 'album': instance.album, - 'durationMs': instance.durationMs, - 'isrc': instance.isrc, - 'explicit': instance.explicit, - }; - -_$TrackSourceInfoImpl _$$TrackSourceInfoImplFromJson(Map json) => - _$TrackSourceInfoImpl( - id: json['id'] as String, - title: json['title'] as String, - artists: json['artists'] as String, - thumbnail: json['thumbnail'] as String, - pageUrl: json['pageUrl'] as String, - durationMs: (json['durationMs'] as num).toInt(), - ); - -Map _$$TrackSourceInfoImplToJson( - _$TrackSourceInfoImpl instance) => - { - 'id': instance.id, - 'title': instance.title, - 'artists': instance.artists, - 'thumbnail': instance.thumbnail, - 'pageUrl': instance.pageUrl, - 'durationMs': instance.durationMs, - }; - -_$TrackSourceImpl _$$TrackSourceImplFromJson(Map json) => _$TrackSourceImpl( - url: json['url'] as String, - quality: $enumDecode(_$SourceQualitiesEnumMap, json['quality']), - codec: $enumDecode(_$SourceCodecsEnumMap, json['codec']), - bitrate: json['bitrate'] as String, - qualityLabel: json['qualityLabel'] as String, - ); - -Map _$$TrackSourceImplToJson(_$TrackSourceImpl instance) => - { - 'url': instance.url, - 'quality': _$SourceQualitiesEnumMap[instance.quality]!, - 'codec': _$SourceCodecsEnumMap[instance.codec]!, - 'bitrate': instance.bitrate, - 'qualityLabel': instance.qualityLabel, - }; - -const _$SourceQualitiesEnumMap = { - SourceQualities.uncompressed: 'uncompressed', - SourceQualities.high: 'high', - SourceQualities.medium: 'medium', - SourceQualities.low: 'low', -}; - -const _$SourceCodecsEnumMap = { - SourceCodecs.m4a: 'm4a', - SourceCodecs.weba: 'weba', - SourceCodecs.mp3: 'mp3', - SourceCodecs.flac: 'flac', -}; diff --git a/lib/modules/library/local_folder/cache_export_dialog.dart b/lib/modules/library/local_folder/cache_export_dialog.dart index fde219c9..4c86a8d5 100644 --- a/lib/modules/library/local_folder/cache_export_dialog.dart +++ b/lib/modules/library/local_folder/cache_export_dialog.dart @@ -7,7 +7,7 @@ import 'package:path/path.dart' as path; import 'package:spotube/extensions/context.dart'; import 'package:spotube/services/logger/logger.dart'; -final codecs = SourceCodecs.values.map((s) => s.name); +const containers = ["m4a", "mp3", "mp4", "ogg", "wav", "flac"]; class LocalFolderCacheExportDialog extends HookConsumerWidget { final Directory exportDir; @@ -29,7 +29,8 @@ class LocalFolderCacheExportDialog extends HookConsumerWidget { final stream = cacheDir.list().where( (event) => event is File && - codecs.contains(path.extension(event.path).replaceAll(".", "")), + containers + .contains(path.extension(event.path).replaceAll(".", "")), ); stream.listen( diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 69262641..5ea690e0 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -21,11 +21,9 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_label.dart'; import 'package:spotube/provider/server/active_track_sources.dart'; import 'package:spotube/provider/volume_provider.dart'; -import 'package:spotube/services/sourced_track/sources/youtube.dart'; - -import 'package:url_launcher/url_launcher_string.dart'; class PlayerView extends HookConsumerWidget { final PanelController panelController; @@ -45,14 +43,7 @@ class PlayerView extends HookConsumerWidget { final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source; final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject; final mediaQuery = MediaQuery.sizeOf(context); - - final activeSourceCodec = useMemoized( - () { - return currentActiveTrackSource - ?.getStreamOfCodec(currentActiveTrackSource.codec); - }, - [currentActiveTrackSource?.sources, currentActiveTrackSource?.codec], - ); + final qualityLabel = ref.watch(audioSourceQualityLabelProvider); final shouldHide = useState(true); @@ -117,22 +108,6 @@ class PlayerView extends HookConsumerWidget { ) ], trailing: [ - if (currentActiveTrackSource is YoutubeSourcedTrack) - TextButton( - size: const ButtonSize(1.2), - leading: Assets.images.logos.songlinkTransparent.image( - width: 20, - height: 20, - color: theme.colorScheme.foreground, - ), - onPressed: () { - final url = - "https://song.link/s/${currentActiveTrack?.id}"; - - launchUrlString(url); - }, - child: Text(context.l10n.song_link), - ), if (!isLocalTrack) Tooltip( tooltip: TooltipContainer( @@ -276,20 +251,19 @@ class PlayerView extends HookConsumerWidget { }), ), const Gap(25), - if (activeSourceCodec != null) - OutlineBadge( - style: const ButtonStyle.outline( - size: ButtonSize.normal, - density: ButtonDensity.dense, - shape: ButtonShape.rectangle, - ).copyWith( - textStyle: (context, states, value) { - return value.copyWith(fontWeight: FontWeight.w500); - }, - ), - leading: const Icon(SpotubeIcons.lightningOutlined), - child: Text(activeSourceCodec.qualityLabel), - ) + OutlineBadge( + style: const ButtonStyle.outline( + size: ButtonSize.normal, + density: ButtonDensity.dense, + shape: ButtonShape.rectangle, + ).copyWith( + textStyle: (context, states, value) { + return value.copyWith(fontWeight: FontWeight.w500); + }, + ), + leading: const Icon(SpotubeIcons.lightningOutlined), + child: Text(qualityLabel), + ) ], ), ), diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index a6c3ae32..7b780143 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -1,60 +1,16 @@ -import 'package:collection/collection.dart'; - import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; -import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; -import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart'; -import 'package:spotube/hooks/utils/use_debounce.dart'; -import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/server/active_track_sources.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/youtube_engine/youtube_engine.dart'; -import 'package:spotube/services/sourced_track/models/video_info.dart'; -import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; -import 'package:spotube/services/sourced_track/sources/youtube.dart'; -import 'package:spotube/utils/service_utils.dart'; - -final sourceInfoToIconMap = { - AudioSource.youtube: - const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)), - AudioSource.jiosaavn: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(90), - image: DecorationImage( - image: Assets.images.logos.jiosaavn.provider(), - fit: BoxFit.cover, - ), - ), - ), - AudioSource.piped: const Icon(SpotubeIcons.piped), - AudioSource.invidious: Container( - height: 18, - width: 18, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(90), - image: DecorationImage( - image: Assets.images.logos.invidious.provider(), - fit: BoxFit.cover, - ), - ), - ), -}; class SiblingTracksSheet extends HookConsumerWidget { final bool floating; @@ -65,94 +21,21 @@ class SiblingTracksSheet extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final theme = Theme.of(context); - final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); - final preferences = ref.watch(userPreferencesProvider); - final youtubeEngine = ref.watch(youtubeEngineProvider); + final controller = useScrollController(); - final isLoading = useState(false); - final isSearching = useState(false); - final searchMode = useState(preferences.searchMode); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final activeTrackSources = ref.watch(activeTrackSourcesProvider); final activeTrackNotifier = activeTrackSources.asData?.value?.notifier; final activeTrack = activeTrackSources.asData?.value?.track; final activeTrackSource = activeTrackSources.asData?.value?.source; - final title = ServiceUtils.getTitle( - activeTrack?.name ?? "", - artists: activeTrack?.artists.map((e) => e.name).toList() ?? [], - onlyCleanArtist: true, - ).trim(); - - final defaultSearchTerm = - "$title - ${activeTrack?.artists.asString() ?? ""}"; - final searchController = useShadcnTextEditingController( - text: defaultSearchTerm, - ); - - final searchTerm = useDebounce( - useValueListenable(searchController).text, - ); - - final controller = useScrollController(); - - final searchRequest = useMemoized(() async { - if (searchTerm.trim().isEmpty || activeTrackSource == null) { - return []; - } - if (preferences.audioSource == AudioSource.jiosaavn) { - final resultsJioSaavn = - await jiosaavnClient.search.songs(searchTerm.trim()); - final results = await Future.wait( - resultsJioSaavn.results.mapIndexed((i, song) async { - final siblingType = JioSaavnSourcedTrack.toSiblingType(song); - return siblingType.info; - })); - - final activeSourceInfo = activeTrackSource.info; - - return results - ..removeWhere((element) => element.id == activeSourceInfo.id) - ..insert( - 0, - activeSourceInfo, - ); - } else { - final resultsYt = await youtubeEngine.searchVideos(searchTerm.trim()); - - final searchResults = await Future.wait( - resultsYt - .map(YoutubeVideoInfo.fromVideo) - .mapIndexed((i, video) async { - if (!context.mounted) return null; - final siblingType = - await YoutubeSourcedTrack.toSiblingType(i, video, ref); - return siblingType.info; - }) - .whereType>() - .toList(), - ); - final activeSourceInfo = activeTrackSource.info; - return searchResults - ..removeWhere((element) => element.id == activeSourceInfo.id) - ..insert(0, activeSourceInfo); - } - }, [ - searchTerm, - searchMode.value, - activeTrack, - activeTrackSource, - preferences.audioSource, - youtubeEngine, - ]); - - final siblings = useMemoized( + final siblings = useMemoized>( () => !isFetchingActiveTrack ? [ if (activeTrackSource != null) activeTrackSource.info, ...?activeTrackSource?.siblings, ] - : [], + : [], [activeTrackSource, isFetchingActiveTrack], ); @@ -166,74 +49,6 @@ class SiblingTracksSheet extends HookConsumerWidget { return null; }, [activeTrack, previousActiveTrack]); - final itemBuilder = useCallback( - (TrackSourceInfo sourceInfo, AudioSource source) { - final icon = sourceInfoToIconMap[source]; - return ButtonTile( - style: ButtonVariance.ghost, - padding: const EdgeInsets.symmetric(horizontal: 8), - title: Text( - sourceInfo.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - leading: UniversalImage( - path: sourceInfo.thumbnail, - height: 60, - width: 60, - ), - trailing: Text(Duration(milliseconds: sourceInfo.durationMs) - .toHumanReadableString()), - subtitle: Row( - children: [ - if (icon != null) icon, - Flexible( - child: Text( - " • ${sourceInfo.artists}", - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - enabled: !isFetchingActiveTrack && !isLoading.value, - selected: !isFetchingActiveTrack && - sourceInfo.id == activeTrackSource?.info.id, - onPressed: () async { - if (!isFetchingActiveTrack && - sourceInfo.id != activeTrackSource?.info.id) { - try { - isLoading.value = true; - await activeTrackNotifier?.swapWithSibling(sourceInfo); - await ref.read(audioPlayerProvider.notifier).swapActiveSource(); - - if (context.mounted) { - if (MediaQuery.sizeOf(context).mdAndUp) { - closeOverlay(context); - } else { - closeDrawer(context); - } - } - } finally { - if (context.mounted) { - isLoading.value = false; - } - } - } - }, - ); - }, - [ - activeTrackSource, - activeTrackNotifier, - siblings, - isFetchingActiveTrack, - isLoading.value, - ], - ); - - final scale = context.theme.scaling; - return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, @@ -244,72 +59,16 @@ class SiblingTracksSheet extends HookConsumerWidget { spacing: 5, children: [ AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: !isSearching.value - ? Text( - context.l10n.alternative_track_sources, - ).bold() - : ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 320 * scale, - maxHeight: 38 * scale, - ), - child: TextField( - autofocus: true, - controller: searchController, - placeholder: Text(context.l10n.search), - style: theme.typography.bold, - ), - ), - ), - const Spacer(), - if (!isSearching.value) ...[ - IconButton.outline( - icon: const Icon(SpotubeIcons.search, size: 18), - onPressed: () { - isSearching.value = true; - }, - ), - if (!floating) const BackButton(icon: SpotubeIcons.angleDown) - ] else ...[ - if (preferences.audioSource == AudioSource.piped) - IconButton.outline( - icon: const Icon(SpotubeIcons.filter, size: 18), - onPressed: () { - showPopover( - context: context, - alignment: Alignment.bottomRight, - builder: (context) { - return DropdownMenu( - children: SearchMode.values - .map( - (e) => MenuButton( - onPressed: (context) { - searchMode.value = e; - }, - enabled: searchMode.value != e, - child: Text(e.label), - ), - ) - .toList(), - ); - }, - ); - }, - ), - IconButton.outline( - icon: const Icon(SpotubeIcons.close, size: 18), - onPressed: () { - isSearching.value = false; - }, - ), - ] + duration: const Duration(milliseconds: 300), + child: Text( + context.l10n.alternative_track_sources, + ).bold()), ], ), ), AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: isLoading.value + child: activeTrackSources.isLoading ? const SizedBox( width: double.infinity, child: LinearProgressIndicator(), @@ -323,42 +82,62 @@ class SiblingTracksSheet extends HookConsumerWidget { FadeTransition(opacity: animation, child: child), child: InterScrollbar( controller: controller, - child: switch (isSearching.value) { - false => ListView.separated( - padding: const EdgeInsets.all(8.0), - controller: controller, - itemCount: siblings.length, - separatorBuilder: (context, index) => const Gap(8), - itemBuilder: (context, index) => itemBuilder( - siblings[index], - activeTrackSource!.source, - ), - ), - true => FutureBuilder( - future: searchRequest, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Center( - child: Text(snapshot.error.toString()), - ); - } else if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator()); - } + child: ListView.separated( + padding: const EdgeInsets.all(8.0), + controller: controller, + itemCount: siblings.length, + separatorBuilder: (context, index) => const Gap(8), + itemBuilder: (context, index) { + final sourceInfo = siblings[index]; - return ListView.separated( - padding: const EdgeInsets.all(8.0), - controller: controller, - itemCount: snapshot.data!.length, - separatorBuilder: (context, index) => const Gap(8), - itemBuilder: (context, index) => itemBuilder( - snapshot.data![index], - preferences.audioSource, - ), - ); + return ButtonTile( + style: ButtonVariance.ghost, + padding: const EdgeInsets.symmetric(horizontal: 8), + title: Text( + sourceInfo.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + leading: sourceInfo.thumbnail != null + ? UniversalImage( + path: sourceInfo.thumbnail!, + height: 60, + width: 60, + ) + : null, + trailing: + Text(sourceInfo.duration.toHumanReadableString()), + subtitle: Flexible( + child: Text( + sourceInfo.artists.join(", "), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + enabled: !isFetchingActiveTrack, + selected: !isFetchingActiveTrack && + sourceInfo.id == activeTrackSource?.info.id, + onPressed: () async { + if (!isFetchingActiveTrack && + sourceInfo.id != activeTrackSource?.info.id) { + await activeTrackNotifier + ?.swapWithSibling(sourceInfo); + await ref + .read(audioPlayerProvider.notifier) + .swapActiveSource(); + + if (context.mounted) { + if (MediaQuery.sizeOf(context).mdAndUp) { + closeOverlay(context); + } else { + closeDrawer(context); + } + } + } }, - ), - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/getting_started/sections/playback.dart b/lib/pages/getting_started/sections/playback.dart index a6f887cb..699024b1 100644 --- a/lib/pages/getting_started/sections/playback.dart +++ b/lib/pages/getting_started/sections/playback.dart @@ -1,32 +1,11 @@ -import 'package:flutter/material.dart' show Badge; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/ui/button_tile.dart'; -import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -final audioSourceToIconMap = { - AudioSource.youtube: const Icon( - SpotubeIcons.youtube, - color: Colors.red, - size: 20, - ), - AudioSource.piped: const Icon(SpotubeIcons.piped, size: 20), - AudioSource.invidious: ClipRRect( - borderRadius: BorderRadius.circular(26), - child: Assets.images.logos.invidious.image(width: 26, height: 26), - ), - AudioSource.jiosaavn: - Assets.images.logos.jiosaavn.image(width: 20, height: 20), - AudioSource.dabMusic: - Assets.images.logos.dabMusic.image(width: 20, height: 20), -}; - class GettingStartedPagePlaybackSection extends HookConsumerWidget { final VoidCallback onNext; final VoidCallback onPrevious; @@ -42,19 +21,19 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget { final preferences = ref.watch(userPreferencesProvider); final preferencesNotifier = ref.read(userPreferencesProvider.notifier); - final audioSourceToDescription = useMemoized( - () => { - AudioSource.youtube: "${context.l10n.youtube_source_description}\n" - "${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}", - AudioSource.piped: context.l10n.piped_source_description, - AudioSource.jiosaavn: - "${context.l10n.jiosaavn_source_description}\n" - "${context.l10n.highest_quality("320kbps mp4")}", - AudioSource.invidious: context.l10n.invidious_source_description, - AudioSource.dabMusic: "${context.l10n.dab_music_source_description}\n" - "${context.l10n.highest_quality("320kbps mp3, HI-RES 24bit 44.1kHz-96kHz flac")}", - }, - []); + // final audioSourceToDescription = useMemoized( + // () => { + // AudioSource.youtube: "${context.l10n.youtube_source_description}\n" + // "${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}", + // AudioSource.piped: context.l10n.piped_source_description, + // AudioSource.jiosaavn: + // "${context.l10n.jiosaavn_source_description}\n" + // "${context.l10n.highest_quality("320kbps mp4")}", + // AudioSource.invidious: context.l10n.invidious_source_description, + // AudioSource.dabMusic: "${context.l10n.dab_music_source_description}\n" + // "${context.l10n.highest_quality("320kbps mp3, HI-RES 24bit 44.1kHz-96kHz flac")}", + // }, + // []); return Center( child: BlurCard( @@ -69,44 +48,44 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget { ], ), const Gap(16), - Align( - alignment: Alignment.centerLeft, - child: Text(context.l10n.select_audio_source).semiBold().large(), - ), - const Gap(16), - RadioGroup( - value: preferences.audioSource, - onChanged: (value) { - preferencesNotifier.setAudioSource(value); - }, - child: Wrap( - spacing: 6, - runSpacing: 6, - children: [ - for (final source in AudioSource.values) - Badge( - isLabelVisible: source == AudioSource.dabMusic, - label: const Text("NEW"), - backgroundColor: Colors.lime[300], - textColor: Colors.black, - child: RadioCard( - value: source, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - audioSourceToIconMap[source]!, - Text(source.label), - ], - ), - ), - ), - ], - ), - ), - const Gap(16), - Text( - audioSourceToDescription[preferences.audioSource]!, - ).small().muted(), + // Align( + // alignment: Alignment.centerLeft, + // child: Text(context.l10n.select_audio_source).semiBold().large(), + // ), + // const Gap(16), + // RadioGroup( + // value: preferences.audioSource, + // onChanged: (value) { + // preferencesNotifier.setAudioSource(value); + // }, + // child: Wrap( + // spacing: 6, + // runSpacing: 6, + // children: [ + // for (final source in AudioSource.values) + // Badge( + // isLabelVisible: source == AudioSource.dabMusic, + // label: const Text("NEW"), + // backgroundColor: Colors.lime[300], + // textColor: Colors.black, + // child: RadioCard( + // value: source, + // child: Column( + // mainAxisSize: MainAxisSize.min, + // children: [ + // audioSourceToIconMap[source]!, + // Text(source.label), + // ], + // ), + // ), + // ), + // ], + // ), + // ), + // const Gap(16), + // Text( + // audioSourceToDescription[preferences.audioSource]!, + // ).small().muted(), const Gap(16), ButtonTile( title: Text(context.l10n.endless_playback), diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 77eaa0c5..29a8c2ea 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -1,29 +1,18 @@ -import 'dart:io'; - import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show ListTile; -import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:piped_client/piped_client.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/modules/settings/playback/edit_connect_port_dialog.dart'; -import 'package:spotube/modules/settings/playback/edit_instance_url_dialog.dart'; -import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; +import 'package:spotube/modules/settings/playback/edit_connect_port_dialog.dart'; +import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart'; -import 'package:spotube/provider/audio_player/sources/invidious_instances_provider.dart'; -import 'package:spotube/provider/audio_player/sources/piped_instances_provider.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/kv_store/kv_store.dart'; -import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/utils/platform.dart'; class SettingsPlaybackSection extends HookConsumerWidget { @@ -33,332 +22,78 @@ class SettingsPlaybackSection extends HookConsumerWidget { Widget build(BuildContext context, ref) { final preferences = ref.watch(userPreferencesProvider); final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); + final sourcePresets = ref.watch(audioSourcePresetsProvider); + final sourcePresetsNotifier = + ref.watch(audioSourcePresetsProvider.notifier); final theme = Theme.of(context); return SectionCardWithHeading( heading: context.l10n.playback, children: [ - AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.audioQuality), - title: Text(context.l10n.audio_quality), - value: preferences.audioQuality, - options: [ - if (preferences.audioSource == AudioSource.dabMusic) - SelectItemButton( - value: SourceQualities.uncompressed, - child: Text(context.l10n.uncompressed), - ), - SelectItemButton( - value: SourceQualities.high, - child: Text(context.l10n.high), - ), - if (preferences.audioSource != AudioSource.dabMusic) ...[ - SelectItemButton( - value: SourceQualities.medium, - child: Text(context.l10n.medium), - ), - SelectItemButton( - value: SourceQualities.low, - child: Text(context.l10n.low), - ), - ] - ], - onChanged: (value) { - if (value != null) { - preferencesNotifier.setAudioQuality(value); - } - }, - ), - AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.api), - title: Text(context.l10n.audio_source), - value: preferences.audioSource, - options: AudioSource.values - .map((e) => SelectItemButton( - value: e, - child: Text(e.label), - )) - .toList(), - onChanged: (value) { - if (value == null) return; - preferencesNotifier.setAudioSource(value); - }, - ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: preferences.audioSource != AudioSource.piped - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: const SizedBox.shrink(), - secondChild: Consumer( - builder: (context, ref, child) { - final instanceList = ref.watch(pipedInstancesFutureProvider); - - return instanceList.when( - data: (data) { - return AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.piped), - title: Text(context.l10n.piped_instance), - subtitle: Text( - "${context.l10n.piped_description}\n" - "${context.l10n.piped_warning}", - ), - value: preferences.pipedInstance, - showValueWhenUnfolded: false, - trailing: [ - Tooltip( - tooltip: TooltipContainer( - child: Text(context.l10n.add_custom_url), - ).call, - child: IconButton.outline( - icon: const Icon(SpotubeIcons.edit), - size: ButtonSize.small, - onPressed: () { - showDialog( - context: context, - barrierColor: Colors.black.withValues(alpha: 0.5), - builder: (context) => - SettingsPlaybackEditInstanceUrlDialog( - title: context.l10n.piped_instance, - initialValue: preferences.pipedInstance, - onSave: (value) { - preferencesNotifier.setPipedInstance(value); - }, - ), - ); - }, - ), - ) - ], - options: [ - if (data - .none((e) => e.apiUrl == preferences.pipedInstance)) - SelectItemButton( - value: preferences.pipedInstance, - child: Text.rich( - TextSpan( - style: theme.typography.xSmall.copyWith( - color: theme.colorScheme.foreground, - ), - children: [ - TextSpan(text: context.l10n.custom), - const TextSpan(text: "\n"), - TextSpan(text: preferences.pipedInstance), - ], - ), - ), - ), - for (final e in data.sortedBy((e) => e.name)) - SelectItemButton( - value: e.apiUrl, - child: RichText( - text: TextSpan( - style: theme.typography.normal.copyWith( - color: theme.colorScheme.foreground, - ), - children: [ - TextSpan( - text: "${e.name.trim()}\n", - ), - TextSpan( - text: e.locations - .map(countryCodeToEmoji) - .join(""), - style: GoogleFonts.notoColorEmoji(), - ), - ], - ), - ), - ), - ], - onChanged: (value) { - if (value != null) { - preferencesNotifier.setPipedInstance(value); - } - }, - ); - }, - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (error, stackTrace) => Text(error.toString()), - ); + if (sourcePresets.presets.isNotEmpty) ...[ + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.api), + title: Text(context.l10n.streaming_music_codec), + value: sourcePresets.selectedStreamingContainerIndex, + options: [ + for (final MapEntry(:key, value: preset) + in sourcePresets.presets.asMap().entries) + SelectItemButton(value: key, child: Text(preset.name)), + ], + onChanged: (value) { + if (value == null) return; + sourcePresetsNotifier.setSelectedStreamingContainerIndex(value); }, ), - ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: preferences.audioSource != AudioSource.invidious - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: const SizedBox.shrink(), - secondChild: Consumer( - builder: (context, ref, child) { - final instanceList = ref.watch(invidiousInstancesProvider); - - return instanceList.when( - data: (data) { - return AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.piped), - title: Text(context.l10n.invidious_instance), - subtitle: Text( - "${context.l10n.invidious_description}\n" - "${context.l10n.invidious_warning}", - ), - trailing: [ - Tooltip( - tooltip: TooltipContainer( - child: Text(context.l10n.add_custom_url), - ).call, - child: IconButton.outline( - icon: const Icon(SpotubeIcons.edit), - size: ButtonSize.small, - onPressed: () { - showDialog( - context: context, - barrierColor: Colors.black.withValues(alpha: 0.5), - builder: (context) => - SettingsPlaybackEditInstanceUrlDialog( - title: context.l10n.invidious_instance, - initialValue: preferences.invidiousInstance, - onSave: (value) { - preferencesNotifier - .setInvidiousInstance(value); - }, - ), - ); - }, - ), - ) - ], - value: preferences.invidiousInstance, - showValueWhenUnfolded: false, - options: [ - if (data.none((e) => - e.details.uri == preferences.invidiousInstance)) - SelectItemButton( - value: preferences.invidiousInstance, - child: Text.rich( - TextSpan( - style: theme.typography.xSmall.copyWith( - color: theme.colorScheme.foreground, - ), - children: [ - TextSpan(text: context.l10n.custom), - const TextSpan(text: "\n"), - TextSpan(text: preferences.invidiousInstance), - ], - ), - ), - ), - for (final e in data.sortedBy((e) => e.name)) - SelectItemButton( - value: e.details.uri, - child: RichText( - text: TextSpan( - style: theme.typography.normal.copyWith( - color: theme.colorScheme.foreground, - ), - children: [ - TextSpan( - text: "${e.name.trim()}\n", - ), - TextSpan( - text: countryCodeToEmoji( - e.details.region, - ), - style: GoogleFonts.notoColorEmoji(), - ), - ], - ), - ), - ), - ], - onChanged: (value) { - if (value != null) { - preferencesNotifier.setInvidiousInstance(value); - } - }, - ); - }, - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (error, stackTrace) => Text(error.toString()), - ); + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.api), + title: const Text("Streaming music quality"), + value: sourcePresets.selectedStreamingQualityIndex, + options: [ + for (final MapEntry(:key, value: quality) in sourcePresets + .presets[sourcePresets.selectedStreamingContainerIndex] + .qualities + .asMap() + .entries) + SelectItemButton(value: key, child: Text(quality.toString())), + ], + onChanged: (value) { + if (value == null) return; + sourcePresetsNotifier.setSelectedStreamingQualityIndex(value); }, ), - ), - switch (preferences.audioSource) { - AudioSource.youtube => AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.engine), - title: Text(context.l10n.youtube_engine), - value: preferences.youtubeClientEngine, - options: YoutubeClientEngine.values - .where((e) => e.isAvailableForPlatform()) - .map((e) => SelectItemButton( - value: e, - child: Text(e.label), - )) - .toList(), - onChanged: (value) async { - if (value == null) return; - if (value == YoutubeClientEngine.ytDlp) { - final customPath = KVStoreService.getYoutubeEnginePath(value); - if (!await YtDlpEngine.isInstalled() && - (customPath == null || - !await File(customPath).exists()) && - context.mounted) { - final hasInstalled = await showDialog( - context: context, - builder: (context) => - YouTubeEngineNotInstalledDialog(engine: value), - ); - if (hasInstalled != true) return; - } - } - preferencesNotifier.setYoutubeClientEngine(value); - }, - ), - AudioSource.piped || - AudioSource.invidious => - AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.search), - title: Text(context.l10n.search_mode), - value: preferences.searchMode, - options: SearchMode.values - .map((e) => SelectItemButton( - value: e, - child: Text(e.label), - )) - .toList(), - onChanged: (value) { - if (value == null) return; - preferencesNotifier.setSearchMode(value); - }, - ), - _ => const SizedBox.shrink(), - }, - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: preferences.searchMode == SearchMode.youtube && - (preferences.audioSource == AudioSource.piped || - preferences.audioSource == AudioSource.youtube || - preferences.audioSource == AudioSource.invidious) - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: ListTile( - leading: const Icon(SpotubeIcons.skip), - title: Text(context.l10n.skip_non_music), - trailing: Switch( - value: preferences.skipNonMusic, - onChanged: (state) { - preferencesNotifier.setSkipNonMusic(state); - }, - ), + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.api), + title: Text(context.l10n.download_music_codec), + value: sourcePresets.selectedDownloadingContainerIndex, + options: [ + for (final MapEntry(:key, value: preset) + in sourcePresets.presets.asMap().entries) + SelectItemButton(value: key, child: Text(preset.name)), + ], + onChanged: (value) { + if (value == null) return; + sourcePresetsNotifier.setSelectedDownloadingContainerIndex(value); + }, ), - secondChild: const SizedBox.shrink(), - ), + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.api), + title: const Text("Downloading music quality"), + value: sourcePresets.selectedStreamingQualityIndex, + options: [ + for (final MapEntry(:key, value: quality) in sourcePresets + .presets[sourcePresets.selectedDownloadingContainerIndex] + .qualities + .asMap() + .entries) + SelectItemButton(value: key, child: Text(quality.toString())), + ], + onChanged: (value) { + if (value == null) return; + sourcePresetsNotifier.setSelectedStreamingQualityIndex(value); + }, + ), + ], ListTile( title: Text(context.l10n.cache_music), subtitle: kIsMobile @@ -402,50 +137,6 @@ class SettingsPlaybackSection extends HookConsumerWidget { onChanged: preferencesNotifier.setNormalizeAudio, ), ), - if (const [AudioSource.jiosaavn, AudioSource.dabMusic] - .contains(preferences.audioSource) == - false) ...[ - AdaptiveSelectTile( - popupConstraints: const BoxConstraints(maxWidth: 300), - secondary: const Icon(SpotubeIcons.stream), - title: Text(context.l10n.streaming_music_codec), - value: preferences.streamMusicCodec, - showValueWhenUnfolded: false, - options: SourceCodecs.values - .map((e) => SelectItemButton( - value: e, - child: Text( - e.label, - style: theme.typography.small, - ), - )) - .toList(), - onChanged: (value) { - if (value == null) return; - preferencesNotifier.setStreamMusicCodec(value); - }, - ), - AdaptiveSelectTile( - popupConstraints: const BoxConstraints(maxWidth: 300), - secondary: const Icon(SpotubeIcons.file), - title: Text(context.l10n.download_music_codec), - value: preferences.downloadMusicCodec, - showValueWhenUnfolded: false, - options: SourceCodecs.values - .map((e) => SelectItemButton( - value: e, - child: Text( - e.label, - style: theme.typography.small, - ), - )) - .toList(), - onChanged: (value) { - if (value == null) return; - preferencesNotifier.setDownloadMusicCodec(value); - }, - ), - ], ListTile( leading: const Icon(SpotubeIcons.repeat), title: Text(context.l10n.endless_playback), diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 5db28125..2d569ab5 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -12,7 +12,7 @@ import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/discord_provider.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; @@ -164,8 +164,8 @@ class AudioPlayerNotifier extends Notifier { final tracks = []; for (final media in playlist.medias) { - final trackQuery = TrackSourceQuery.parseUri(media.uri); - final track = trackGroupedById[trackQuery.id]?.firstOrNull; + final track = trackGroupedById[SpotubeMedia.media(media).track.id] + ?.firstOrNull; if (track != null) { tracks.add(track); } @@ -401,9 +401,8 @@ class AudioPlayerNotifier extends Notifier { final intendedActiveTrack = medias.elementAt(initialIndex); if (intendedActiveTrack.track is! SpotubeLocalTrackObject) { await ref.read( - trackSourcesProvider( - TrackSourceQuery.fromTrack( - intendedActiveTrack.track as SpotubeFullTrackObject), + sourcedTrackProvider( + intendedActiveTrack.track as SpotubeFullTrackObject, ).future, ); } diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index 507e9d49..eff13134 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -3,14 +3,13 @@ import 'dart:math'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/discord_provider.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/metadata_plugin/core/scrobble.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; import 'package:spotube/provider/skip_segments/skip_segments.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -156,9 +155,7 @@ class AudioPlayerStreamListeners { try { await ref.read( - trackSourcesProvider( - TrackSourceQuery.fromTrack(nextTrack as SpotubeFullTrackObject), - ).future, + sourcedTrackProvider(nextTrack as SpotubeFullTrackObject).future, ); } finally { lastTrack = nextTrack.id; diff --git a/lib/provider/audio_player/querying_track_info.dart b/lib/provider/audio_player/querying_track_info.dart index ce99b261..06e9653c 100644 --- a/lib/provider/audio_player/querying_track_info.dart +++ b/lib/provider/audio_player/querying_track_info.dart @@ -1,8 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; final queryingTrackInfoProvider = Provider((ref) { final audioPlayer = ref.watch(audioPlayerProvider); @@ -16,10 +15,9 @@ final queryingTrackInfoProvider = Provider((ref) { } return ref - .watch(trackSourcesProvider( - TrackSourceQuery.fromTrack( - audioPlayer.activeTrack! as SpotubeFullTrackObject, - ), - )) + .watch( + sourcedTrackProvider( + audioPlayer.activeTrack! as SpotubeFullTrackObject), + ) .isLoading; }); diff --git a/lib/provider/audio_player/sources/invidious_instances_provider.dart b/lib/provider/audio_player/sources/invidious_instances_provider.dart deleted file mode 100644 index c04ac765..00000000 --- a/lib/provider/audio_player/sources/invidious_instances_provider.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/services/sourced_track/sources/invidious.dart'; - -final invidiousInstancesProvider = FutureProvider((ref) async { - final invidious = ref.watch(invidiousProvider); - - final instances = await invidious.instances(); - - return instances - .where((instance) => instance.details.type == "https") - .toList(); -}); diff --git a/lib/provider/audio_player/sources/piped_instances_provider.dart b/lib/provider/audio_player/sources/piped_instances_provider.dart deleted file mode 100644 index 3c5d5f04..00000000 --- a/lib/provider/audio_player/sources/piped_instances_provider.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:spotube/services/logger/logger.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:piped_client/piped_client.dart'; -import 'package:spotube/services/sourced_track/sources/piped.dart'; - -final pipedInstancesFutureProvider = FutureProvider>( - (ref) async { - try { - final pipedClient = ref.watch(pipedProvider); - - return await pipedClient.instanceList(); - } catch (e, stack) { - AppLogger.reportError(e, stack); - return []; - } - }, -); diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index d0112765..bc1de813 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'dart:io'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -26,7 +26,10 @@ class DownloadManagerProvider extends ChangeNotifier { final (:request, :status) = event; final sourcedTrack = $history.firstWhereOrNull( - (element) => element.getUrlOfCodec(downloadCodec) == request.url, + (element) => + element.getUrlOfQuality( + downloadContainer, downloadQualityIndex) == + request.url, ); if (sourcedTrack == null) return; final track = $backHistory.firstWhereOrNull( @@ -48,7 +51,8 @@ class DownloadManagerProvider extends ChangeNotifier { //? WebA audiotagging is not supported yet //? Although in future by converting weba to opus & then tagging it //? is possible using vorbis comments - downloadCodec == SourceCodecs.weba) { + downloadContainer.name == "weba" || + downloadContainer.name == "webm") { return; } @@ -87,8 +91,13 @@ class DownloadManagerProvider extends ChangeNotifier { String get downloadDirectory => ref.read(userPreferencesProvider.select((s) => s.downloadLocation)); - SourceCodecs get downloadCodec => - ref.read(userPreferencesProvider.select((s) => s.downloadMusicCodec)); + SpotubeAudioSourceContainerPreset get downloadContainer => ref.read( + audioSourcePresetsProvider + .select((s) => s.presets[s.selectedDownloadingContainerIndex]), + ); + + int get downloadQualityIndex => ref.read(audioSourcePresetsProvider + .select((s) => s.selectedDownloadingQualityIndex)); int get $downloadCount => dl .getAllDownloads() @@ -107,7 +116,7 @@ class DownloadManagerProvider extends ChangeNotifier { String getTrackFileUrl(SourcedTrack track) { final name = - "${track.query.title} - ${track.query.artists.join(", ")}.${downloadCodec.name}"; + "${track.query.name} - ${track.query.artists.join(", ")}.${downloadContainer.name}"; return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name)); } @@ -129,13 +138,16 @@ class DownloadManagerProvider extends ChangeNotifier { download.status.value == DownloadStatus.queued, ) .map((e) => e.request.url) - .contains(sourcedTrack.getUrlOfCodec(downloadCodec)!); + .contains(sourcedTrack.getUrlOfQuality( + downloadContainer, + downloadQualityIndex, + )!); } /// For singular downloads Future addToQueue(SpotubeFullTrackObject track) async { final sourcedTrack = await ref.read( - trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future, + sourcedTrackProvider(track).future, ); final savePath = getTrackFileUrl(sourcedTrack); @@ -149,9 +161,9 @@ class DownloadManagerProvider extends ChangeNotifier { await oldFile.rename("$savePath.old"); } - if (sourcedTrack.codec == downloadCodec) { + if (sourcedTrack.qualityPreset == downloadContainer) { final downloadTask = await dl.addDownload( - sourcedTrack.getUrlOfCodec(downloadCodec)!, + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!, savePath, ); if (downloadTask != null) { @@ -159,18 +171,13 @@ class DownloadManagerProvider extends ChangeNotifier { } } else { $backHistory.add(track); - final sourcedTrack = await ref - .read( - trackSourcesProvider( - TrackSourceQuery.fromTrack(track), - ).future, - ) - .then((d) { + final sourcedTrack = + await ref.read(sourcedTrackProvider(track).future).then((d) { $backHistory.remove(track); return d; }); final downloadTask = await dl.addDownload( - sourcedTrack.getUrlOfCodec(downloadCodec)!, + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!, savePath, ); if (downloadTask != null) { @@ -203,18 +210,21 @@ class DownloadManagerProvider extends ChangeNotifier { Future removeFromQueue(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - await dl.removeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); + await dl.removeDownload( + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); $history.remove(sourcedTrack); } Future pause(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - return dl.pauseDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); + return dl.pauseDownload( + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); } Future resume(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - return dl.resumeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); + return dl.resumeDownload( + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); } Future retry(SpotubeFullTrackObject track) { @@ -223,7 +233,8 @@ class DownloadManagerProvider extends ChangeNotifier { void cancel(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - return dl.cancelDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); + return dl.cancelDownload( + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); } void cancelAll() { @@ -241,9 +252,7 @@ class DownloadManagerProvider extends ChangeNotifier { return historicTrack; } - final sourcedTrack = await ref.read( - trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future, - ); + final sourcedTrack = await ref.read(sourcedTrackProvider(track).future); return sourcedTrack; } @@ -257,7 +266,10 @@ class DownloadManagerProvider extends ChangeNotifier { if (sourcedTrack == null) { return null; } - return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!)?.status; + return dl + .getDownload(sourcedTrack.getUrlOfQuality( + downloadContainer, downloadQualityIndex)!) + ?.status; } ValueNotifier? getProgressNotifier(SpotubeFullTrackObject track) { @@ -267,7 +279,10 @@ class DownloadManagerProvider extends ChangeNotifier { if (sourcedTrack == null) { return null; } - return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!)?.progress; + return dl + .getDownload(sourcedTrack.getUrlOfQuality( + downloadContainer, downloadQualityIndex)!) + ?.progress; } } diff --git a/lib/provider/metadata_plugin/audio_source/quality_label.dart b/lib/provider/metadata_plugin/audio_source/quality_label.dart new file mode 100644 index 00000000..7d1dc95a --- /dev/null +++ b/lib/provider/metadata_plugin/audio_source/quality_label.dart @@ -0,0 +1,12 @@ +import 'package:riverpod/riverpod.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; + +final audioSourceQualityLabelProvider = Provider((ref) { + final sourceQuality = ref.watch(audioSourcePresetsProvider); + final sourceContainer = + sourceQuality.presets[sourceQuality.selectedStreamingContainerIndex]; + final quality = + sourceContainer.qualities[sourceQuality.selectedStreamingQualityIndex]; + + return "${sourceContainer.name} • ${quality.toString()}"; +}); diff --git a/lib/provider/metadata_plugin/audio_source/quality_presets.dart b/lib/provider/metadata_plugin/audio_source/quality_presets.dart new file mode 100644 index 00000000..9cc7dc44 --- /dev/null +++ b/lib/provider/metadata_plugin/audio_source/quality_presets.dart @@ -0,0 +1,120 @@ +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; + +part 'quality_presets.g.dart'; +part 'quality_presets.freezed.dart'; + +@freezed +class AudioSourcePresetsState with _$AudioSourcePresetsState { + factory AudioSourcePresetsState({ + @Default([]) final List presets, + @Default(0) final int selectedStreamingQualityIndex, + @Default(0) final int selectedStreamingContainerIndex, + @Default(0) final int selectedDownloadingQualityIndex, + @Default(0) final int selectedDownloadingContainerIndex, + }) = _AudioSourcePresetsState; + + factory AudioSourcePresetsState.fromJson(Map json) => + _$AudioSourcePresetsStateFromJson(json); +} + +class AudioSourceAvailableQualityPresetsNotifier + extends Notifier { + @override + build() { + ref.watch(audioSourcePluginProvider); + + _initialize(); + + listenSelf((previous, next) { + final isNewLossless = + next.presets.elementAtOrNull(next.selectedStreamingContainerIndex) + is SpotubeAudioSourceContainerPresetLossless; + final isOldLossless = previous?.presets + .elementAtOrNull(previous.selectedStreamingContainerIndex) + is SpotubeAudioSourceContainerPresetLossless; + if (!isOldLossless && isNewLossless) { + audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB + } else if (isOldLossless && !isNewLossless) { + audioPlayer.setDemuxerBufferSize(4 * 1024 * 1024); // 4MB + } + }); + + return AudioSourcePresetsState(); + } + + void _initialize() async { + final audioSource = await ref.read(audioSourcePluginProvider.future); + final audioSourceConfig = await ref.read( + metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig), + ); + if (audioSource == null || audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + final preferences = await SharedPreferences.getInstance(); + final persistedStateStr = + preferences.getString("audioSourceState-${audioSourceConfig.slug}"); + + if (persistedStateStr != null) { + state = AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr)); + } else { + state = AudioSourcePresetsState( + presets: audioSource.audioSource.supportedPresets, + ); + } + } + + void setSelectedStreamingContainerIndex(int index) { + state = state.copyWith( + selectedStreamingContainerIndex: index, + selectedStreamingQualityIndex: + 0, // Resetting both because it's a different quality + ); + _updatePreferences(); + } + + void setSelectedStreamingQualityIndex(int index) { + state = state.copyWith(selectedStreamingQualityIndex: index); + _updatePreferences(); + } + + void setSelectedDownloadingContainerIndex(int index) { + state = state.copyWith( + selectedDownloadingContainerIndex: index, + selectedDownloadingQualityIndex: + 0, // Resetting both because it's a different quality + ); + _updatePreferences(); + } + + void setSelectedDownloadingQualityIndex(int index) { + state = state.copyWith(selectedDownloadingQualityIndex: index); + _updatePreferences(); + } + + void _updatePreferences() async { + final audioSourceConfig = await ref.read(metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig)); + if (audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + + final preferences = await SharedPreferences.getInstance(); + await preferences.setString( + "audioSourceState-${audioSourceConfig.slug}", + jsonEncode(state), + ); + } +} + +final audioSourcePresetsProvider = NotifierProvider< + AudioSourceAvailableQualityPresetsNotifier, AudioSourcePresetsState>( + () => AudioSourceAvailableQualityPresetsNotifier(), +); diff --git a/lib/provider/metadata_plugin/audio_source/quality_presets.freezed.dart b/lib/provider/metadata_plugin/audio_source/quality_presets.freezed.dart new file mode 100644 index 00000000..a8e0c9f7 --- /dev/null +++ b/lib/provider/metadata_plugin/audio_source/quality_presets.freezed.dart @@ -0,0 +1,289 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'quality_presets.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +AudioSourcePresetsState _$AudioSourcePresetsStateFromJson( + Map json) { + return _AudioSourcePresetsState.fromJson(json); +} + +/// @nodoc +mixin _$AudioSourcePresetsState { + List get presets => + throw _privateConstructorUsedError; + int get selectedStreamingQualityIndex => throw _privateConstructorUsedError; + int get selectedStreamingContainerIndex => throw _privateConstructorUsedError; + int get selectedDownloadingQualityIndex => throw _privateConstructorUsedError; + int get selectedDownloadingContainerIndex => + throw _privateConstructorUsedError; + + /// Serializes this AudioSourcePresetsState to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AudioSourcePresetsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AudioSourcePresetsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AudioSourcePresetsStateCopyWith<$Res> { + factory $AudioSourcePresetsStateCopyWith(AudioSourcePresetsState value, + $Res Function(AudioSourcePresetsState) then) = + _$AudioSourcePresetsStateCopyWithImpl<$Res, AudioSourcePresetsState>; + @useResult + $Res call( + {List presets, + int selectedStreamingQualityIndex, + int selectedStreamingContainerIndex, + int selectedDownloadingQualityIndex, + int selectedDownloadingContainerIndex}); +} + +/// @nodoc +class _$AudioSourcePresetsStateCopyWithImpl<$Res, + $Val extends AudioSourcePresetsState> + implements $AudioSourcePresetsStateCopyWith<$Res> { + _$AudioSourcePresetsStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AudioSourcePresetsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? presets = null, + Object? selectedStreamingQualityIndex = null, + Object? selectedStreamingContainerIndex = null, + Object? selectedDownloadingQualityIndex = null, + Object? selectedDownloadingContainerIndex = null, + }) { + return _then(_value.copyWith( + presets: null == presets + ? _value.presets + : presets // ignore: cast_nullable_to_non_nullable + as List, + selectedStreamingQualityIndex: null == selectedStreamingQualityIndex + ? _value.selectedStreamingQualityIndex + : selectedStreamingQualityIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedStreamingContainerIndex: null == selectedStreamingContainerIndex + ? _value.selectedStreamingContainerIndex + : selectedStreamingContainerIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedDownloadingQualityIndex: null == selectedDownloadingQualityIndex + ? _value.selectedDownloadingQualityIndex + : selectedDownloadingQualityIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedDownloadingContainerIndex: null == + selectedDownloadingContainerIndex + ? _value.selectedDownloadingContainerIndex + : selectedDownloadingContainerIndex // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$AudioSourcePresetsStateImplCopyWith<$Res> + implements $AudioSourcePresetsStateCopyWith<$Res> { + factory _$$AudioSourcePresetsStateImplCopyWith( + _$AudioSourcePresetsStateImpl value, + $Res Function(_$AudioSourcePresetsStateImpl) then) = + __$$AudioSourcePresetsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List presets, + int selectedStreamingQualityIndex, + int selectedStreamingContainerIndex, + int selectedDownloadingQualityIndex, + int selectedDownloadingContainerIndex}); +} + +/// @nodoc +class __$$AudioSourcePresetsStateImplCopyWithImpl<$Res> + extends _$AudioSourcePresetsStateCopyWithImpl<$Res, + _$AudioSourcePresetsStateImpl> + implements _$$AudioSourcePresetsStateImplCopyWith<$Res> { + __$$AudioSourcePresetsStateImplCopyWithImpl( + _$AudioSourcePresetsStateImpl _value, + $Res Function(_$AudioSourcePresetsStateImpl) _then) + : super(_value, _then); + + /// Create a copy of AudioSourcePresetsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? presets = null, + Object? selectedStreamingQualityIndex = null, + Object? selectedStreamingContainerIndex = null, + Object? selectedDownloadingQualityIndex = null, + Object? selectedDownloadingContainerIndex = null, + }) { + return _then(_$AudioSourcePresetsStateImpl( + presets: null == presets + ? _value._presets + : presets // ignore: cast_nullable_to_non_nullable + as List, + selectedStreamingQualityIndex: null == selectedStreamingQualityIndex + ? _value.selectedStreamingQualityIndex + : selectedStreamingQualityIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedStreamingContainerIndex: null == selectedStreamingContainerIndex + ? _value.selectedStreamingContainerIndex + : selectedStreamingContainerIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedDownloadingQualityIndex: null == selectedDownloadingQualityIndex + ? _value.selectedDownloadingQualityIndex + : selectedDownloadingQualityIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedDownloadingContainerIndex: null == + selectedDownloadingContainerIndex + ? _value.selectedDownloadingContainerIndex + : selectedDownloadingContainerIndex // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$AudioSourcePresetsStateImpl implements _AudioSourcePresetsState { + _$AudioSourcePresetsStateImpl( + {final List presets = const [], + this.selectedStreamingQualityIndex = 0, + this.selectedStreamingContainerIndex = 0, + this.selectedDownloadingQualityIndex = 0, + this.selectedDownloadingContainerIndex = 0}) + : _presets = presets; + + factory _$AudioSourcePresetsStateImpl.fromJson(Map json) => + _$$AudioSourcePresetsStateImplFromJson(json); + + final List _presets; + @override + @JsonKey() + List get presets { + if (_presets is EqualUnmodifiableListView) return _presets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_presets); + } + + @override + @JsonKey() + final int selectedStreamingQualityIndex; + @override + @JsonKey() + final int selectedStreamingContainerIndex; + @override + @JsonKey() + final int selectedDownloadingQualityIndex; + @override + @JsonKey() + final int selectedDownloadingContainerIndex; + + @override + String toString() { + return 'AudioSourcePresetsState(presets: $presets, selectedStreamingQualityIndex: $selectedStreamingQualityIndex, selectedStreamingContainerIndex: $selectedStreamingContainerIndex, selectedDownloadingQualityIndex: $selectedDownloadingQualityIndex, selectedDownloadingContainerIndex: $selectedDownloadingContainerIndex)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AudioSourcePresetsStateImpl && + const DeepCollectionEquality().equals(other._presets, _presets) && + (identical(other.selectedStreamingQualityIndex, + selectedStreamingQualityIndex) || + other.selectedStreamingQualityIndex == + selectedStreamingQualityIndex) && + (identical(other.selectedStreamingContainerIndex, + selectedStreamingContainerIndex) || + other.selectedStreamingContainerIndex == + selectedStreamingContainerIndex) && + (identical(other.selectedDownloadingQualityIndex, + selectedDownloadingQualityIndex) || + other.selectedDownloadingQualityIndex == + selectedDownloadingQualityIndex) && + (identical(other.selectedDownloadingContainerIndex, + selectedDownloadingContainerIndex) || + other.selectedDownloadingContainerIndex == + selectedDownloadingContainerIndex)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_presets), + selectedStreamingQualityIndex, + selectedStreamingContainerIndex, + selectedDownloadingQualityIndex, + selectedDownloadingContainerIndex); + + /// Create a copy of AudioSourcePresetsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AudioSourcePresetsStateImplCopyWith<_$AudioSourcePresetsStateImpl> + get copyWith => __$$AudioSourcePresetsStateImplCopyWithImpl< + _$AudioSourcePresetsStateImpl>(this, _$identity); + + @override + Map toJson() { + return _$$AudioSourcePresetsStateImplToJson( + this, + ); + } +} + +abstract class _AudioSourcePresetsState implements AudioSourcePresetsState { + factory _AudioSourcePresetsState( + {final List presets, + final int selectedStreamingQualityIndex, + final int selectedStreamingContainerIndex, + final int selectedDownloadingQualityIndex, + final int selectedDownloadingContainerIndex}) = + _$AudioSourcePresetsStateImpl; + + factory _AudioSourcePresetsState.fromJson(Map json) = + _$AudioSourcePresetsStateImpl.fromJson; + + @override + List get presets; + @override + int get selectedStreamingQualityIndex; + @override + int get selectedStreamingContainerIndex; + @override + int get selectedDownloadingQualityIndex; + @override + int get selectedDownloadingContainerIndex; + + /// Create a copy of AudioSourcePresetsState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AudioSourcePresetsStateImplCopyWith<_$AudioSourcePresetsStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/provider/metadata_plugin/audio_source/quality_presets.g.dart b/lib/provider/metadata_plugin/audio_source/quality_presets.g.dart new file mode 100644 index 00000000..f3d8fd41 --- /dev/null +++ b/lib/provider/metadata_plugin/audio_source/quality_presets.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'quality_presets.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AudioSourcePresetsStateImpl _$$AudioSourcePresetsStateImplFromJson( + Map json) => + _$AudioSourcePresetsStateImpl( + presets: (json['presets'] as List?) + ?.map((e) => SpotubeAudioSourceContainerPreset.fromJson( + Map.from(e as Map))) + .toList() ?? + const [], + selectedStreamingQualityIndex: + (json['selectedStreamingQualityIndex'] as num?)?.toInt() ?? 0, + selectedStreamingContainerIndex: + (json['selectedStreamingContainerIndex'] as num?)?.toInt() ?? 0, + selectedDownloadingQualityIndex: + (json['selectedDownloadingQualityIndex'] as num?)?.toInt() ?? 0, + selectedDownloadingContainerIndex: + (json['selectedDownloadingContainerIndex'] as num?)?.toInt() ?? 0, + ); + +Map _$$AudioSourcePresetsStateImplToJson( + _$AudioSourcePresetsStateImpl instance) => + { + 'presets': instance.presets.map((e) => e.toJson()).toList(), + 'selectedStreamingQualityIndex': instance.selectedStreamingQualityIndex, + 'selectedStreamingContainerIndex': + instance.selectedStreamingContainerIndex, + 'selectedDownloadingQualityIndex': + instance.selectedDownloadingQualityIndex, + 'selectedDownloadingContainerIndex': + instance.selectedDownloadingContainerIndex, + }; diff --git a/lib/provider/server/active_track_sources.dart b/lib/provider/server/active_track_sources.dart index 5b64dc26..603ca0e4 100644 --- a/lib/provider/server/active_track_sources.dart +++ b/lib/provider/server/active_track_sources.dart @@ -1,14 +1,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; final activeTrackSourcesProvider = FutureProvider< ({ SourcedTrack? source, - TrackSourcesNotifier? notifier, + SourcedTrackNotifier? notifier, SpotubeTrackObject track, })?>((ref) async { final audioPlayerState = ref.watch(audioPlayerProvider); @@ -25,13 +24,15 @@ final activeTrackSourcesProvider = FutureProvider< ); } - final trackQuery = TrackSourceQuery.fromTrack( - audioPlayerState.activeTrack! as SpotubeFullTrackObject, + final sourcedTrack = await ref.watch( + sourcedTrackProvider( + audioPlayerState.activeTrack! as SpotubeFullTrackObject, + ).future, ); - - final sourcedTrack = await ref.watch(trackSourcesProvider(trackQuery).future); final sourcedTrackNotifier = ref.watch( - trackSourcesProvider(trackQuery).notifier, + sourcedTrackProvider( + audioPlayerState.activeTrack! as SpotubeFullTrackObject, + ).notifier, ); return ( diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index 7155edca..ec3a98a1 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -11,12 +11,11 @@ import 'package:path/path.dart'; import 'package:shelf/shelf.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/parser/range_headers.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/server/active_track_sources.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; @@ -49,26 +48,30 @@ class ServerPlaybackRoutes { return join( await UserPreferencesNotifier.getMusicCacheDir(), ServiceUtils.sanitizeFilename( - '${track.query.title} - ${track.query.artists.join(",")} (${track.info.id}).${track.codec.name}', + '${track.query.name} - ${track.query.artists.join(",")} (${track.info.id}).${track.qualityPreset!.name}', ), ); } Future _getSourcedTrack( - Request request, String trackId) async { + Request request, + String trackId, + ) async { final track = playlist.tracks.firstWhere((element) => element.id == trackId); final activeSourcedTrack = await ref.read(activeTrackSourcesProvider.future); + + final media = audioPlayer.playlist.medias + .firstWhere((e) => e.uri == request.requestedUri.toString()); + final spotubeMedia = + media is SpotubeMedia ? media : SpotubeMedia.media(media); final sourcedTrack = activeSourcedTrack?.track.id == track.id ? activeSourcedTrack?.source : await ref.read( - trackSourcesProvider( - //! Use [Request.requestedUri] as it contains full https url. - //! [Request.url] will exclude and starts relatively. (streams/... basically) - TrackSourceQuery.parseUri(request.requestedUri.toString()), - ).future, + sourcedTrackProvider(spotubeMedia.track as SpotubeFullTrackObject) + .future, ); return sourcedTrack; @@ -79,7 +82,7 @@ class ServerPlaybackRoutes { SourcedTrack track, ) async { AppLogger.log.i( - "HEAD request for track: ${track.query.title}\n" + "HEAD request for track: ${track.query.name}\n" "Headers: ${request.headers}", ); @@ -91,7 +94,7 @@ class ServerPlaybackRoutes { return dio_lib.Response( statusCode: 200, headers: Headers.fromMap({ - "content-type": ["audio/${track.codec.name}"], + "content-type": ["audio/${track.qualityPreset!.name}"], "content-length": ["$fileLength"], "accept-ranges": ["bytes"], "content-range": ["bytes 0-$fileLength/$fileLength"], @@ -102,7 +105,7 @@ class ServerPlaybackRoutes { String url = track.url ?? await ref - .read(trackSourcesProvider(track.query).notifier) + .read(sourcedTrackProvider(track.query).notifier) .swapWithNextSibling() .then((track) => track.url!); @@ -128,7 +131,7 @@ class ServerPlaybackRoutes { Map headers, ) async { AppLogger.log.i( - "GET request for track: ${track.query.title}\n" + "GET request for track: ${track.query.name}\n" "Headers: ${request.headers}", ); @@ -142,7 +145,7 @@ class ServerPlaybackRoutes { response: dio_lib.Response( statusCode: 200, headers: Headers.fromMap({ - "content-type": ["audio/${track.codec.name}"], + "content-type": ["audio/${track.qualityPreset!.name}"], "content-length": ["$cachedFileLength"], "accept-ranges": ["bytes"], "content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"], @@ -157,7 +160,7 @@ class ServerPlaybackRoutes { String url = track.url ?? await ref - .read(trackSourcesProvider(track.query).notifier) + .read(sourcedTrackProvider(track.query).notifier) .swapWithNextSibling() .then((track) => track.url!); @@ -179,7 +182,7 @@ class ServerPlaybackRoutes { AppLogger.reportError(e, stack); final sourcedTrack = await ref - .read(trackSourcesProvider(track.query).notifier) + .read(sourcedTrackProvider(track.query).notifier) .refreshStreamingUrl(); url = sourcedTrack.url!; @@ -205,11 +208,9 @@ class ServerPlaybackRoutes { ); } - if (headers["range"] == "bytes=0-" && track.codec == SourceCodecs.flac) { - final bufferSize = - userPreferences.audioQuality == SourceQualities.uncompressed - ? 6 * 1024 * 1024 // 6MB for lossless - : 4 * 1024 * 1024; // 4MB for lossy + if (headers["range"] == "bytes=0-" && + track.qualityPreset is SpotubeAudioSourceContainerPresetLossless) { + const bufferSize = 6 * 1024 * 1024; // 6MB for lossless final endRange = min( bufferSize, @@ -227,7 +228,7 @@ class ServerPlaybackRoutes { final res = await dio.get(url, options: options); AppLogger.log.i( - "Response for track: ${track.query.title}\n" + "Response for track: ${track.query.name}\n" "Status Code: ${res.statusCode}\n" "Headers: ${res.headers.map}", ); @@ -261,7 +262,9 @@ class ServerPlaybackRoutes { await trackPartialCacheFile.rename(trackCacheFile.path); } - if (contentRange.total == fileLength && track.codec != SourceCodecs.weba) { + if (contentRange.total == fileLength && + track.qualityPreset!.name != "webm" || + track.qualityPreset!.name != "weba") { final playlistTrack = playlist.tracks.firstWhereOrNull( (element) => element.id == track.query.id, ); diff --git a/lib/provider/server/sourced_track_provider.dart b/lib/provider/server/sourced_track_provider.dart new file mode 100644 index 00000000..7934ecc7 --- /dev/null +++ b/lib/provider/server/sourced_track_provider.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; + +class SourcedTrackNotifier + extends FamilyAsyncNotifier { + @override + FutureOr build(query) { + ref.watch(audioSourcePluginProvider); + ref.watch(audioSourcePresetsProvider); + + return SourcedTrack.fetchFromTrack(query: query, ref: ref); + } + + Future refreshStreamingUrl() async { + return await update((prev) async { + return await prev.refreshStream(); + }); + } + + Future copyWithSibling() async { + return await update((prev) async { + return prev.copyWithSibling(); + }); + } + + Future swapWithSibling( + SpotubeAudioSourceMatchObject sibling, + ) async { + return await update((prev) async { + return await prev.swapWithSibling(sibling) ?? prev; + }); + } + + Future swapWithNextSibling() async { + return await update((prev) async { + return await prev.swapWithSibling(prev.siblings.first) as SourcedTrack; + }); + } +} + +final sourcedTrackProvider = AsyncNotifierProviderFamily( + () => SourcedTrackNotifier(), +); diff --git a/lib/provider/server/track_sources.dart b/lib/provider/server/track_sources.dart deleted file mode 100644 index 24502471..00000000 --- a/lib/provider/server/track_sources.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:async'; - -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; - -class TrackSourcesNotifier - extends FamilyAsyncNotifier { - @override - FutureOr build(query) { - ref.watch(userPreferencesProvider.select((p) => p.audioQuality)); - ref.watch(userPreferencesProvider.select((p) => p.audioSource)); - ref.watch(userPreferencesProvider.select((p) => p.streamMusicCodec)); - ref.watch(userPreferencesProvider.select((p) => p.downloadMusicCodec)); - - return SourcedTrack.fetchFromQuery(query: query, ref: ref); - } - - Future refreshStreamingUrl() async { - return await update((prev) async { - return await prev.refreshStream(); - }); - } - - Future copyWithSibling() async { - return await update((prev) async { - return prev.copyWithSibling(); - }); - } - - Future swapWithSibling(TrackSourceInfo sibling) async { - return await update((prev) async { - return await prev.swapWithSibling(sibling) ?? prev; - }); - } - - Future swapWithNextSibling() async { - return await update((prev) async { - return await prev.swapWithSibling(prev.siblings.first) as SourcedTrack; - }); - } -} - -final trackSourcesProvider = AsyncNotifierProviderFamily( - () => TrackSourcesNotifier(), -); diff --git a/lib/provider/skip_segments/skip_segments.dart b/lib/provider/skip_segments/skip_segments.dart index accccddd..dc06f326 100644 --- a/lib/provider/skip_segments/skip_segments.dart +++ b/lib/provider/skip_segments/skip_segments.dart @@ -86,18 +86,10 @@ final segmentProvider = FutureProvider( if (snapshot == null) return null; final (:track, :source, :notifier) = snapshot; if (track is SpotubeLocalTrackObject) return null; - if (source!.source case AudioSource.jiosaavn) return null; + if (!source!.source.toLowerCase().contains("youtube")) return null; - final skipNonMusic = ref.watch( - userPreferencesProvider.select( - (s) { - final isPipedYTMusicMode = s.audioSource == AudioSource.piped && - s.searchMode == SearchMode.youtubeMusic; - - return s.skipNonMusic && !isPipedYTMusicMode; - }, - ), - ); + final skipNonMusic = + ref.watch(userPreferencesProvider.select((s) => s.skipNonMusic)); if (!skipNonMusic) { return SourcedSegments(segments: [], source: source.info.id); diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 9bc64f4f..0b43d043 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -53,7 +53,6 @@ class UserPreferencesNotifier extends Notifier { } await audioPlayer.setAudioNormalization(state.normalizeAudio); - await _updatePlayerBufferSize(event.audioQuality, state.audioQuality); } catch (e, stack) { AppLogger.reportError(e, stack); } @@ -79,24 +78,6 @@ class UserPreferencesNotifier extends Notifier { }); } - /// Sets audio player's buffer size based on the selected audio quality - /// Uncompressed quality gets a larger buffer size for smoother playback - /// while other qualities use a standard buffer size. - Future _updatePlayerBufferSize( - SourceQualities newQuality, - SourceQualities oldQuality, - ) async { - if (newQuality == SourceQualities.uncompressed) { - audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB - return; - } - - if (oldQuality == SourceQualities.uncompressed && - newQuality != SourceQualities.uncompressed) { - audioPlayer.setDemuxerBufferSize(4 * 1024 * 1024); // 4MB - } - } - Future setData(PreferencesTableCompanion data) async { final db = ref.read(databaseProvider); @@ -137,14 +118,6 @@ class UserPreferencesNotifier extends Notifier { } } - void setStreamMusicCodec(SourceCodecs codec) { - setData(PreferencesTableCompanion(streamMusicCodec: Value(codec))); - } - - void setDownloadMusicCodec(SourceCodecs codec) { - setData(PreferencesTableCompanion(downloadMusicCodec: Value(codec))); - } - void setThemeMode(ThemeMode mode) { setData(PreferencesTableCompanion(themeMode: Value(mode))); } @@ -171,11 +144,6 @@ class UserPreferencesNotifier extends Notifier { setData(PreferencesTableCompanion(checkUpdate: Value(check))); } - void setAudioQuality(SourceQualities quality) { - setData(PreferencesTableCompanion(audioQuality: Value(quality))); - _updatePlayerBufferSize(quality, state.audioQuality); - } - void setDownloadLocation(String downloadDir) { if (downloadDir.isEmpty) return; setData(PreferencesTableCompanion(downloadLocation: Value(downloadDir))); @@ -206,14 +174,6 @@ class UserPreferencesNotifier extends Notifier { setData(PreferencesTableCompanion(locale: Value(locale))); } - void setPipedInstance(String instance) { - setData(PreferencesTableCompanion(pipedInstance: Value(instance))); - } - - void setInvidiousInstance(String instance) { - setData(PreferencesTableCompanion(invidiousInstance: Value(instance))); - } - void setSearchMode(SearchMode mode) { setData(PreferencesTableCompanion(searchMode: Value(mode))); } @@ -222,27 +182,6 @@ class UserPreferencesNotifier extends Notifier { setData(PreferencesTableCompanion(skipNonMusic: Value(skip))); } - void setAudioSource(AudioSource type) { - switch ((type, state.audioQuality)) { - // DAB music only supports high quality/uncompressed streams - case ( - AudioSource.dabMusic, - SourceQualities.low || SourceQualities.medium - ): - setAudioQuality(SourceQualities.high); - break; - // If the user switches from DAB music to other sources and has - // uncompressed quality selected, downgrade to high quality - case (!= AudioSource.dabMusic, SourceQualities.uncompressed): - setAudioQuality(SourceQualities.high); - break; - default: - break; - } - - setData(PreferencesTableCompanion(audioSource: Value(type))); - } - void setYoutubeClientEngine(YoutubeClientEngine engine) { setData(PreferencesTableCompanion(youtubeClientEngine: Value(engine))); } diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 262b9d10..a30fafba 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:spotube/services/audio_player/custom_player.dart'; @@ -22,21 +21,9 @@ class SpotubeMedia extends mk.Media { static String get _host => kIsWindows ? "localhost" : InternetAddress.anyIPv4.address; - static String _queries(SpotubeFullTrackObject track) { - final params = TrackSourceQuery.fromTrack(track).toJson(); - - return params.entries - .map((e) => - "${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value is List ? e.value.join(",") : e.value.toString())}") - .join("&"); - } - final SpotubeTrackObject track; - SpotubeMedia( - this.track, { - Map? extras, - super.httpHeaders, - }) : assert( + SpotubeMedia(this.track) + : assert( track is SpotubeLocalTrackObject || track is SpotubeFullTrackObject, "Track must be a either a local track or a full track object with ISRC", ), @@ -44,8 +31,14 @@ class SpotubeMedia extends mk.Media { super( track is SpotubeLocalTrackObject ? track.path - : "http://$_host:$serverPort/stream/${track.id}?${_queries(track as SpotubeFullTrackObject)}", + : "http://$_host:$serverPort/stream/${track.id}", + extras: track.toJson(), ); + + factory SpotubeMedia.media(Media media) { + assert(media.extras != null, "[Media] must have extra metadata set"); + return SpotubeMedia(SpotubeFullTrackObject.fromJson(media.extras!)); + } } abstract class AudioPlayerInterface { diff --git a/lib/services/sourced_track/exceptions.dart b/lib/services/sourced_track/exceptions.dart index c841e1e2..4817c9fb 100644 --- a/lib/services/sourced_track/exceptions.dart +++ b/lib/services/sourced_track/exceptions.dart @@ -1,12 +1,12 @@ -import 'package:spotube/models/playback/track_sources.dart'; +import 'package:spotube/models/metadata/metadata.dart'; class TrackNotFoundError extends Error { - final TrackSourceQuery track; + final SpotubeTrackObject track; TrackNotFoundError(this.track); @override String toString() { - return '[TrackNotFoundError] ${track.title} - ${track.artists.join(", ")}'; + return '[TrackNotFoundError] ${track.name} - ${track.artists.join(", ")}'; } } diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index 661a8447..76b202da 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -4,13 +4,12 @@ import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/extensions/string.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/logger/logger.dart'; @@ -34,19 +33,7 @@ class SourcedTrack extends BasicSourcedTrack { required super.sources, }); - static String getSearchTerm(SpotubeFullTrackObject track) { - final title = ServiceUtils.getTitle( - track.name, - artists: track.artists.map((e) => e.name).toList(), - onlyCleanArtist: true, - ).trim(); - - assert(title.trim().isNotEmpty, "Title should not be empty"); - - return "$title - ${track.artists.join(", ")}"; - } - - static Future fetchFromQuery({ + static Future fetchFromTrack({ required SpotubeFullTrackObject query, required Ref ref, }) async { @@ -79,22 +66,25 @@ class SourcedTrack extends BasicSourcedTrack { await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( trackId: query.id, - source: jsonEncode(siblings.first), - sourceType: Value(audioSourceConfig.slug), + sourceInfo: Value(jsonEncode(siblings.first)), + sourceType: audioSourceConfig.slug, ), ); + final manifest = await audioSource.audioSource.streams(siblings.first); + return SourcedTrack( ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - info: siblings.first.info, + siblings: siblings.skip(1).toList(), + info: siblings.first, source: audioSourceConfig.slug, - sources: siblings.first.source ?? [], + sources: manifest, query: query, ); } - final item = - SpotubeAudioSourceMatchObject.fromJson(jsonDecode(cachedSource.source)); + final item = SpotubeAudioSourceMatchObject.fromJson( + jsonDecode(cachedSource.sourceInfo), + ); final manifest = await audioSource.audioSource.streams(item); final sourcedTrack = SourcedTrack( @@ -229,8 +219,8 @@ class SourcedTrack extends BasicSourcedTrack { await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( trackId: query.id, - source: jsonEncode(siblings.first), - sourceType: Value(audioSourceConfig.slug), + sourceInfo: Value(jsonEncode(siblings.first)), + sourceType: audioSourceConfig.slug, createdAt: Value(DateTime.now()), ), mode: InsertMode.replace, @@ -298,13 +288,12 @@ class SourcedTrack extends BasicSourcedTrack { } String? get url { - final preferences = ref.read(userPreferencesProvider); + final preferences = ref.read(audioSourcePresetsProvider); - final codec = preferences.audioSource == AudioSource.jiosaavn - ? SourceCodecs.m4a - : preferences.streamMusicCodec; - - return getUrlOfCodec(codec); + return getUrlOfQuality( + preferences.presets[preferences.selectedStreamingContainerIndex], + preferences.selectedStreamingQualityIndex, + ); } /// Returns the URL of the track based on the codec and quality preferences. @@ -384,4 +373,10 @@ class SourcedTrack extends BasicSourcedTrack { ) { return getStreamOfQuality(preset, qualityIndex)?.url; } + + SpotubeAudioSourceContainerPreset? get qualityPreset { + final presetState = ref.read(audioSourcePresetsProvider); + return presetState.presets + .elementAtOrNull(presetState.selectedStreamingContainerIndex); + } } diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 81c4bfe4..738e4033 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -10,11 +10,9 @@ import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart'; import 'package:spotube/modules/root/update_dialog.dart'; -import 'package:spotube/models/lyrics.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:collection/collection.dart'; @@ -189,95 +187,6 @@ abstract class ServiceUtils { return lyrics; } - @Deprecated("In favor spotify lyrics api, this isn't needed anymore") - static const baseUri = "https://www.rentanadviser.com/subtitles"; - - @Deprecated("In favor spotify lyrics api, this isn't needed anymore") - static Future getTimedLyrics(SourcedTrack track) async { - final artistNames = track.query.artists; - final query = getTitle( - track.query.title, - artists: artistNames, - ); - - final searchUri = Uri.parse("$baseUri/subtitles4songs.aspx").replace( - queryParameters: {"q": query}, - ); - - final res = await globalDio.getUri( - searchUri, - options: Options(responseType: ResponseType.plain), - ); - final document = parser.parse(res.data); - final results = - document.querySelectorAll("#tablecontainer table tbody tr td a"); - - final rateSortedResults = results.map((result) { - final title = result.text.trim().toLowerCase(); - int points = 0; - final hasAllArtists = track.query.artists - .every((artist) => title.contains(artist.toLowerCase())); - final hasTrackName = title.contains(track.query.title.toLowerCase()); - final isNotLive = !PrimitiveUtils.containsTextInBracket(title, "live"); - final exactYtMatch = title == track.info.title.toLowerCase(); - if (exactYtMatch) points = 7; - for (final criteria in [hasTrackName, hasAllArtists, isNotLive]) { - if (criteria) points++; - } - return {"result": result, "points": points}; - }).sorted((a, b) => (b["points"] as int).compareTo(a["points"] as int)); - - // not result was found at all - if (rateSortedResults.first["points"] == 0) { - return Future.error("Subtitle lookup failed", StackTrace.current); - } - - final topResult = rateSortedResults.first["result"] as Element; - final subtitleUri = - Uri.parse("$baseUri/${topResult.attributes["href"]}&type=lrc"); - - final lrcDocument = parser.parse((await globalDio.getUri( - subtitleUri, - options: Options(responseType: ResponseType.plain), - )) - .data); - final lrcList = lrcDocument - .querySelector("#ctl00_ContentPlaceHolder1_lbllyrics") - ?.innerHtml - .replaceAll(RegExp(r'

.*

'), "") - .split("
") - .map((e) { - e = e.trim(); - final regexp = RegExp(r'\[.*\]'); - final timeStr = regexp - .firstMatch(e) - ?.group(0) - ?.replaceAll(RegExp(r'\[|\]'), "") - .trim() - .split(":"); - final minuteSeconds = timeStr?.last.split("."); - - return LyricSlice( - time: Duration( - minutes: int.parse(timeStr?.first ?? "0"), - seconds: int.parse(minuteSeconds?.first ?? "0"), - milliseconds: int.parse(minuteSeconds?.last ?? "0"), - ), - text: e.split(regexp).last); - }).toList() ?? - []; - - final subtitle = SubtitleSimple( - name: topResult.text.trim(), - uri: subtitleUri, - lyrics: lrcList, - rating: rateSortedResults.first["points"] as int, - provider: "Rent An Adviser", - ); - - return subtitle; - } - static DateTime parseSpotifyAlbumDate(SpotubeFullAlbumObject? album) { if (album == null) { return DateTime.parse("1975-01-01"); diff --git a/pubspec.lock b/pubspec.lock index 8623af4e..0ae02b4c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -467,14 +467,6 @@ packages: url: "https://github.com/KRTirtho/dab_music_api.git" source: git version: "0.1.0" - dart_des: - dependency: transitive - description: - name: dart_des - sha256: "0a66afb8883368c824497fd2a1fd67bdb1a785965a3956728382c03d40747c33" - url: "https://pub.dev" - source: hosted - version: "1.0.2" dart_mappable: dependency: transitive description: @@ -1408,14 +1400,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" - invidious: - dependency: "direct main" - description: - name: invidious - sha256: "0da8ebc4c4110057f03302bbd54514b10642154d7be569e7994172f2202dcfe8" - url: "https://pub.dev" - source: hosted - version: "0.1.2" io: dependency: "direct dev" description: @@ -1440,14 +1424,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" - jiosaavn: - dependency: "direct main" - description: - name: jiosaavn - sha256: b6bde15c56398ebfd439825a64fb540a265773d1a518ba103e79988e13d16e1d - url: "https://pub.dev" - source: hosted - version: "0.1.1" jovial_misc: dependency: transitive description: @@ -1935,14 +1911,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.4" - piped_client: - dependency: "direct main" - description: - name: piped_client - sha256: "947613e2a8d368b72cb36473de2c5c2784e4e72b2d3f17e5a5181b98b1a5436e" - url: "https://pub.dev" - source: hosted - version: "0.1.2" pixel_snap: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4087bc0d..812e690f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,8 +81,6 @@ dependencies: http: ^1.2.1 image_picker: ^1.1.0 intl: any - invidious: ^0.1.2 - jiosaavn: ^0.1.1 json_annotation: ^4.8.1 local_notifier: ^0.1.6 logger: ^2.0.2 @@ -104,7 +102,6 @@ dependencies: path: ^1.9.0 path_provider: ^2.1.3 permission_handler: ^11.3.1 - piped_client: ^0.1.2 riverpod: ^2.5.1 scrobblenaut: git: diff --git a/test/drift/app_db/generated/schema_v10.dart b/test/drift/app_db/generated/schema_v10.dart index 36cc2a6b..2811ad02 100644 --- a/test/drift/app_db/generated/schema_v10.dart +++ b/test/drift/app_db/generated/schema_v10.dart @@ -552,16 +552,6 @@ class PreferencesTable extends Table type: DriftSqlType.string, requiredDuringInsert: false, defaultValue: const Constant("")); - late final GeneratedColumn pipedInstance = GeneratedColumn( - 'piped_instance', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("https://pipedapi.kavin.rocks")); - late final GeneratedColumn invidiousInstance = - GeneratedColumn('invidious_instance', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("https://inv.nadeko.net")); late final GeneratedColumn themeMode = GeneratedColumn( 'theme_mode', aliasedName, false, type: DriftSqlType.string, @@ -626,8 +616,6 @@ class PreferencesTable extends Table searchMode, downloadLocation, localLibraryLocation, - pipedInstance, - invidiousInstance, themeMode, audioSourceId, youtubeClientEngine, @@ -681,10 +669,6 @@ class PreferencesTable extends Table localLibraryLocation: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}local_library_location'])!, - pipedInstance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, - invidiousInstance: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}invidious_instance'])!, themeMode: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}theme_mode'])!, audioSourceId: attachedDatabase.typeMapping @@ -729,8 +713,6 @@ class PreferencesTableData extends DataClass final String searchMode; final String downloadLocation; final String localLibraryLocation; - final String pipedInstance; - final String invidiousInstance; final String themeMode; final String? audioSourceId; final String youtubeClientEngine; @@ -756,8 +738,6 @@ class PreferencesTableData extends DataClass required this.searchMode, required this.downloadLocation, required this.localLibraryLocation, - required this.pipedInstance, - required this.invidiousInstance, required this.themeMode, this.audioSourceId, required this.youtubeClientEngine, @@ -785,8 +765,6 @@ class PreferencesTableData extends DataClass map['search_mode'] = Variable(searchMode); map['download_location'] = Variable(downloadLocation); map['local_library_location'] = Variable(localLibraryLocation); - map['piped_instance'] = Variable(pipedInstance); - map['invidious_instance'] = Variable(invidiousInstance); map['theme_mode'] = Variable(themeMode); if (!nullToAbsent || audioSourceId != null) { map['audio_source_id'] = Variable(audioSourceId); @@ -818,8 +796,6 @@ class PreferencesTableData extends DataClass searchMode: Value(searchMode), downloadLocation: Value(downloadLocation), localLibraryLocation: Value(localLibraryLocation), - pipedInstance: Value(pipedInstance), - invidiousInstance: Value(invidiousInstance), themeMode: Value(themeMode), audioSourceId: audioSourceId == null && nullToAbsent ? const Value.absent() @@ -854,8 +830,6 @@ class PreferencesTableData extends DataClass downloadLocation: serializer.fromJson(json['downloadLocation']), localLibraryLocation: serializer.fromJson(json['localLibraryLocation']), - pipedInstance: serializer.fromJson(json['pipedInstance']), - invidiousInstance: serializer.fromJson(json['invidiousInstance']), themeMode: serializer.fromJson(json['themeMode']), audioSourceId: serializer.fromJson(json['audioSourceId']), youtubeClientEngine: @@ -887,8 +861,6 @@ class PreferencesTableData extends DataClass 'searchMode': serializer.toJson(searchMode), 'downloadLocation': serializer.toJson(downloadLocation), 'localLibraryLocation': serializer.toJson(localLibraryLocation), - 'pipedInstance': serializer.toJson(pipedInstance), - 'invidiousInstance': serializer.toJson(invidiousInstance), 'themeMode': serializer.toJson(themeMode), 'audioSourceId': serializer.toJson(audioSourceId), 'youtubeClientEngine': serializer.toJson(youtubeClientEngine), @@ -917,8 +889,6 @@ class PreferencesTableData extends DataClass String? searchMode, String? downloadLocation, String? localLibraryLocation, - String? pipedInstance, - String? invidiousInstance, String? themeMode, Value audioSourceId = const Value.absent(), String? youtubeClientEngine, @@ -944,8 +914,6 @@ class PreferencesTableData extends DataClass searchMode: searchMode ?? this.searchMode, downloadLocation: downloadLocation ?? this.downloadLocation, localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, - pipedInstance: pipedInstance ?? this.pipedInstance, - invidiousInstance: invidiousInstance ?? this.invidiousInstance, themeMode: themeMode ?? this.themeMode, audioSourceId: audioSourceId.present ? audioSourceId.value : this.audioSourceId, @@ -997,12 +965,6 @@ class PreferencesTableData extends DataClass localLibraryLocation: data.localLibraryLocation.present ? data.localLibraryLocation.value : this.localLibraryLocation, - pipedInstance: data.pipedInstance.present - ? data.pipedInstance.value - : this.pipedInstance, - invidiousInstance: data.invidiousInstance.present - ? data.invidiousInstance.value - : this.invidiousInstance, themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, audioSourceId: data.audioSourceId.present ? data.audioSourceId.value @@ -1045,8 +1007,6 @@ class PreferencesTableData extends DataClass ..write('searchMode: $searchMode, ') ..write('downloadLocation: $downloadLocation, ') ..write('localLibraryLocation: $localLibraryLocation, ') - ..write('pipedInstance: $pipedInstance, ') - ..write('invidiousInstance: $invidiousInstance, ') ..write('themeMode: $themeMode, ') ..write('audioSourceId: $audioSourceId, ') ..write('youtubeClientEngine: $youtubeClientEngine, ') @@ -1077,8 +1037,6 @@ class PreferencesTableData extends DataClass searchMode, downloadLocation, localLibraryLocation, - pipedInstance, - invidiousInstance, themeMode, audioSourceId, youtubeClientEngine, @@ -1108,8 +1066,6 @@ class PreferencesTableData extends DataClass other.searchMode == this.searchMode && other.downloadLocation == this.downloadLocation && other.localLibraryLocation == this.localLibraryLocation && - other.pipedInstance == this.pipedInstance && - other.invidiousInstance == this.invidiousInstance && other.themeMode == this.themeMode && other.audioSourceId == this.audioSourceId && other.youtubeClientEngine == this.youtubeClientEngine && @@ -1137,8 +1093,6 @@ class PreferencesTableCompanion extends UpdateCompanion { final Value searchMode; final Value downloadLocation; final Value localLibraryLocation; - final Value pipedInstance; - final Value invidiousInstance; final Value themeMode; final Value audioSourceId; final Value youtubeClientEngine; @@ -1164,8 +1118,6 @@ class PreferencesTableCompanion extends UpdateCompanion { this.searchMode = const Value.absent(), this.downloadLocation = const Value.absent(), this.localLibraryLocation = const Value.absent(), - this.pipedInstance = const Value.absent(), - this.invidiousInstance = const Value.absent(), this.themeMode = const Value.absent(), this.audioSourceId = const Value.absent(), this.youtubeClientEngine = const Value.absent(), @@ -1192,8 +1144,6 @@ class PreferencesTableCompanion extends UpdateCompanion { this.searchMode = const Value.absent(), this.downloadLocation = const Value.absent(), this.localLibraryLocation = const Value.absent(), - this.pipedInstance = const Value.absent(), - this.invidiousInstance = const Value.absent(), this.themeMode = const Value.absent(), this.audioSourceId = const Value.absent(), this.youtubeClientEngine = const Value.absent(), @@ -1220,8 +1170,6 @@ class PreferencesTableCompanion extends UpdateCompanion { Expression? searchMode, Expression? downloadLocation, Expression? localLibraryLocation, - Expression? pipedInstance, - Expression? invidiousInstance, Expression? themeMode, Expression? audioSourceId, Expression? youtubeClientEngine, @@ -1250,8 +1198,6 @@ class PreferencesTableCompanion extends UpdateCompanion { if (downloadLocation != null) 'download_location': downloadLocation, if (localLibraryLocation != null) 'local_library_location': localLibraryLocation, - if (pipedInstance != null) 'piped_instance': pipedInstance, - if (invidiousInstance != null) 'invidious_instance': invidiousInstance, if (themeMode != null) 'theme_mode': themeMode, if (audioSourceId != null) 'audio_source_id': audioSourceId, if (youtubeClientEngine != null) @@ -1281,8 +1227,6 @@ class PreferencesTableCompanion extends UpdateCompanion { Value? searchMode, Value? downloadLocation, Value? localLibraryLocation, - Value? pipedInstance, - Value? invidiousInstance, Value? themeMode, Value? audioSourceId, Value? youtubeClientEngine, @@ -1308,8 +1252,6 @@ class PreferencesTableCompanion extends UpdateCompanion { searchMode: searchMode ?? this.searchMode, downloadLocation: downloadLocation ?? this.downloadLocation, localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, - pipedInstance: pipedInstance ?? this.pipedInstance, - invidiousInstance: invidiousInstance ?? this.invidiousInstance, themeMode: themeMode ?? this.themeMode, audioSourceId: audioSourceId ?? this.audioSourceId, youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, @@ -1373,12 +1315,6 @@ class PreferencesTableCompanion extends UpdateCompanion { map['local_library_location'] = Variable(localLibraryLocation.value); } - if (pipedInstance.present) { - map['piped_instance'] = Variable(pipedInstance.value); - } - if (invidiousInstance.present) { - map['invidious_instance'] = Variable(invidiousInstance.value); - } if (themeMode.present) { map['theme_mode'] = Variable(themeMode.value); } @@ -1426,8 +1362,6 @@ class PreferencesTableCompanion extends UpdateCompanion { ..write('searchMode: $searchMode, ') ..write('downloadLocation: $downloadLocation, ') ..write('localLibraryLocation: $localLibraryLocation, ') - ..write('pipedInstance: $pipedInstance, ') - ..write('invidiousInstance: $invidiousInstance, ') ..write('themeMode: $themeMode, ') ..write('audioSourceId: $audioSourceId, ') ..write('youtubeClientEngine: $youtubeClientEngine, ') @@ -1935,7 +1869,9 @@ class SourceMatchTable extends Table type: DriftSqlType.string, requiredDuringInsert: true); late final GeneratedColumn sourceInfo = GeneratedColumn( 'source_info', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("{}")); late final GeneratedColumn sourceType = GeneratedColumn( 'source_type', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); @@ -2101,11 +2037,10 @@ class SourceMatchTableCompanion extends UpdateCompanion { SourceMatchTableCompanion.insert({ this.id = const Value.absent(), required String trackId, - required String sourceInfo, + this.sourceInfo = const Value.absent(), required String sourceType, this.createdAt = const Value.absent(), }) : trackId = Value(trackId), - sourceInfo = Value(sourceInfo), sourceType = Value(sourceType); static Insertable custom({ Expression? id, From 4b5108e54e4ff21dd561127e1d78d01578276174 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Nov 2025 21:27:06 +0600 Subject: [PATCH 06/14] fix: streaming not working --- lib/models/database/database.dart | 28 +++++++++++---- lib/pages/settings/sections/playback.dart | 35 +++++++++++++++++++ lib/provider/audio_player/audio_player.dart | 3 +- .../audio_source/quality_label.dart | 10 +++--- .../metadata_plugin_provider.dart | 2 +- .../metadata/endpoints/audio_source.dart | 4 +-- pubspec.lock | 17 --------- pubspec.yaml | 4 --- 8 files changed, 65 insertions(+), 38 deletions(-) diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 786b813f..55ff5abf 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -19,6 +19,7 @@ import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; @@ -211,13 +212,26 @@ class AppDatabase extends _$AppDatabase { ); }, from9To10: (m, schema) async { - await m.dropColumn(schema.preferencesTable, "piped_instance"); - await m.dropColumn(schema.preferencesTable, "invidious_instance"); - await m.addColumn( - schema.sourceMatchTable, - sourceMatchTable.sourceInfo, - ); - await m.dropColumn(schema.sourceMatchTable, "source_id"); + try { + await m + .dropColumn(schema.preferencesTable, "piped_instance") + .catchError((e) {}); + await m + .dropColumn(schema.preferencesTable, "invidious_instance") + .catchError((e) {}); + await m + .addColumn( + schema.sourceMatchTable, + sourceMatchTable.sourceInfo, + ) + .catchError((e) {}); + await m + .dropColumn(schema.sourceMatchTable, "source_id") + .catchError((e) {}); + } catch (e) { + AppLogger.log.e(e); + return; + } }, ), ); diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 29a8c2ea..7a498ac0 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show ListTile; @@ -7,11 +9,15 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/playback/edit_connect_port_dialog.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart'; import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/kv_store/kv_store.dart'; +import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/utils/platform.dart'; @@ -30,6 +36,35 @@ class SettingsPlaybackSection extends HookConsumerWidget { return SectionCardWithHeading( heading: context.l10n.playback, children: [ + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.engine), + title: Text(context.l10n.youtube_engine), + value: preferences.youtubeClientEngine, + options: YoutubeClientEngine.values + .where((e) => e.isAvailableForPlatform()) + .map((e) => SelectItemButton( + value: e, + child: Text(e.label), + )) + .toList(), + onChanged: (value) async { + if (value == null) return; + if (value == YoutubeClientEngine.ytDlp) { + final customPath = KVStoreService.getYoutubeEnginePath(value); + if (!await YtDlpEngine.isInstalled() && + (customPath == null || !await File(customPath).exists()) && + context.mounted) { + final hasInstalled = await showDialog( + context: context, + builder: (context) => + YouTubeEngineNotInstalledDialog(engine: value), + ); + if (hasInstalled != true) return; + } + } + preferencesNotifier.setYoutubeClientEngine(value); + }, + ), if (sourcePresets.presets.isNotEmpty) ...[ AdaptiveSelectTile( secondary: const Icon(SpotubeIcons.api), diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 2d569ab5..1bfd8f2d 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -7,7 +7,6 @@ import 'package:media_kit/media_kit.dart'; import 'package:spotube/extensions/list.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/database/database.dart'; @@ -400,7 +399,7 @@ class AudioPlayerNotifier extends Notifier { // because of timeout final intendedActiveTrack = medias.elementAt(initialIndex); if (intendedActiveTrack.track is! SpotubeLocalTrackObject) { - await ref.read( + ref.read( sourcedTrackProvider( intendedActiveTrack.track as SpotubeFullTrackObject, ).future, diff --git a/lib/provider/metadata_plugin/audio_source/quality_label.dart b/lib/provider/metadata_plugin/audio_source/quality_label.dart index 7d1dc95a..113ed54e 100644 --- a/lib/provider/metadata_plugin/audio_source/quality_label.dart +++ b/lib/provider/metadata_plugin/audio_source/quality_label.dart @@ -3,10 +3,10 @@ import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.da final audioSourceQualityLabelProvider = Provider((ref) { final sourceQuality = ref.watch(audioSourcePresetsProvider); - final sourceContainer = - sourceQuality.presets[sourceQuality.selectedStreamingContainerIndex]; - final quality = - sourceContainer.qualities[sourceQuality.selectedStreamingQualityIndex]; + final sourceContainer = sourceQuality.presets + .elementAtOrNull(sourceQuality.selectedStreamingContainerIndex); + final quality = sourceContainer?.qualities + .elementAtOrNull(sourceQuality.selectedStreamingQualityIndex); - return "${sourceContainer.name} • ${quality.toString()}"; + return "${sourceContainer?.name ?? "Unknown"} • ${quality?.toString() ?? "Unknown"}"; }); diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index 13d72c93..ab3c8547 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -543,7 +543,7 @@ final audioSourcePluginProvider = FutureProvider( metadataPluginsProvider .selectAsync((data) => data.defaultAudioSourcePluginConfig), ); - final youtubeEngine = ref.read(youtubeEngineProvider); + final youtubeEngine = ref.watch(youtubeEngineProvider); if (defaultPlugin == null) { return null; diff --git a/lib/services/metadata/endpoints/audio_source.dart b/lib/services/metadata/endpoints/audio_source.dart index 3493c112..d22449c6 100644 --- a/lib/services/metadata/endpoints/audio_source.dart +++ b/lib/services/metadata/endpoints/audio_source.dart @@ -22,7 +22,7 @@ class MetadataPluginAudioSourceEndpoint { SpotubeFullTrackObject track, ) async { final raw = await hetuMetadataAudioSource - .invoke("matches", positionalArgs: [track]) as List; + .invoke("matches", positionalArgs: [track.toJson()]) as List; return raw.map((e) => SpotubeAudioSourceMatchObject.fromJson(e)).toList(); } @@ -31,7 +31,7 @@ class MetadataPluginAudioSourceEndpoint { SpotubeAudioSourceMatchObject match, ) async { final raw = await hetuMetadataAudioSource - .invoke("streams", positionalArgs: [match]) as List; + .invoke("streams", positionalArgs: [match.toJson()]) as List; return raw.map((e) => SpotubeAudioSourceStreamObject.fromJson(e)).toList(); } diff --git a/pubspec.lock b/pubspec.lock index 0ae02b4c..6f004f11 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -458,15 +458,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - dab_music_api: - dependency: "direct main" - description: - path: "." - ref: main - resolved-ref: "55f96368b7465eec2e5e81774f9f2a7b18acc4ab" - url: "https://github.com/KRTirtho/dab_music_api.git" - source: git - version: "0.1.0" dart_mappable: dependency: transitive description: @@ -2023,14 +2014,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - retrofit: - dependency: transitive - description: - name: retrofit - sha256: "699cf44ec6c7fc7d248740932eca75d334e36bdafe0a8b3e9ff93100591c8a25" - url: "https://pub.dev" - source: hosted - version: "4.7.2" riverpod: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 812e690f..9780e0fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,10 +24,6 @@ dependencies: bonsoir: ^5.1.10 cached_network_image: ^3.3.1 connectivity_plus: ^6.1.2 - dab_music_api: - git: - url: https://github.com/KRTirtho/dab_music_api.git - ref: main desktop_webview_window: git: path: packages/desktop_webview_window From 6272f376ea27b8af62548d17b3229d8001df41b8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 4 Nov 2025 12:02:10 +0600 Subject: [PATCH 07/14] fix: quality preset initialization fails and audio source auth --- android/.gitignore | 1 + lib/models/metadata/audio_source.dart | 2 +- .../metadata_plugins/installed_plugin.dart | 7 +-- .../audio_source/quality_presets.dart | 50 +++++++++++-------- lib/provider/metadata_plugin/core/auth.dart | 4 +- lib/provider/server/routes/playback.dart | 6 ++- lib/services/sourced_track/sourced_track.dart | 2 +- 7 files changed, 42 insertions(+), 30 deletions(-) diff --git a/android/.gitignore b/android/.gitignore index 6f568019..2391a77e 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -11,3 +11,4 @@ GeneratedPluginRegistrant.java key.properties **/*.keystore **/*.jks +.kotlin \ No newline at end of file diff --git a/lib/models/metadata/audio_source.dart b/lib/models/metadata/audio_source.dart index 44804285..898300e9 100644 --- a/lib/models/metadata/audio_source.dart +++ b/lib/models/metadata/audio_source.dart @@ -44,7 +44,7 @@ class SpotubeAudioLossyContainerQuality @override toString() { - return "${oneOptionalDecimalFormatter.format(bitrate)}kbps"; + return "${oneOptionalDecimalFormatter.format(bitrate / 1000)}kbps"; } } diff --git a/lib/modules/metadata_plugins/installed_plugin.dart b/lib/modules/metadata_plugins/installed_plugin.dart index 34881aaf..7abda5ec 100644 --- a/lib/modules/metadata_plugins/installed_plugin.dart +++ b/lib/modules/metadata_plugins/installed_plugin.dart @@ -52,9 +52,10 @@ class MetadataInstalledPluginItem extends HookConsumerWidget { ref.watch(metadataPluginAuthenticatedProvider); final isAudioSourceAuthenticatedSnapshot = ref.watch(audioSourcePluginAuthenticatedProvider); - final isAuthenticated = - isMetadataAuthenticatedSnapshot.asData?.value == true || - isAudioSourceAuthenticatedSnapshot.asData?.value == true; + final isAuthenticated = (isDefaultMetadata && + isMetadataAuthenticatedSnapshot.asData?.value == true) || + (isDefaultAudioSource && + isAudioSourceAuthenticatedSnapshot.asData?.value == true); final metadataUpdateAvailable = ref.watch(metadataPluginUpdateCheckerProvider); diff --git a/lib/provider/metadata_plugin/audio_source/quality_presets.dart b/lib/provider/metadata_plugin/audio_source/quality_presets.dart index 9cc7dc44..05028bc1 100644 --- a/lib/provider/metadata_plugin/audio_source/quality_presets.dart +++ b/lib/provider/metadata_plugin/audio_source/quality_presets.dart @@ -6,6 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/metadata/metadata.dart'; part 'quality_presets.g.dart'; part 'quality_presets.freezed.dart'; @@ -28,9 +29,13 @@ class AudioSourceAvailableQualityPresetsNotifier extends Notifier { @override build() { - ref.watch(audioSourcePluginProvider); + final audioSourceSnapshot = ref.watch(audioSourcePluginProvider); + final audioSourceConfigSnapshot = ref.watch( + metadataPluginsProvider.select((data) => + data.whenData((value) => value.defaultAudioSourcePluginConfig)), + ); - _initialize(); + _initialize(audioSourceSnapshot, audioSourceConfigSnapshot); listenSelf((previous, next) { final isNewLossless = @@ -49,26 +54,29 @@ class AudioSourceAvailableQualityPresetsNotifier return AudioSourcePresetsState(); } - void _initialize() async { - final audioSource = await ref.read(audioSourcePluginProvider.future); - final audioSourceConfig = await ref.read( - metadataPluginsProvider - .selectAsync((data) => data.defaultAudioSourcePluginConfig), - ); - if (audioSource == null || audioSourceConfig == null) { - throw Exception("Dude wat?"); - } - final preferences = await SharedPreferences.getInstance(); - final persistedStateStr = - preferences.getString("audioSourceState-${audioSourceConfig.slug}"); + void _initialize( + AsyncValue audioSourceSnapshot, + AsyncValue audioSourceConfigSnapshot, + ) async { + audioSourceConfigSnapshot.whenData((audioSourceConfig) { + audioSourceSnapshot.whenData((audioSource) async { + if (audioSource == null || audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + final preferences = await SharedPreferences.getInstance(); + final persistedStateStr = + preferences.getString("audioSourceState-${audioSourceConfig.slug}"); - if (persistedStateStr != null) { - state = AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr)); - } else { - state = AudioSourcePresetsState( - presets: audioSource.audioSource.supportedPresets, - ); - } + if (persistedStateStr != null) { + state = + AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr)); + } else { + state = AudioSourcePresetsState( + presets: audioSource.audioSource.supportedPresets, + ); + } + }); + }); } void setSelectedStreamingContainerIndex(int index) { diff --git a/lib/provider/metadata_plugin/core/auth.dart b/lib/provider/metadata_plugin/core/auth.dart index 647b94f9..dc5e7eb6 100644 --- a/lib/provider/metadata_plugin/core/auth.dart +++ b/lib/provider/metadata_plugin/core/auth.dart @@ -65,6 +65,6 @@ class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier { } final audioSourcePluginAuthenticatedProvider = - AsyncNotifierProvider( - MetadataPluginAuthenticatedNotifier.new, + AsyncNotifierProvider( + AudioSourcePluginAuthenticatedNotifier.new, ); diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index ec3a98a1..792d7797 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -48,7 +48,7 @@ class ServerPlaybackRoutes { return join( await UserPreferencesNotifier.getMusicCacheDir(), ServiceUtils.sanitizeFilename( - '${track.query.name} - ${track.query.artists.join(",")} (${track.info.id}).${track.qualityPreset!.name}', + '${track.query.name} - ${track.query.artists.map((d) => d.name).join(",")} (${track.info.id}).${track.qualityPreset!.name}', ), ); } @@ -288,7 +288,9 @@ class ServerPlaybackRoutes { imageBytes: imageBytes, fileLength: fileLength, ), - ); + ).catchError((e, stackTrace) { + AppLogger.reportError(e, stackTrace); + }); } return (bytes: bytes, response: res); diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index 76b202da..a738ffba 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -317,7 +317,7 @@ class SourcedTrack extends BasicSourcedTrack { source.bitDepth == quality.bitDepth; } else { return source.bitrate == - (preset as SpotubeAudioLossyContainerQuality).bitrate; + (quality as SpotubeAudioLossyContainerQuality).bitrate; } }, ); From e1fa9efa14ef6a95d55a57089e9fccad1bb90d61 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 4 Nov 2025 13:45:23 +0600 Subject: [PATCH 08/14] fix: selection preset quality returning null --- .../audio_source/quality_presets.dart | 5 +- lib/services/sourced_track/sourced_track.dart | 55 ++++++------------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/lib/provider/metadata_plugin/audio_source/quality_presets.dart b/lib/provider/metadata_plugin/audio_source/quality_presets.dart index 05028bc1..0a8b00fe 100644 --- a/lib/provider/metadata_plugin/audio_source/quality_presets.dart +++ b/lib/provider/metadata_plugin/audio_source/quality_presets.dart @@ -69,7 +69,10 @@ class AudioSourceAvailableQualityPresetsNotifier if (persistedStateStr != null) { state = - AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr)); + AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr)) + .copyWith( + presets: audioSource.audioSource.supportedPresets, + ); } else { state = AudioSourcePresetsState( presets: audioSource.audioSource.supportedPresets, diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index a738ffba..5da54fc8 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -306,6 +306,8 @@ class SourcedTrack extends BasicSourcedTrack { SpotubeAudioSourceContainerPreset preset, int qualityIndex, ) { + if (sources.isEmpty) return null; + final quality = preset.qualities[qualityIndex]; final exactMatch = sources.firstWhereOrNull( @@ -326,45 +328,24 @@ class SourcedTrack extends BasicSourcedTrack { return exactMatch; } - // Find the closest to preset - SpotubeAudioSourceStreamObject? closest; - for (final source in sources) { - if (source.container != preset.name) continue; - - if (quality case SpotubeAudioLosslessContainerQuality()) { - final sourceBps = (source.bitDepth ?? 0) * (source.sampleRate ?? 0); - final qualityBps = quality.bitDepth * quality.sampleRate; - final closestBps = - (closest?.bitDepth ?? 0) * (closest?.sampleRate ?? 0); - - if (sourceBps == qualityBps) { - closest = source; - break; - } - final closestDiff = (closestBps - qualityBps).abs(); - final sourceDiff = (sourceBps - qualityBps).abs(); - - if (sourceDiff < closestDiff) { - closest = source; - } + // Find the preset with closest quality to the supplied quality + return sources.where((source) { + return source.container == preset.name; + }).reduce((prev, curr) { + if (quality is SpotubeAudioLosslessContainerQuality) { + final prevDiff = ((prev.sampleRate ?? 0) - quality.sampleRate).abs() + + ((prev.bitDepth ?? 0) - quality.bitDepth).abs(); + final currDiff = ((curr.sampleRate ?? 0) - quality.sampleRate).abs() + + ((curr.bitDepth ?? 0) - quality.bitDepth).abs(); + return currDiff < prevDiff ? curr : prev; } else { - final presetBitrate = - (preset as SpotubeAudioLossyContainerQuality).bitrate; - if (presetBitrate == source.bitrate) { - closest = source; - break; - } - - final closestDiff = (closest?.bitrate ?? 0) - presetBitrate; - final sourceDiff = (source.bitrate ?? 0) - presetBitrate; - - if (sourceDiff < closestDiff) { - closest = source; - } + final prevDiff = ((prev.bitrate ?? 0) - + (quality as SpotubeAudioLossyContainerQuality).bitrate) + .abs(); + final currDiff = ((curr.bitrate ?? 0) - quality.bitrate).abs(); + return currDiff < prevDiff ? curr : prev; } - } - - return closest; + }); } String? getUrlOfQuality( From d1b73dbb1c6050ef928136a036ef53edad083e56 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Nov 2025 18:48:18 +0600 Subject: [PATCH 09/14] feat: add NewPipe support for desktop platforms --- lib/main.dart | 2 ++ lib/services/youtube_engine/newpipe_engine.dart | 2 +- linux/flutter/generated_plugin_registrant.cc | 4 ++++ linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ pubspec.lock | 2 +- windows/flutter/generated_plugin_registrant.cc | 3 +++ windows/flutter/generated_plugins.cmake | 1 + 8 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f29933e6..ecf7148d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -83,6 +83,8 @@ Future main(List rawArgs) async { // force High Refresh Rate on some Android devices (like One Plus) if (kIsAndroid) { await FlutterDisplayMode.setHighRefreshRate(); + } + if (kIsAndroid || kIsDesktop) { await NewPipeExtractor.init(); } diff --git a/lib/services/youtube_engine/newpipe_engine.dart b/lib/services/youtube_engine/newpipe_engine.dart index ae451e22..d6445a19 100644 --- a/lib/services/youtube_engine/newpipe_engine.dart +++ b/lib/services/youtube_engine/newpipe_engine.dart @@ -6,7 +6,7 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:http_parser/http_parser.dart'; class NewPipeEngine implements YouTubeEngine { - static bool get isAvailableForPlatform => kIsAndroid; + static bool get isAvailableForPlatform => kIsAndroid || kIsDesktop; AudioOnlyStreamInfo _parseAudioStream(AudioStream stream, String videoId) { return AudioOnlyStreamInfo( diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index eda2d021..63e83265 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -30,6 +31,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_new_pipe_extractor_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterNewPipeExtractorPlugin"); + flutter_new_pipe_extractor_plugin_register_with_registrar(flutter_new_pipe_extractor_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b9ca593f..e5c8a845 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_webview_window file_selector_linux + flutter_new_pipe_extractor flutter_secure_storage_linux flutter_timezone gtk diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9385ed14..d211f518 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,6 +15,7 @@ import device_info_plus import file_picker import file_selector_macos import flutter_inappwebview_macos +import flutter_new_pipe_extractor import flutter_secure_storage_macos import flutter_timezone import irondash_engine_context @@ -44,6 +45,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + FlutterNewPipeExtractorPlugin.register(with: registry.registrar(forPlugin: "FlutterNewPipeExtractorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 6f004f11..4e98e422 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -955,7 +955,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: d4d71545111c8ca6c91f0040091c42d74cce1762 + resolved-ref: "898fd4ebcef77f5177b08aa6f9b9047bd02c6b9b" url: "https://github.com/KRTirtho/flutter_new_pipe_extractor.git" source: git version: "0.1.0" diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 87b34e37..ac2fd1e0 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); + FlutterNewPipeExtractorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterNewPipeExtractorPluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); FlutterTimezonePluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 798e47c8..6e831cf5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_webview_window file_selector_windows flutter_inappwebview_windows + flutter_new_pipe_extractor flutter_secure_storage_windows flutter_timezone irondash_engine_context From 64f937bd1421692693ecfdbb4c7873c75962f61c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Nov 2025 18:59:55 +0600 Subject: [PATCH 10/14] chore: remove useless appbundle build --- .github/workflows/spotube-publish-binary.yml | 46 +++++++++--------- .github/workflows/spotube-release-binary.yml | 1 - cli/commands/build/android.dart | 51 -------------------- pubspec.lock | 2 +- pubspec.yaml | 1 - 5 files changed, 24 insertions(+), 77 deletions(-) diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml index f88e618c..e682dbdd 100644 --- a/.github/workflows/spotube-publish-binary.yml +++ b/.github/workflows/spotube-publish-binary.yml @@ -12,10 +12,10 @@ on: type: boolean default: true jobs: - description: Jobs to run (flathub,aur,winget,chocolatey,playstore) + description: Jobs to run (flathub,aur,winget,chocolatey) required: true type: string - default: "flathub,aur,winget,chocolatey,playstore" + default: "flathub,aur,winget,chocolatey" jobs: flathub: @@ -112,26 +112,26 @@ jobs: - name: Tagname (workflow dispatch) run: echo 'TAG_NAME=${{inputs.version}}' >> $GITHUB_ENV - - uses: robinraju/release-downloader@main - with: - repository: KRTirtho/spotube - tag: v${{ env.TAG_NAME }} - tarBall: false - zipBall: false - out-file-path: dist - fileName: "Spotube-playstore-all-arch.aab" + # - uses: robinraju/release-downloader@main + # with: + # repository: KRTirtho/spotube + # tag: v${{ env.TAG_NAME }} + # tarBall: false + # zipBall: false + # out-file-path: dist + # fileName: "Spotube-playstore-all-arch.aab" - - name: Create service-account.json - run: | - echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json + # - name: Create service-account.json + # run: | + # echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json - - name: Upload Android Release to Play Store - if: ${{!inputs.dry_run}} - uses: r0adkll/upload-google-play@v1 - with: - serviceAccountJson: ./service-account.json - releaseFiles: ./dist/Spotube-playstore-all-arch.aab - packageName: oss.krtirtho.spotube - track: production - status: draft - releaseName: ${{ env.TAG_NAME }} + # - name: Upload Android Release to Play Store + # if: ${{!inputs.dry_run}} + # uses: r0adkll/upload-google-play@v1 + # with: + # serviceAccountJson: ./service-account.json + # releaseFiles: ./dist/Spotube-playstore-all-arch.aab + # packageName: oss.krtirtho.spotube + # track: production + # status: draft + # releaseName: ${{ env.TAG_NAME }} diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 4f2cff34..2ddd7a6a 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -49,7 +49,6 @@ jobs: arch: all files: | build/Spotube-android-all-arch.apk - build/Spotube-playstore-all-arch.aab - os: windows-latest platform: windows arch: x86 diff --git a/cli/commands/build/android.dart b/cli/commands/build/android.dart index 4216553a..b9edeb84 100644 --- a/cli/commands/build/android.dart +++ b/cli/commands/build/android.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:args/command_runner.dart'; -import 'package:collection/collection.dart'; import 'package:path/path.dart'; -import 'package:xml/xml.dart'; import '../../core/env.dart'; import 'common.dart'; @@ -24,39 +22,6 @@ class AndroidBuildCommand extends Command with BuildCommandCommonSteps { "flutter build apk --flavor ${CliEnv.channel.name}", ); - await dotEnvFile.writeAsString( - "\nENABLE_UPDATE_CHECK=0" - "\nHIDE_DONATIONS=1", - mode: FileMode.append, - ); - - final androidManifestFile = File( - join(cwd.path, "android", "app", "src", "main", "AndroidManifest.xml")); - - final androidManifestXml = - XmlDocument.parse(await androidManifestFile.readAsString()); - - final deletingElement = - androidManifestXml.findAllElements("meta-data").firstWhereOrNull( - (el) => - el.getAttribute("android:name") == - "com.google.android.gms.car.application", - ); - - deletingElement?.parent?.children.remove(deletingElement); - - await androidManifestFile.writeAsString( - androidManifestXml.toXmlString(pretty: true), - ); - - await shell.run( - """ - dart run build_runner clean - dart run build_runner build --delete-conflicting-outputs - flutter build appbundle --flavor ${CliEnv.channel.name} - """, - ); - final ogApkFile = File( join( "build", @@ -71,22 +36,6 @@ class AndroidBuildCommand extends Command with BuildCommandCommonSteps { join(cwd.path, "build", "Spotube-android-all-arch.apk"), ); - final ogAppbundleFile = File( - join( - cwd.path, - "build", - "app", - "outputs", - "bundle", - "${CliEnv.channel.name}Release", - "app-${CliEnv.channel.name}-release.aab", - ), - ); - - await ogAppbundleFile.copy( - join(cwd.path, "build", "Spotube-playstore-all-arch.aab"), - ); - stdout.writeln("✅ Built Android Apk and Appbundle"); } } diff --git a/pubspec.lock b/pubspec.lock index 4e98e422..91f1c2eb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2821,7 +2821,7 @@ packages: source: hosted version: "1.1.0" xml: - dependency: "direct dev" + dependency: transitive description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 diff --git a/pubspec.yaml b/pubspec.yaml index 9780e0fe..0c31a0cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -177,7 +177,6 @@ dev_dependencies: process_run: ^0.14.2 pubspec_parse: ^1.3.0 pub_api_client: ^3.0.0 - xml: ^6.5.0 io: ^1.0.4 drift_dev: ^2.21.0 auto_route_generator: ^9.0.0 From a012a8f3af9b144fa4048bb65f5cfb14e72fbc1a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Nov 2025 20:28:09 +0600 Subject: [PATCH 11/14] chore: fix unique index on source_match_table causing failure on insert --- lib/models/database/database.dart | 29 ++++++-------------- lib/models/database/tables/source_match.dart | 5 ---- pubspec.lock | 4 +-- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 55ff5abf..387bcdb7 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -19,7 +19,6 @@ import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; -import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; @@ -212,26 +211,14 @@ class AppDatabase extends _$AppDatabase { ); }, from9To10: (m, schema) async { - try { - await m - .dropColumn(schema.preferencesTable, "piped_instance") - .catchError((e) {}); - await m - .dropColumn(schema.preferencesTable, "invidious_instance") - .catchError((e) {}); - await m - .addColumn( - schema.sourceMatchTable, - sourceMatchTable.sourceInfo, - ) - .catchError((e) {}); - await m - .dropColumn(schema.sourceMatchTable, "source_id") - .catchError((e) {}); - } catch (e) { - AppLogger.log.e(e); - return; - } + await m.dropColumn(schema.preferencesTable, "piped_instance"); + await m.dropColumn(schema.preferencesTable, "invidious_instance"); + await m.addColumn( + schema.sourceMatchTable, + sourceMatchTable.sourceInfo, + ); + await customStatement("DROP INDEX IF EXISTS uniq_track_match;"); + await m.dropColumn(schema.sourceMatchTable, "source_id"); }, ), ); diff --git a/lib/models/database/tables/source_match.dart b/lib/models/database/tables/source_match.dart index 9ef79e9b..66a4959c 100644 --- a/lib/models/database/tables/source_match.dart +++ b/lib/models/database/tables/source_match.dart @@ -1,10 +1,5 @@ part of '../database.dart'; -@TableIndex( - name: "uniq_track_match", - columns: {#trackId, #sourceInfo, #sourceType}, - unique: true, -) class SourceMatchTable extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get trackId => text()(); diff --git a/pubspec.lock b/pubspec.lock index 91f1c2eb..d86cb541 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2002,10 +2002,10 @@ packages: dependency: transitive description: name: random_user_agents - sha256: "19facde509a2482dababb454faf2aceff797a6ae08e80f91268c0c8a7420f03b" + sha256: "95647149687167e82a7b39e1b4616fdebb574981b71b6f0cfca21b69f36293a8" url: "https://pub.dev" source: hosted - version: "1.0.15" + version: "1.0.17" recase: dependency: transitive description: From 7c632c8f06c834d6572f38c43c18b6f7d0e8c601 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Nov 2025 21:56:03 +0600 Subject: [PATCH 12/14] cd: remove unnecessary stuff for android build --- .github/workflows/spotube-release-binary.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 2ddd7a6a..449165e6 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -76,6 +76,14 @@ jobs: cache: true git-source: https://github.com/flutter/flutter.git + - name: free disk space + if: ${{ matrix.platform == 'android' }} + run: | + sudo swapoff -a + sudo rm -f /swapfile + sudo apt clean + docker rmi $(docker image ls -aq) + df -h - name: Setup Java if: ${{matrix.platform == 'android'}} uses: actions/setup-java@v4 From fda2257119a57116315ab2a84b23e81a559b13fb Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Nov 2025 22:51:48 +0600 Subject: [PATCH 13/14] feat: add default plugin loading capability --- .../plugin.smplug | Bin 0 -> 91456 bytes .../plugin.smplug | Bin 0 -> 19827 bytes lib/models/database/database.dart | 1 + lib/models/database/tables/preferences.dart | 4 +- .../metadata_plugin_provider.dart | 79 +++++++++++++++++- pubspec.yaml | 2 + 6 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug create mode 100644 assets/plugins/spotube-plugin-youtube-audio/plugin.smplug diff --git a/assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug b/assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug new file mode 100644 index 0000000000000000000000000000000000000000..41be05a4fd3916e42b4ed6f267be8409ee05707c GIT binary patch literal 91456 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

EPT!^j+33SaT!Cf$%!1)YT^@4|E01+izgxo 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 literal 0 HcmV?d00001 diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 387bcdb7..8df70968 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -22,6 +22,7 @@ import 'package:drift/native.dart'; import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; +import 'package:spotube/utils/platform.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index cc810ae7..3029e2a8 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -111,7 +111,9 @@ class PreferencesTable extends Table { localLibraryLocation: [], themeMode: ThemeMode.system, audioSourceId: null, - youtubeClientEngine: YoutubeClientEngine.youtubeExplode, + youtubeClientEngine: kIsIOS + ? YoutubeClientEngine.youtubeExplode + : YoutubeClientEngine.newPipe, discordPresence: true, endlessPlayback: true, enableConnect: false, diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index ab3c8547..cdc96c41 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; +import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; @@ -101,7 +102,11 @@ class MetadataPluginNotifier extends AsyncNotifier { final plugins = await database.pluginsTable.select().get(); - return await toStatePlugins(plugins); + final pluginState = await toStatePlugins(plugins); + + await _loadDefaultPlugins(pluginState); + + return pluginState; } Future toStatePlugins( @@ -171,6 +176,45 @@ class MetadataPluginNotifier extends AsyncNotifier { ); } + Future _loadDefaultPlugins(MetadataPluginState pluginState) async { + const plugins = [ + "spotube-plugin-musicbrainz-listenbrainz", + "spotube-plugin-youtube-audio", + ]; + + for (final plugin in plugins) { + final byteData = await rootBundle.load( + "assets/plugins/$plugin/plugin.smplug", + ); + final pluginConfig = + await extractPluginArchive(byteData.buffer.asUint8List()); + try { + await addPlugin(pluginConfig); + } on MetadataPluginException catch (e) { + if (e.errorCode == MetadataPluginErrorCode.duplicatePlugin && + await isPluginUpdate(pluginConfig)) { + final oldConfig = pluginState.plugins + .firstWhereOrNull((p) => p.slug == pluginConfig.slug); + if (oldConfig == null) continue; + final isDefaultMetadata = + oldConfig == pluginState.defaultMetadataPluginConfig; + final isDefaultAudioSource = + oldConfig == pluginState.defaultAudioSourcePluginConfig; + + await removePlugin(pluginConfig); + await addPlugin(pluginConfig); + + if (isDefaultMetadata) { + await setDefaultMetadataPlugin(pluginConfig); + } + if (isDefaultAudioSource) { + await setDefaultAudioSourcePlugin(pluginConfig); + } + } + } + } + } + Uri _getGithubReleasesUrl(String repoUrl) { final parsedUri = Uri.parse(repoUrl); final uri = parsedUri.replace( @@ -373,11 +417,19 @@ class MetadataPluginNotifier extends AsyncNotifier { repository: Value(plugin.repository), // Setting the very first plugin as the default plugin selectedForMetadata: Value( - (state.valueOrNull?.plugins.isEmpty ?? true) && + (state.valueOrNull?.plugins + .where( + (d) => d.abilities.contains(PluginAbilities.metadata)) + .isEmpty ?? + true) && plugin.abilities.contains(PluginAbilities.metadata), ), selectedForAudioSource: Value( - (state.valueOrNull?.plugins.isEmpty ?? true) && + (state.valueOrNull?.plugins + .where((d) => + d.abilities.contains(PluginAbilities.audioSource)) + .isEmpty ?? + true) && plugin.abilities.contains(PluginAbilities.audioSource), ), ), @@ -420,6 +472,27 @@ class MetadataPluginNotifier extends AsyncNotifier { } } + Future isPluginUpdate(PluginConfiguration newPlugin) async { + final pluginRes = await (database.pluginsTable.select() + ..where( + (tbl) => + tbl.name.equals(newPlugin.name) & + tbl.author.equals(newPlugin.author), + ) + ..limit(1)) + .get(); + + if (pluginRes.isEmpty) { + return false; + } + + final oldPlugin = pluginRes.first; + final oldPluginApiVersion = Version.parse(oldPlugin.pluginApiVersion); + final newPluginApiVersion = Version.parse(newPlugin.pluginApiVersion); + + return newPluginApiVersion > oldPluginApiVersion; + } + Future updatePlugin( PluginConfiguration plugin, PluginUpdateAvailable update, diff --git a/pubspec.yaml b/pubspec.yaml index 0c31a0cb..fd3d78c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -227,6 +227,8 @@ flutter: - assets/branding/spotube-logo.png - assets/branding/spotube-logo-light.png - assets/branding/spotube-logo.ico + - assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug + - assets/plugins/spotube-plugin-youtube-audio/plugin.smplug - LICENSE - packages/flutter_undraw/assets/undraw/access_denied.svg - packages/flutter_undraw/assets/undraw/fixing_bugs.svg From d2dd60aa5c6391f70c369887de90254cd1ed0b6a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 8 Nov 2025 13:48:50 +0600 Subject: [PATCH 14/14] chore: update YoutubeExplode to v3 --- .../youtube_engine/quickjs_solver.dart | 167 ++++++++++++++++++ .../youtube_explode_engine.dart | 5 +- linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 16 +- pubspec.yaml | 3 +- windows/flutter/generated_plugins.cmake | 1 + 6 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 lib/services/youtube_engine/quickjs_solver.dart diff --git a/lib/services/youtube_engine/quickjs_solver.dart b/lib/services/youtube_engine/quickjs_solver.dart new file mode 100644 index 00000000..4e8bfafb --- /dev/null +++ b/lib/services/youtube_engine/quickjs_solver.dart @@ -0,0 +1,167 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:youtube_explode_dart/js_challenge.dart'; +// ignore: implementation_imports +import 'package:youtube_explode_dart/src/reverse_engineering/challenges/ejs/ejs.dart'; +import 'package:jsf/jsf.dart'; + +/// [WIP] +class QuickJSEJSSolver extends BaseJSChallengeSolver { + final _playerCache = {}; + final _sigCache = <(String, String, JSChallengeType), String>{}; + final QuickJSRuntime qjs; + QuickJSEJSSolver._(this.qjs); + + static Future init() async { + final modules = await EJSBuilder.getJSModules(); + final deno = await QuickJSRuntime.init(modules); + return QuickJSEJSSolver._(deno); + } + + @override + Future solve( + String playerUrl, JSChallengeType type, String challenge) async { + final key = (playerUrl, challenge, type); + if (_sigCache.containsKey(key)) { + return _sigCache[key]!; + } + + var playerScript = _playerCache[playerUrl]; + if (playerScript == null) { + final resp = await http.get(Uri.parse(playerUrl)); + playerScript = _playerCache[playerUrl] = resp.body; + } + final jsCall = EJSBuilder.buildJSCall(playerScript, { + type: [challenge], + }); + + final result = await qjs.eval(jsCall); + // Trim the first and last characters (' delimiters of the JS string) + final data = json.decode(result.substring(1, result.length - 1)) + as Map; + + if (data['type'] != 'result') { + throw Exception('Unexpected response type: ${data['type']}'); + } + final response = data['responses'][0]; + if (response['type'] != 'result') { + throw Exception('Unexpected item response type: ${response['type']}'); + } + final decoded = response['data'][challenge]; + if (decoded == null) { + throw Exception('No data for challenge: $challenge'); + } + + _sigCache[key] = decoded; + + return decoded; + } + + @override + void dispose() { + qjs.dispose(); + } +} + +class _EvalRequest { + final String code; + final Completer completer; + + _EvalRequest(this.code, this.completer); +} + +class QuickJSRuntime { + final JsRuntime _runtime; + final StreamController _stdoutController = + StreamController.broadcast(); + + // Queue for incoming eval requests + final Queue<_EvalRequest> _evalQueue = Queue<_EvalRequest>(); + bool _isProcessing = false; // Flag to indicate if an eval is currently active + + QuickJSRuntime(this._runtime); + + /// Disposes the Deno process. + void dispose() { + _stdoutController.close(); + _runtime.dispose(); + } + + /// Sends JavaScript code to Deno for evaluation. + /// Assumes single-line input produces single-line output. + Future eval(String code) { + final completer = Completer(); + final request = _EvalRequest(code, completer); + _evalQueue.addLast(request); // Add request to the end of the queue + _processQueue(); // Attempt to process the queue + + return completer.future; + } + + // Processes the eval queue. + void _processQueue() { + if (_isProcessing || _evalQueue.isEmpty) { + return; // Already processing or nothing in queue + } + + _isProcessing = true; + final request = + _evalQueue.first; // Get the next request without removing it yet + + StreamSubscription? currentOutputSubscription; + Completer lineReceived = Completer(); + + currentOutputSubscription = _stdoutController.stream.listen((data) { + if (!lineReceived.isCompleted) { + // Assuming single line output per eval. + // This will capture the first full line or chunk received after sending the code. + request.completer.complete(data.trim()); + lineReceived.complete(); + currentOutputSubscription + ?.cancel(); // Cancel subscription for this request + _evalQueue.removeFirst(); // Remove the processed request + _isProcessing = false; // Mark as no longer processing + _processQueue(); // Attempt to process next item in queue + } + }, onError: (e) { + if (!request.completer.isCompleted) { + request.completer.completeError(e); + lineReceived.completeError(e); + currentOutputSubscription?.cancel(); + _evalQueue.removeFirst(); + _isProcessing = false; + _processQueue(); + } + }, onDone: () { + if (!request.completer.isCompleted) { + request.completer.completeError( + StateError('Deno process closed while awaiting eval result.')); + lineReceived.completeError( + StateError('Deno process closed while awaiting eval result.')); + currentOutputSubscription?.cancel(); + _evalQueue.removeFirst(); + _isProcessing = false; + _processQueue(); + } + }); + + debugPrint("[QuickJS Solver] Evaluate ${request.code}"); + final result = _runtime.eval(request.code); + debugPrint("[QuickJS Solver] Evaluation Result $result"); + _stdoutController.add(result); + } + + static Future init(String initCode) async { + debugPrint("[QuickJS Solver] Initializing"); + debugPrint("[QuickJS Solver] script $initCode"); + + final runtime = JsRuntime(); + + runtime.execInitScript(initCode); + + return QuickJSRuntime(runtime); + } +} diff --git a/lib/services/youtube_engine/youtube_explode_engine.dart b/lib/services/youtube_engine/youtube_explode_engine.dart index c552f883..f8587ca6 100644 --- a/lib/services/youtube_engine/youtube_explode_engine.dart +++ b/lib/services/youtube_engine/youtube_explode_engine.dart @@ -2,6 +2,7 @@ import 'dart:isolate'; import 'package:flutter/foundation.dart'; import 'package:spotube/services/youtube_engine/youtube_engine.dart'; +// import 'package:youtube_explode_dart/solvers.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'dart:async'; @@ -55,8 +56,9 @@ class IsolatedYoutubeExplode { } } - static void _isolateEntry(SendPort mainSendPort) { + static Future _isolateEntry(SendPort mainSendPort) async { final receivePort = ReceivePort(); + // final solver = await DenoEJSSolver.init(); final youtubeExplode = YoutubeExplode(); final stopWatch = kDebugMode ? Stopwatch() : null; @@ -163,6 +165,7 @@ class YouTubeExplodeEngine implements YouTubeEngine { ytClients: [ YoutubeApiClient.ios, YoutubeApiClient.androidVr, + YoutubeApiClient.android, ], ); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index e5c8a845..541826e6 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -24,6 +24,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_discord_rpc + jsf metadata_god ) diff --git a/pubspec.lock b/pubspec.lock index d86cb541..7e53e91c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -446,10 +446,10 @@ packages: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" csslib: dependency: transitive description: @@ -1439,6 +1439,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + jsf: + dependency: "direct main" + description: + name: jsf + sha256: "189ba3b9216702f9b6f2d8ea90fa5acaca13bbe5dd2f72fb38618005b41a737f" + url: "https://pub.dev" + source: hosted + version: "0.6.1" json_annotation: dependency: "direct main" description: @@ -2840,10 +2848,10 @@ packages: dependency: "direct main" description: name: youtube_explode_dart - sha256: "947ba05e0c4f050743e480e7bca3575ff6427d86cc898c1a69f5e1d188cdc9e0" + sha256: add33de45d80c7f71a5e3dd464dd82fafd7fb5ab875fd303c023f30f76618325 url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "3.0.0" yt_dlp_dart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index fd3d78c5..9b84196b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -131,7 +131,7 @@ dependencies: wikipedia_api: ^0.1.0 win32_registry: ^1.1.5 window_manager: ^0.4.3 - youtube_explode_dart: ^2.5.3 + youtube_explode_dart: ^3.0.0 yt_dlp_dart: git: url: https://github.com/KRTirtho/yt_dlp_dart.git @@ -161,6 +161,7 @@ dependencies: flutter_markdown_plus: ^1.0.3 pub_semver: ^2.2.0 change_case: ^1.1.0 + jsf: ^0.6.1 dev_dependencies: build_runner: ^2.4.13 diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 6e831cf5..53cd3667 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -27,6 +27,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_discord_rpc + jsf metadata_god smtc_windows )