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
+1 -1
View File
@@ -742,7 +742,7 @@ class _SwingIconState extends State<SwingIcon>
TweenSequenceItem(tween: Tween(begin: 0.15, end: -0.1), weight: 20),
TweenSequenceItem(tween: Tween(begin: -0.1, end: 0.05), weight: 20),
TweenSequenceItem(tween: Tween(begin: 0.05, end: 0.0), weight: 20),
]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
]).animate(_controller);
_controller.forward();
}
@@ -465,34 +465,6 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
),
],
SettingsItem(
title: context.l10n.youtubeOpusBitrateTitle,
subtitle:
'${settings.youtubeOpusBitrate}kbps (128/256/320)',
onTap: () => _showYoutubeBitratePicker(
context: context,
title: context.l10n.youtubeOpusBitrateTitle,
currentValue: settings.youtubeOpusBitrate,
options: const [128, 256, 320],
onSave: (value) => ref
.read(settingsProvider.notifier)
.setYoutubeOpusBitrate(value),
),
),
SettingsItem(
title: context.l10n.youtubeMp3BitrateTitle,
subtitle: '${settings.youtubeMp3Bitrate}kbps (128/256/320)',
onTap: () => _showYoutubeBitratePicker(
context: context,
title: context.l10n.youtubeMp3BitrateTitle,
currentValue: settings.youtubeMp3Bitrate,
options: const [128, 256, 320],
onSave: (value) => ref
.read(settingsProvider.notifier)
.setYoutubeMp3Bitrate(value),
),
showDivider: false,
),
],
),
),
@@ -1689,68 +1661,6 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
);
}
void _showYoutubeBitratePicker({
required BuildContext context,
required String title,
required int currentValue,
required List<int> options,
required void Function(int value) onSave,
}) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
),
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.fromLTRB(24, 12, 24, 8),
child: Row(
children: [
Expanded(
child: Text(
title,
style: Theme.of(sheetContext).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
),
for (final bitrate in options)
ListTile(
title: Text('$bitrate kbps'),
trailing: bitrate == currentValue
? Icon(Icons.check, color: colorScheme.primary)
: null,
onTap: () {
onSave(bitrate);
Navigator.pop(sheetContext);
},
),
const SizedBox(height: 8),
],
),
),
);
}
void _showMusixmatchLanguagePicker(
BuildContext context,
WidgetRef ref,
@@ -2100,7 +2010,7 @@ class _ServiceSelector extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final extState = ref.watch(extensionProvider);
final builtInServiceIds = ['tidal', 'qobuz', 'deezer', 'youtube'];
final builtInServiceIds = ['tidal', 'qobuz', 'deezer'];
final extensionProviders = extState.extensions
.where((e) => e.enabled && e.hasDownloadProvider)
@@ -2136,15 +2046,6 @@ class _ServiceSelector extends ConsumerWidget {
onTap: () => onChanged('qobuz'),
),
),
const SizedBox(width: 8),
Expanded(
child: _ServiceChip(
icon: Icons.smart_display,
label: 'YouTube',
isSelected: effectiveService == 'youtube',
onTap: () => onChanged('youtube'),
),
),
],
),
if (extensionProviders.isNotEmpty) ...[
@@ -340,12 +340,6 @@ class _ProviderItem extends StatelessWidget {
icon: Icons.graphic_eq,
isBuiltIn: true,
);
case 'youtube':
return _ProviderInfo(
name: 'YouTube',
icon: Icons.play_circle_outline,
isBuiltIn: true,
);
default:
return _ProviderInfo(
name: provider,
+36 -5
View File
@@ -343,16 +343,23 @@ class _StoreTabState extends ConsumerState<StoreTab> {
labelText: context.l10n.storeRepoUrlLabel,
prefixIcon: const Icon(Icons.link),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(28),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.outline),
borderRadius: BorderRadius.circular(28),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(28),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
keyboardType: TextInputType.url,
autocorrect: false,
@@ -441,7 +448,31 @@ class _StoreTabState extends ConsumerState<StoreTab> {
labelText: context.l10n.storeNewRepoUrlLabel,
prefixIcon: const Icon(Icons.link),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(28),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(28),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(28),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
filled: true,
fillColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
keyboardType: TextInputType.url,