refactor: extract YouTube download to ytmusic extension and fix UI issues

Remove built-in YouTube/Cobalt download pipeline from Go backend and
Dart frontend. YouTube downloading now requires the ytmusic-spotiflac
extension (with download_provider capability).

Go backend:
- Delete youtube.go (745 lines) and youtube_quality_test.go
- Remove DownloadFromYouTube, IsYouTubeURLExport,
  ExtractYouTubeVideoIDExport from exports.go
- Remove YouTube routing from DownloadTrack and DownloadByStrategy

Dart frontend:
- Remove YouTube from built-in services, bitrate settings, quality UI
- Remove youtubeOpusBitrate/youtubeMp3Bitrate from settings model
- Add migration 7: default service youtube -> tidal
- Remove YouTube l10n keys from all 14 arb files and regenerate
- Update _determineOutputExt to handle opus_/mp3_ quality strings
- Add SAF opus/mp3 metadata embedding in unified branch
- Fix TweenSequence assertion crash (t outside 0.0-1.0)
- Fix store URL TextField styling consistency

Extension changes (gitignored, in extension/YT-Music-SpotiFLAC/):
- Add download_provider type, qualityOptions, network permissions
- Implement checkAvailability and download via SpotubeDL/Cobalt
This commit is contained in:
zarzet
2026-03-26 16:17:57 +07:00
parent d4b37edc2f
commit 5e1cc3ecb5
41 changed files with 100 additions and 1650 deletions
+2 -2
View File
@@ -748,7 +748,7 @@ class _DownloadSuccessOverlayState extends State<DownloadSuccessOverlay>
_flashAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 0.0, end: 0.15), weight: 30),
TweenSequenceItem(tween: Tween(begin: 0.15, end: 0.0), weight: 70),
]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
]).animate(_controller);
}
@override
@@ -816,7 +816,7 @@ class _AnimatedBadgeState extends State<AnimatedBadge>
_scaleAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.3), weight: 40),
TweenSequenceItem(tween: Tween(begin: 1.3, end: 1.0), weight: 60),
]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack));
]).animate(_controller);
}
@override
+1 -76
View File
@@ -77,24 +77,6 @@ const _builtInServices = [
),
],
),
BuiltInService(
id: 'youtube',
label: 'YouTube',
qualityOptions: [
QualityOption(
id: 'opus_320',
label: 'Opus 320kbps',
description: 'Best quality lossy (~10MB per track)',
),
QualityOption(
id: 'mp3_320',
label: 'MP3 320kbps',
description: 'Best compatibility (~10MB per track)',
),
],
isDisabled: false,
disabledReason: null,
),
];
class DownloadServicePicker extends ConsumerStatefulWidget {
@@ -148,9 +130,6 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
}
class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
static const List<int> _youtubeOpusSupportedBitrates = [128, 256, 320];
static const List<int> _youtubeMp3SupportedBitrates = [128, 256, 320];
late String _selectedService;
@override
@@ -167,30 +146,6 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
/// Get quality options for the selected service
List<QualityOption> _getQualityOptions() {
final settings = ref.read(settingsProvider);
if (_selectedService == 'youtube') {
final opusBitrate = _nearestSupportedBitrate(
settings.youtubeOpusBitrate,
_youtubeOpusSupportedBitrates,
);
final mp3Bitrate = _nearestSupportedBitrate(
settings.youtubeMp3Bitrate,
_youtubeMp3SupportedBitrates,
);
return [
QualityOption(
id: 'opus_$opusBitrate',
label: 'Opus ${opusBitrate}kbps',
description: 'Configured from YouTube settings',
),
QualityOption(
id: 'mp3_$mp3Bitrate',
label: 'MP3 ${mp3Bitrate}kbps',
description: 'Configured from YouTube settings',
),
];
}
final builtIn = _builtInServices
.where((s) => s.id == _selectedService)
.firstOrNull;
@@ -215,22 +170,6 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
];
}
int _nearestSupportedBitrate(int value, List<int> supported) {
var nearest = supported.first;
var nearestDistance = (value - nearest).abs();
for (final option in supported.skip(1)) {
final distance = (value - option).abs();
if (distance < nearestDistance ||
(distance == nearestDistance && option > nearest)) {
nearest = option;
nearestDistance = distance;
}
}
return nearest;
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
@@ -324,9 +263,7 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
),
),
if (_builtInServices.any(
(s) => s.id == _selectedService && s.id != 'youtube',
))
if (_builtInServices.any((s) => s.id == _selectedService))
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 12),
child: Text(
@@ -338,18 +275,6 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
),
),
if (_selectedService == 'youtube')
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 12),
child: Text(
context.l10n.youtubeQualityNote,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
),
for (final quality in qualityOptions)
_QualityOption(
title: quality.label,