mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-24 03:42:32 +02:00
feat: YouTube customizable bitrate, improved title matching, SpotubeDL engine fallback
- Add configurable YouTube Opus (96-256kbps) and MP3 (96-320kbps) bitrates - Improve title matching with loose normalization for symbol-heavy tracks - Add SpotubeDL engine v2 fallback for MP3 requests - Improve filename sanitization in track metadata screen - Bump version to 3.6.9+82
This commit is contained in:
@@ -261,6 +261,33 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
),
|
||||
),
|
||||
],
|
||||
SettingsItem(
|
||||
title: context.l10n.youtubeOpusBitrateTitle,
|
||||
subtitle: '${settings.youtubeOpusBitrate}kbps (128/256)',
|
||||
onTap: () => _showYoutubeBitratePicker(
|
||||
context: context,
|
||||
title: context.l10n.youtubeOpusBitrateTitle,
|
||||
currentValue: settings.youtubeOpusBitrate,
|
||||
options: const [128, 256],
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -271,20 +298,35 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.subtitles_outlined,
|
||||
title: context.l10n.optionsEmbedLyrics,
|
||||
subtitle: context.l10n.optionsEmbedLyricsSubtitle,
|
||||
value: settings.embedLyrics,
|
||||
onChanged: (value) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setEmbedLyrics(value),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.lyrics_outlined,
|
||||
title: context.l10n.lyricsMode,
|
||||
subtitle: _getLyricsModeLabel(context, settings.lyricsMode),
|
||||
onTap: () => _showLyricsModePicker(
|
||||
context,
|
||||
ref,
|
||||
settings.lyricsMode,
|
||||
),
|
||||
subtitle: settings.embedLyrics
|
||||
? _getLyricsModeLabel(context, settings.lyricsMode)
|
||||
: context.l10n.extensionsDisabled,
|
||||
onTap: settings.embedLyrics
|
||||
? () => _showLyricsModePicker(
|
||||
context,
|
||||
ref,
|
||||
settings.lyricsMode,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.source_outlined,
|
||||
title: 'Lyrics Providers',
|
||||
subtitle: _getLyricsProvidersSubtitle(settings.lyricsProviders),
|
||||
subtitle: _getLyricsProvidersSubtitle(
|
||||
settings.lyricsProviders,
|
||||
),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -1250,9 +1292,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
|
||||
String _getLyricsProvidersSubtitle(List<String> providers) {
|
||||
if (providers.isEmpty) return 'None enabled';
|
||||
return providers
|
||||
.map((p) => _providerDisplayNames[p] ?? p)
|
||||
.join(' > ');
|
||||
return providers.map((p) => _providerDisplayNames[p] ?? p).join(' > ');
|
||||
}
|
||||
|
||||
String _normalizeMusixmatchLanguage(String value) {
|
||||
@@ -1260,6 +1300,67 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
return normalized.replaceAll(RegExp(r'[^a-z0-9\-_]'), '');
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
@@ -1288,9 +1389,9 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
children: [
|
||||
Text(
|
||||
'Musixmatch Language',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
@@ -1319,7 +1420,9 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
const SizedBox(width: 8),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref.read(settingsProvider.notifier).setMusixmatchLanguage('');
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setMusixmatchLanguage('');
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Auto'),
|
||||
|
||||
@@ -76,6 +76,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
);
|
||||
static final RegExp _lrcSpeakerPrefixPattern = RegExp(r'^(v1|v2):\s*');
|
||||
static final RegExp _lrcBackgroundLinePattern = RegExp(r'^\[bg:(.*)\]$');
|
||||
static final RegExp _invalidFileNameChars = RegExp(r'[<>:"/\\|?*\x00-\x1f]');
|
||||
static final RegExp _multiUnderscore = RegExp(r'_+');
|
||||
static final RegExp _leadingOrTrailingDots = RegExp(r'^\.+|\.+$');
|
||||
static const List<String> _months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
@@ -1722,9 +1725,19 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
String _sanitizeFileNameSegment(String value) {
|
||||
var sanitized = value.replaceAll(_invalidFileNameChars, '_').trim();
|
||||
sanitized = sanitized.replaceAll(_leadingOrTrailingDots, '');
|
||||
sanitized = sanitized.replaceAll(_multiUnderscore, '_');
|
||||
if (sanitized.isEmpty) {
|
||||
return 'untitled';
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
String _buildSaveBaseName() {
|
||||
final artist = artistName.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_');
|
||||
final track = trackName.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_');
|
||||
final artist = _sanitizeFileNameSegment(artistName);
|
||||
final track = _sanitizeFileNameSegment(trackName);
|
||||
return '$artist - $track';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user