mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-03 16:50:40 +02:00
feat(library): add lyrics filtering and metadata actions
This commit is contained in:
@@ -79,6 +79,9 @@ class BackupService {
|
||||
static const int formatVersion = 1;
|
||||
static const String fileExtension = 'json';
|
||||
|
||||
static String encode(Map<String, dynamic> envelope) =>
|
||||
const JsonEncoder.withIndent(' ').convert(envelope);
|
||||
|
||||
/// Builds the backup envelope written to disk.
|
||||
static Map<String, dynamic> buildEnvelope({
|
||||
required Map<String, dynamic>? settings,
|
||||
@@ -119,7 +122,7 @@ class BackupService {
|
||||
final fileName = 'spotiflac_backup_$stamp.$fileExtension';
|
||||
final file = File(p.join(backupsDir.path, fileName));
|
||||
|
||||
await file.writeAsString(jsonEncode(envelope), flush: true);
|
||||
await file.writeAsString(encode(envelope), flush: true);
|
||||
_log.i('Backup written to ${file.path}');
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/utils/string_utils.dart';
|
||||
|
||||
class SavedCoverResult {
|
||||
final String fileName;
|
||||
@@ -20,9 +21,9 @@ class CoverDownloadService {
|
||||
required String baseName,
|
||||
required AppSettings settings,
|
||||
}) async {
|
||||
final normalizedUrl = coverUrl.trim();
|
||||
if (normalizedUrl.isEmpty) {
|
||||
throw const FormatException('No cover art source available');
|
||||
final normalizedUrl = normalizeRemoteHttpUrl(coverUrl);
|
||||
if (normalizedUrl == null) {
|
||||
throw const FormatException('No remote cover art source available');
|
||||
}
|
||||
|
||||
final safeBaseName = await PlatformBridge.sanitizeFilename(baseName.trim());
|
||||
|
||||
@@ -65,7 +65,7 @@ class HistoryBatchLookupRequest {
|
||||
}
|
||||
|
||||
class HistoryDatabase {
|
||||
static const int schemaVersion = 12;
|
||||
static const int schemaVersion = 13;
|
||||
static final HistoryDatabase instance = HistoryDatabase._init();
|
||||
static final sqlite.SingleFlightInitializer<Database> _database =
|
||||
sqlite.SingleFlightInitializer<Database>();
|
||||
@@ -120,6 +120,8 @@ class HistoryDatabase {
|
||||
label TEXT,
|
||||
copyright TEXT,
|
||||
explicit INTEGER NOT NULL DEFAULT 0,
|
||||
has_lyrics INTEGER NOT NULL DEFAULT 0,
|
||||
lyrics_metadata_scan_version INTEGER NOT NULL DEFAULT 0,
|
||||
spotify_id_norm TEXT,
|
||||
isrc_norm TEXT,
|
||||
match_key TEXT,
|
||||
@@ -239,6 +241,21 @@ class HistoryDatabase {
|
||||
);
|
||||
_log.i('Added explicit-content metadata');
|
||||
}
|
||||
if (oldVersion < 13) {
|
||||
await sqlite.addColumnIfMissing(
|
||||
db,
|
||||
'history',
|
||||
'has_lyrics',
|
||||
'INTEGER NOT NULL DEFAULT 0',
|
||||
);
|
||||
await sqlite.addColumnIfMissing(
|
||||
db,
|
||||
'history',
|
||||
'lyrics_metadata_scan_version',
|
||||
'INTEGER NOT NULL DEFAULT 0',
|
||||
);
|
||||
_log.i('Added indexed lyrics availability metadata');
|
||||
}
|
||||
}
|
||||
|
||||
static String normalizeLookupText(String? value) =>
|
||||
@@ -628,6 +645,10 @@ class HistoryDatabase {
|
||||
'label': json['label'],
|
||||
'copyright': json['copyright'],
|
||||
'explicit': json['explicit'] == true ? 1 : 0,
|
||||
'has_lyrics': json['hasLyrics'] == true ? 1 : 0,
|
||||
'lyrics_metadata_scan_version':
|
||||
(json['lyricsMetadataScanVersion'] as num?)?.toInt() ??
|
||||
(json.containsKey('hasLyrics') ? 1 : 0),
|
||||
};
|
||||
row.addAll(
|
||||
_queueSortColumns(
|
||||
@@ -687,6 +708,8 @@ class HistoryDatabase {
|
||||
'label': row['label'],
|
||||
'copyright': row['copyright'],
|
||||
'explicit': row['explicit'] == 1 || row['explicit'] == true,
|
||||
'hasLyrics': row['has_lyrics'] == 1 || row['has_lyrics'] == true,
|
||||
'lyricsMetadataScanVersion': row['lyrics_metadata_scan_version'] ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1089,6 +1112,8 @@ class HistoryDatabase {
|
||||
String? newQuality,
|
||||
int? newBitDepth,
|
||||
int? newSampleRate,
|
||||
bool? hasLyrics,
|
||||
int? lyricsMetadataScanVersion,
|
||||
}) async {
|
||||
final db = await database;
|
||||
final values = <String, dynamic>{};
|
||||
@@ -1101,6 +1126,12 @@ class HistoryDatabase {
|
||||
if (newSampleRate != null) {
|
||||
values['sample_rate'] = newSampleRate;
|
||||
}
|
||||
if (hasLyrics != null) {
|
||||
values['has_lyrics'] = hasLyrics ? 1 : 0;
|
||||
}
|
||||
if (lyricsMetadataScanVersion != null) {
|
||||
values['lyrics_metadata_scan_version'] = lyricsMetadataScanVersion;
|
||||
}
|
||||
if (values.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ final _log = AppLogger('LibraryDatabase');
|
||||
|
||||
class LibraryDatabase {
|
||||
static final LibraryDatabase instance = LibraryDatabase._init();
|
||||
static const int schemaVersion = 12;
|
||||
static const int schemaVersion = 13;
|
||||
static const String legacySourceId = LocalLibraryItem.legacySourceId;
|
||||
static const String visibleLibraryView = 'library_visible';
|
||||
static const int audioMetadataScanVersion = 2;
|
||||
static const int audioMetadataScanVersion = 3;
|
||||
static final sqlite.SingleFlightInitializer<Database> _database =
|
||||
sqlite.SingleFlightInitializer<Database>();
|
||||
bool _historyAttached = false;
|
||||
@@ -84,8 +84,9 @@ class LibraryDatabase {
|
||||
label TEXT,
|
||||
copyright TEXT,
|
||||
explicit INTEGER NOT NULL DEFAULT 0,
|
||||
has_lyrics INTEGER NOT NULL DEFAULT 0,
|
||||
format TEXT,
|
||||
audio_metadata_scan_version INTEGER NOT NULL DEFAULT 2,
|
||||
audio_metadata_scan_version INTEGER NOT NULL DEFAULT 3,
|
||||
track_name_norm TEXT,
|
||||
artist_name_norm TEXT,
|
||||
album_name_norm TEXT,
|
||||
@@ -211,6 +212,15 @@ class LibraryDatabase {
|
||||
);
|
||||
_log.i('Added explicit-content metadata');
|
||||
}
|
||||
if (oldVersion < 13) {
|
||||
await sqlite.addColumnIfMissing(
|
||||
db,
|
||||
'library',
|
||||
'has_lyrics',
|
||||
'INTEGER NOT NULL DEFAULT 0',
|
||||
);
|
||||
_log.i('Added indexed lyrics availability metadata');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createLibrarySources(DatabaseExecutor db) async {
|
||||
@@ -470,6 +480,7 @@ class LibraryDatabase {
|
||||
'label': json['label'],
|
||||
'copyright': json['copyright'],
|
||||
'explicit': json['explicit'] == true || json['explicit'] == 1 ? 1 : 0,
|
||||
'has_lyrics': json['hasLyrics'] == true || json['hasLyrics'] == 1 ? 1 : 0,
|
||||
'format': json['format'],
|
||||
'audio_metadata_scan_version':
|
||||
(json['audioMetadataScanVersion'] as num?)?.toInt() ??
|
||||
@@ -525,6 +536,7 @@ class LibraryDatabase {
|
||||
'label': row['label'],
|
||||
'copyright': row['copyright'],
|
||||
'explicit': row['explicit'] == 1 || row['explicit'] == true,
|
||||
'hasLyrics': row['has_lyrics'] == 1 || row['has_lyrics'] == true,
|
||||
'format': row['format'],
|
||||
};
|
||||
}
|
||||
@@ -1486,6 +1498,7 @@ class LibraryDatabase {
|
||||
int? sampleRate,
|
||||
int? bitrate,
|
||||
bool? explicit,
|
||||
bool? hasLyrics,
|
||||
String? format,
|
||||
}) async {
|
||||
final values = <String, dynamic>{};
|
||||
@@ -1504,6 +1517,9 @@ class LibraryDatabase {
|
||||
if (explicit != null) {
|
||||
values['explicit'] = explicit ? 1 : 0;
|
||||
}
|
||||
if (hasLyrics != null) {
|
||||
values['has_lyrics'] = hasLyrics ? 1 : 0;
|
||||
}
|
||||
final normalizedFormat = normalizeAudioFormatValue(format);
|
||||
if (normalizedFormat != null) {
|
||||
values['format'] = normalizedFormat;
|
||||
|
||||
@@ -39,6 +39,7 @@ class LocalLibraryItem {
|
||||
final String? label;
|
||||
final String? copyright;
|
||||
final bool explicit;
|
||||
final bool hasLyrics;
|
||||
final String? format; // flac, alac, eac3, ac3, ac4, mp3, opus, m4a
|
||||
|
||||
const LocalLibraryItem({
|
||||
@@ -67,6 +68,7 @@ class LocalLibraryItem {
|
||||
this.label,
|
||||
this.copyright,
|
||||
this.explicit = false,
|
||||
this.hasLyrics = false,
|
||||
this.format,
|
||||
});
|
||||
|
||||
@@ -96,6 +98,7 @@ class LocalLibraryItem {
|
||||
'label': label,
|
||||
'copyright': copyright,
|
||||
'explicit': explicit,
|
||||
'hasLyrics': hasLyrics,
|
||||
'format': format,
|
||||
};
|
||||
|
||||
@@ -126,6 +129,7 @@ class LocalLibraryItem {
|
||||
label: json['label'] as String?,
|
||||
copyright: json['copyright'] as String?,
|
||||
explicit: json['explicit'] == true || json['explicit'] == 1,
|
||||
hasLyrics: json['hasLyrics'] == true || json['hasLyrics'] == 1,
|
||||
format: json['format'] as String?,
|
||||
);
|
||||
|
||||
@@ -135,6 +139,7 @@ class LocalLibraryItem {
|
||||
int? sampleRate,
|
||||
int? bitrate,
|
||||
bool? explicit,
|
||||
bool? hasLyrics,
|
||||
String? format,
|
||||
}) {
|
||||
return LocalLibraryItem(
|
||||
@@ -163,6 +168,7 @@ class LocalLibraryItem {
|
||||
label: label,
|
||||
copyright: copyright,
|
||||
explicit: explicit ?? this.explicit,
|
||||
hasLyrics: hasLyrics ?? this.hasLyrics,
|
||||
format: format ?? this.format,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -385,6 +385,7 @@ extension _LibraryDbQueueSql on LibraryDatabase {
|
||||
discNumberExpr: 'h.disc_number',
|
||||
isrcExpr: 'h.isrc',
|
||||
labelExpr: 'h.label',
|
||||
hasLyricsExpr: 'h.has_lyrics',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -415,6 +416,7 @@ extension _LibraryDbQueueSql on LibraryDatabase {
|
||||
discNumberExpr: 'l.disc_number',
|
||||
isrcExpr: 'l.isrc',
|
||||
labelExpr: 'l.label',
|
||||
hasLyricsExpr: 'l.has_lyrics',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -434,6 +436,7 @@ extension _LibraryDbQueueSql on LibraryDatabase {
|
||||
required String discNumberExpr,
|
||||
required String isrcExpr,
|
||||
required String labelExpr,
|
||||
required String hasLyricsExpr,
|
||||
}) {
|
||||
final quality = request.quality?.trim().toLowerCase();
|
||||
if (quality != null && quality.isNotEmpty) {
|
||||
@@ -518,6 +521,9 @@ extension _LibraryDbQueueSql on LibraryDatabase {
|
||||
case 'missing-label':
|
||||
where.add('NOT ($hasLabel)');
|
||||
break;
|
||||
case 'missing-lyrics':
|
||||
where.add('COALESCE($hasLyricsExpr, 0) = 0');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user