mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-29 07:18:49 +02:00
perf(library): streamline search and grouped counts
This commit is contained in:
@@ -273,7 +273,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
void _onSearchChanged(String value) {
|
||||
_searchDebounce?.cancel();
|
||||
final normalized = value.trim().toLowerCase();
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 180), () {
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 350), () {
|
||||
if (!mounted || _searchQuery == normalized) return;
|
||||
setState(() {
|
||||
_searchQuery = normalized;
|
||||
|
||||
@@ -73,7 +73,7 @@ class HistoryDatabase {
|
||||
if (_database != null) return _database!;
|
||||
_database = await sqlite.openAppDatabase(
|
||||
'history.db',
|
||||
version: 9,
|
||||
version: 10,
|
||||
onCreate: _createDB,
|
||||
onUpgrade: _upgradeDB,
|
||||
);
|
||||
@@ -118,7 +118,9 @@ class HistoryDatabase {
|
||||
copyright TEXT,
|
||||
spotify_id_norm TEXT,
|
||||
isrc_norm TEXT,
|
||||
match_key TEXT
|
||||
match_key TEXT,
|
||||
album_key TEXT,
|
||||
search_text TEXT
|
||||
)
|
||||
''');
|
||||
|
||||
@@ -188,13 +190,17 @@ class HistoryDatabase {
|
||||
await sqlite.addColumnIfMissing(db, 'history', 'spotify_id_norm', 'TEXT');
|
||||
await sqlite.addColumnIfMissing(db, 'history', 'isrc_norm', 'TEXT');
|
||||
await sqlite.addColumnIfMissing(db, 'history', 'match_key', 'TEXT');
|
||||
await _backfillNormalizedColumns(db);
|
||||
await _createNormalizedIndexes(db);
|
||||
}
|
||||
if (oldVersion < 9) {
|
||||
await sqlite.addColumnIfMissing(db, 'history', 'bitrate', 'INTEGER');
|
||||
await sqlite.addColumnIfMissing(db, 'history', 'format', 'TEXT');
|
||||
}
|
||||
if (oldVersion < 10) {
|
||||
await sqlite.addColumnIfMissing(db, 'history', 'album_key', 'TEXT');
|
||||
await sqlite.addColumnIfMissing(db, 'history', 'search_text', 'TEXT');
|
||||
await _backfillNormalizedColumns(db);
|
||||
await _createNormalizedIndexes(db);
|
||||
}
|
||||
}
|
||||
|
||||
static String normalizeLookupText(String? value) =>
|
||||
@@ -238,12 +244,23 @@ class HistoryDatabase {
|
||||
await db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS idx_history_match_key ON history(match_key)',
|
||||
);
|
||||
await db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS idx_history_album_key ON history(album_key)',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _backfillNormalizedColumns(Database db) async {
|
||||
final rows = await db.query(
|
||||
'history',
|
||||
columns: ['id', 'spotify_id', 'isrc', 'track_name', 'artist_name'],
|
||||
columns: [
|
||||
'id',
|
||||
'spotify_id',
|
||||
'isrc',
|
||||
'track_name',
|
||||
'artist_name',
|
||||
'album_name',
|
||||
'album_artist',
|
||||
],
|
||||
);
|
||||
final batch = db.batch();
|
||||
for (final row in rows) {
|
||||
@@ -254,6 +271,8 @@ class HistoryDatabase {
|
||||
isrc: row['isrc'] as String?,
|
||||
trackName: row['track_name'] as String?,
|
||||
artistName: row['artist_name'] as String?,
|
||||
albumName: row['album_name'] as String?,
|
||||
albumArtist: row['album_artist'] as String?,
|
||||
),
|
||||
where: 'id = ?',
|
||||
whereArgs: [row['id']],
|
||||
@@ -267,11 +286,26 @@ class HistoryDatabase {
|
||||
required String? isrc,
|
||||
required String? trackName,
|
||||
required String? artistName,
|
||||
required String? albumName,
|
||||
required String? albumArtist,
|
||||
}) {
|
||||
final normalizedTrack = normalizeLookupText(trackName);
|
||||
final normalizedArtist = normalizeLookupText(artistName);
|
||||
final normalizedAlbum = normalizeLookupText(albumName);
|
||||
final normalizedAlbumArtist = normalizeLookupText(
|
||||
(albumArtist ?? '').trim().isEmpty ? artistName : albumArtist,
|
||||
);
|
||||
return {
|
||||
'spotify_id_norm': normalizeSpotifyId(spotifyId),
|
||||
'isrc_norm': normalizeIsrc(isrc),
|
||||
'match_key': matchKeyFor(trackName, artistName),
|
||||
'album_key': '$normalizedAlbum|$normalizedAlbumArtist',
|
||||
'search_text': [
|
||||
normalizedTrack,
|
||||
normalizedArtist,
|
||||
normalizedAlbum,
|
||||
normalizedAlbumArtist,
|
||||
].where((value) => value.isNotEmpty).join(' '),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -466,6 +500,8 @@ class HistoryDatabase {
|
||||
isrc: json['isrc'] as String?,
|
||||
trackName: json['trackName'] as String?,
|
||||
artistName: json['artistName'] as String?,
|
||||
albumName: json['albumName'] as String?,
|
||||
albumArtist: json['albumArtist'] as String?,
|
||||
),
|
||||
);
|
||||
return row;
|
||||
@@ -562,11 +598,12 @@ class HistoryDatabase {
|
||||
String artistName,
|
||||
) async {
|
||||
final db = await database;
|
||||
final albumKey =
|
||||
'${normalizeLookupText(albumName)}|${normalizeLookupText(artistName)}';
|
||||
final rows = await db.query(
|
||||
'history',
|
||||
where:
|
||||
'LOWER(album_name) = ? AND LOWER(COALESCE(album_artist, artist_name)) = ?',
|
||||
whereArgs: [albumName.toLowerCase(), artistName.toLowerCase()],
|
||||
where: 'album_key = ?',
|
||||
whereArgs: [albumKey],
|
||||
orderBy:
|
||||
'COALESCE(disc_number, 0), COALESCE(track_number, 0), track_name',
|
||||
);
|
||||
@@ -794,7 +831,7 @@ class HistoryDatabase {
|
||||
FROM (
|
||||
SELECT COUNT(*) AS track_count
|
||||
FROM history
|
||||
GROUP BY LOWER(album_name), LOWER(COALESCE(album_artist, artist_name))
|
||||
GROUP BY album_key
|
||||
)
|
||||
''');
|
||||
final row = rows.isEmpty ? const <String, Object?>{} : rows.first;
|
||||
|
||||
@@ -838,52 +838,75 @@ class LibraryDatabase {
|
||||
Future<QueueLibraryCounts> getQueueCounts(QueueLibraryDbQuery request) async {
|
||||
final db = await database;
|
||||
await _ensureHistoryAttached(db);
|
||||
final parts = <String>[];
|
||||
final args = <Object?>[];
|
||||
|
||||
final allArgs = <Object?>[];
|
||||
final allSql = _queueTrackUnionSql(
|
||||
QueueLibraryDbQuery(
|
||||
limit: request.limit,
|
||||
offset: request.offset,
|
||||
filterMode: 'all',
|
||||
searchQuery: request.searchQuery,
|
||||
source: request.source,
|
||||
quality: request.quality,
|
||||
format: request.format,
|
||||
metadata: request.metadata,
|
||||
sortMode: request.sortMode,
|
||||
includeLocal: request.includeLocal,
|
||||
),
|
||||
allArgs,
|
||||
);
|
||||
final singleArgs = <Object?>[];
|
||||
final singleSql = _queueTrackUnionSql(
|
||||
QueueLibraryDbQuery(
|
||||
limit: request.limit,
|
||||
offset: request.offset,
|
||||
filterMode: 'singles',
|
||||
searchQuery: request.searchQuery,
|
||||
source: request.source,
|
||||
quality: request.quality,
|
||||
format: request.format,
|
||||
metadata: request.metadata,
|
||||
sortMode: request.sortMode,
|
||||
includeLocal: request.includeLocal,
|
||||
),
|
||||
singleArgs,
|
||||
);
|
||||
if (request.source != 'local') {
|
||||
final where = <String>[];
|
||||
_appendQueueHistoryFilters(where, args, request);
|
||||
parts.add('''
|
||||
SELECT
|
||||
COUNT(*) AS all_count,
|
||||
COUNT(DISTINCT CASE WHEN grouped.track_count > 1 THEN h.album_key END) AS album_count,
|
||||
COALESCE(SUM(CASE WHEN grouped.track_count = 1 THEN 1 ELSE 0 END), 0) AS single_count
|
||||
FROM history_db.history h
|
||||
JOIN (
|
||||
SELECT album_key, COUNT(*) AS track_count
|
||||
FROM history_db.history
|
||||
GROUP BY album_key
|
||||
) grouped ON grouped.album_key = h.album_key
|
||||
${where.isEmpty ? '' : 'WHERE ${where.join(' AND ')}'}
|
||||
''');
|
||||
}
|
||||
|
||||
final albumArgs = <Object?>[];
|
||||
final albumSql = _queueAlbumUnionSql(request, albumArgs);
|
||||
if (request.includeLocal && request.source != 'downloaded') {
|
||||
final where = <String>[
|
||||
'''
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM library_path_keys lpk
|
||||
JOIN history_db.history_path_keys hpk ON hpk.path_key = lpk.path_key
|
||||
WHERE lpk.item_id = l.id
|
||||
)
|
||||
''',
|
||||
];
|
||||
_appendQueueLocalFilters(where, args, request);
|
||||
parts.add('''
|
||||
SELECT
|
||||
COUNT(*) AS all_count,
|
||||
COUNT(DISTINCT CASE WHEN grouped.track_count > 1 THEN l.album_key END) AS album_count,
|
||||
COALESCE(SUM(CASE WHEN grouped.track_count = 1 THEN 1 ELSE 0 END), 0) AS single_count
|
||||
FROM library l
|
||||
JOIN (
|
||||
SELECT album_key, COUNT(*) AS track_count
|
||||
FROM library candidate
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM library_path_keys lpk
|
||||
JOIN history_db.history_path_keys hpk ON hpk.path_key = lpk.path_key
|
||||
WHERE lpk.item_id = candidate.id
|
||||
)
|
||||
GROUP BY album_key
|
||||
) grouped ON grouped.album_key = l.album_key
|
||||
WHERE ${where.join(' AND ')}
|
||||
''');
|
||||
}
|
||||
|
||||
final rows = await db.rawQuery(
|
||||
'''
|
||||
if (parts.isEmpty) {
|
||||
return const QueueLibraryCounts(
|
||||
allTrackCount: 0,
|
||||
albumCount: 0,
|
||||
singleTrackCount: 0,
|
||||
);
|
||||
}
|
||||
|
||||
final rows = await db.rawQuery('''
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM ($allSql)) AS all_count,
|
||||
(SELECT COUNT(*) FROM ($singleSql)) AS single_count,
|
||||
(SELECT COUNT(*) FROM ($albumSql)) AS album_count
|
||||
''',
|
||||
[...allArgs, ...singleArgs, ...albumArgs],
|
||||
);
|
||||
COALESCE(SUM(all_count), 0) AS all_count,
|
||||
COALESCE(SUM(single_count), 0) AS single_count,
|
||||
COALESCE(SUM(album_count), 0) AS album_count
|
||||
FROM (${parts.join(' UNION ALL ')})
|
||||
''', args);
|
||||
final row = rows.isNotEmpty ? rows.first : const <String, Object?>{};
|
||||
|
||||
return QueueLibraryCounts(
|
||||
@@ -949,11 +972,10 @@ class LibraryDatabase {
|
||||
_appendQueueHistoryFilters(where, args, request);
|
||||
if (request.filterMode == 'singles') {
|
||||
where.add('''
|
||||
LOWER(h.album_name) || '|' || LOWER(COALESCE(h.album_artist, h.artist_name))
|
||||
IN (
|
||||
SELECT LOWER(album_name) || '|' || LOWER(COALESCE(album_artist, artist_name))
|
||||
h.album_key IN (
|
||||
SELECT album_key
|
||||
FROM history_db.history
|
||||
GROUP BY LOWER(album_name), LOWER(COALESCE(album_artist, artist_name))
|
||||
GROUP BY album_key
|
||||
HAVING COUNT(*) = 1
|
||||
)
|
||||
''');
|
||||
@@ -1158,14 +1180,14 @@ class LibraryDatabase {
|
||||
FROM history_db.history h
|
||||
JOIN (
|
||||
SELECT
|
||||
LOWER(album_name) || '|' || LOWER(COALESCE(album_artist, artist_name)) AS album_key,
|
||||
album_key,
|
||||
COUNT(*) AS track_count,
|
||||
MAX(CAST(strftime('%s', downloaded_at) AS INTEGER) * 1000) AS latest_added
|
||||
FROM history_db.history
|
||||
GROUP BY LOWER(album_name), LOWER(COALESCE(album_artist, artist_name))
|
||||
GROUP BY album_key
|
||||
HAVING COUNT(*) > 1
|
||||
) c
|
||||
ON c.album_key = LOWER(h.album_name) || '|' || LOWER(COALESCE(h.album_artist, h.artist_name))
|
||||
ON c.album_key = h.album_key
|
||||
${where.isEmpty ? '' : 'WHERE ${where.join(' AND ')}'}
|
||||
GROUP BY c.album_key
|
||||
''');
|
||||
@@ -1249,15 +1271,8 @@ class LibraryDatabase {
|
||||
final query = normalizeLookupText(request.searchQuery);
|
||||
if (query.isNotEmpty) {
|
||||
final like = '%${_escapeLikePattern(query)}%';
|
||||
where.add('''
|
||||
(
|
||||
LOWER(h.track_name) LIKE ? ESCAPE '\\' OR
|
||||
LOWER(h.artist_name) LIKE ? ESCAPE '\\' OR
|
||||
LOWER(h.album_name) LIKE ? ESCAPE '\\' OR
|
||||
LOWER(COALESCE(h.album_artist, '')) LIKE ? ESCAPE '\\'
|
||||
)
|
||||
''');
|
||||
args.addAll([like, like, like, like]);
|
||||
where.add("h.search_text LIKE ? ESCAPE '\\'");
|
||||
args.add(like);
|
||||
}
|
||||
_appendQueueCommonFilters(
|
||||
where,
|
||||
|
||||
Reference in New Issue
Block a user