Files
SpotiFLAC-Mobile/lib/services/library_database_queue_sql.dart
T

817 lines
25 KiB
Dart

part of 'library_database.dart';
// SQL builders for the queue tab's history+local union queries.
class _QueueOrderTerm {
final String column;
final bool descending;
const _QueueOrderTerm(this.column, {this.descending = false});
}
extension _LibraryDbQueueSql on LibraryDatabase {
String _queueTrackUnionSql(
QueueLibraryDbQuery request,
List<Object?> args, {
required List<_QueueOrderTerm> orderTerms,
required bool usesCursor,
}) {
final parts = <String>[];
if (request.source != 'local') {
final where = <String>[];
_appendQueueHistoryFilters(where, args, request);
if (request.filterMode == 'singles') {
where.add('''
h.album_key IN (
SELECT album_key
FROM history_db.history
GROUP BY album_key
HAVING COUNT(*) = 1
)
''');
}
final selectSql =
'''
SELECT
'downloaded' AS queue_source,
'dl_' || h.id AS unified_id,
h.id,
h.track_name,
h.artist_name,
h.album_name,
h.album_artist,
h.cover_url,
h.file_path,
h.storage_mode,
h.download_tree_uri,
h.saf_relative_dir,
h.saf_file_name,
h.saf_repaired,
h.service,
h.downloaded_at,
h.isrc,
h.spotify_id,
h.track_number,
h.total_tracks,
h.disc_number,
h.total_discs,
h.duration,
h.release_date,
h.quality,
h.bit_depth,
h.sample_rate,
h.genre,
h.composer,
h.label,
h.copyright,
NULL AS cover_path,
NULL AS scanned_at,
NULL AS file_mod_time,
h.bitrate,
h.format,
h.sort_track,
h.sort_artist,
h.sort_album,
h.sort_genre,
h.sort_release,
h.sort_added
FROM history_db.history h
${where.isEmpty ? '' : 'WHERE ${where.join(' AND ')}'}
''';
parts.add(
_boundedQueuePart(
selectSql,
request,
args,
orderTerms,
usesCursor: usesCursor,
),
);
}
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);
if (request.filterMode == 'singles') {
where.add('''
l.album_key IN (
SELECT album_key
FROM ${LibraryDatabase.visibleLibraryView} 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
HAVING COUNT(*) = 1
)
''');
}
final selectSql =
'''
SELECT
'local' AS queue_source,
'local_' || l.id AS unified_id,
l.id,
l.track_name,
l.artist_name,
l.album_name,
l.album_artist,
NULL AS cover_url,
l.file_path,
NULL AS storage_mode,
NULL AS download_tree_uri,
NULL AS saf_relative_dir,
NULL AS saf_file_name,
0 AS saf_repaired,
'local' AS service,
NULL AS downloaded_at,
l.isrc,
NULL AS spotify_id,
l.track_number,
l.total_tracks,
l.disc_number,
l.total_discs,
l.duration,
l.release_date,
NULL AS quality,
l.bit_depth,
l.sample_rate,
l.genre,
l.composer,
l.label,
l.copyright,
l.cover_path,
l.scanned_at,
l.file_mod_time,
l.bitrate,
l.format,
l.track_name_norm AS sort_track,
l.artist_name_norm AS sort_artist,
l.album_name_norm AS sort_album,
l.sort_genre,
l.sort_release,
l.sort_added
FROM ${LibraryDatabase.visibleLibraryView} l
${where.isEmpty ? '' : 'WHERE ${where.join(' AND ')}'}
''';
parts.add(
_boundedQueuePart(
selectSql,
request,
args,
orderTerms,
usesCursor: usesCursor,
),
);
}
if (parts.isEmpty) {
return '''
SELECT
NULL AS queue_source,
NULL AS unified_id,
NULL AS id,
NULL AS track_name,
NULL AS artist_name,
NULL AS album_name,
NULL AS album_artist,
NULL AS cover_url,
NULL AS file_path,
NULL AS storage_mode,
NULL AS download_tree_uri,
NULL AS saf_relative_dir,
NULL AS saf_file_name,
NULL AS saf_repaired,
NULL AS service,
NULL AS downloaded_at,
NULL AS isrc,
NULL AS spotify_id,
NULL AS track_number,
NULL AS total_tracks,
NULL AS disc_number,
NULL AS total_discs,
NULL AS duration,
NULL AS release_date,
NULL AS quality,
NULL AS bit_depth,
NULL AS sample_rate,
NULL AS genre,
NULL AS composer,
NULL AS label,
NULL AS copyright,
NULL AS cover_path,
NULL AS scanned_at,
NULL AS file_mod_time,
NULL AS bitrate,
NULL AS format,
NULL AS sort_track,
NULL AS sort_artist,
NULL AS sort_album,
NULL AS sort_genre,
NULL AS sort_release,
NULL AS sort_added
WHERE 0
''';
}
return parts.join(' UNION ALL ');
}
String _queueAlbumUnionSql(
QueueLibraryDbQuery request,
List<Object?> args, {
required List<_QueueOrderTerm> orderTerms,
required bool usesCursor,
}) {
final parts = <String>[];
if (request.source != 'local') {
final where = <String>[];
_appendQueueHistoryFilters(where, args, request);
final selectSql =
'''
SELECT
'downloaded' AS queue_source,
c.album_key,
MIN(h.album_name) AS album_name,
COALESCE(NULLIF(MIN(h.album_artist), ''), MIN(h.artist_name)) AS artist_name,
MAX(CASE WHEN h.cover_url IS NOT NULL AND h.cover_url != '' THEN h.cover_url END) AS cover_url,
NULL AS cover_path,
MAX(h.file_path) AS sample_file_path,
COUNT(*) AS track_count,
c.latest_added AS sort_added,
MIN(COALESCE(h.sort_album, '')) AS sort_album,
MIN(COALESCE(h.sort_album_artist, '')) AS sort_artist,
COALESCE(MAX(h.release_date), '') AS sort_release,
COALESCE(MAX(h.sort_genre), '') AS sort_genre
FROM history_db.history h
JOIN (
SELECT
album_key,
COUNT(*) AS track_count,
MAX(COALESCE(sort_added, 0)) AS latest_added
FROM history_db.history
GROUP BY album_key
HAVING COUNT(*) > 1
) c
ON c.album_key = h.album_key
${where.isEmpty ? '' : 'WHERE ${where.join(' AND ')}'}
GROUP BY c.album_key
''';
parts.add(
_boundedQueuePart(
selectSql,
request,
args,
orderTerms,
usesCursor: usesCursor,
),
);
}
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);
final selectSql =
'''
SELECT
'local' AS queue_source,
c.album_key,
MIN(l.album_name) AS album_name,
COALESCE(NULLIF(MIN(l.album_artist), ''), MIN(l.artist_name)) AS artist_name,
NULL AS cover_url,
MAX(CASE WHEN l.cover_path IS NOT NULL AND l.cover_path != '' THEN l.cover_path END) AS cover_path,
MAX(l.file_path) AS sample_file_path,
COUNT(*) AS track_count,
c.latest_added AS sort_added,
MIN(l.album_name_norm) AS sort_album,
MIN(l.album_artist_norm) AS sort_artist,
COALESCE(MAX(l.release_date), '') AS sort_release,
COALESCE(MAX(l.sort_genre), '') AS sort_genre
FROM ${LibraryDatabase.visibleLibraryView} l
JOIN (
SELECT
album_key,
COUNT(*) AS track_count,
MAX(COALESCE(sort_added, 0)) AS latest_added
FROM ${LibraryDatabase.visibleLibraryView} 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
HAVING COUNT(*) > 1
) c ON c.album_key = l.album_key
${where.isEmpty ? '' : 'WHERE ${where.join(' AND ')}'}
GROUP BY c.album_key
''';
parts.add(
_boundedQueuePart(
selectSql,
request,
args,
orderTerms,
usesCursor: usesCursor,
),
);
}
if (parts.isEmpty) {
return '''
SELECT
NULL AS queue_source,
NULL AS album_key,
NULL AS album_name,
NULL AS artist_name,
NULL AS cover_url,
NULL AS cover_path,
NULL AS sample_file_path,
NULL AS track_count,
NULL AS sort_added,
NULL AS sort_album,
NULL AS sort_artist,
NULL AS sort_release,
NULL AS sort_genre
WHERE 0
''';
}
return parts.join(' UNION ALL ');
}
void _appendQueueHistoryFilters(
List<String> where,
List<Object?> args,
QueueLibraryDbQuery request,
) {
final query = LibraryDatabase.normalizeLookupText(request.searchQuery);
if (query.isNotEmpty) {
final like = '%${_escapeLikePattern(query)}%';
where.add("h.search_text LIKE ? ESCAPE '\\'");
args.add(like);
}
_appendQueueCommonFilters(
where,
args,
request,
filePathExpr: 'h.file_path',
formatExpr: 'h.format',
qualityExpr: 'h.quality',
bitDepthExpr: 'h.bit_depth',
artistExpr: 'h.artist_name',
albumArtistExpr: 'h.album_artist',
releaseDateExpr: 'h.release_date',
genreExpr: 'h.genre',
trackNumberExpr: 'h.track_number',
discNumberExpr: 'h.disc_number',
isrcExpr: 'h.isrc',
labelExpr: 'h.label',
);
}
void _appendQueueLocalFilters(
List<String> where,
List<Object?> args,
QueueLibraryDbQuery request,
) {
final query = LibraryDatabase.normalizeLookupText(request.searchQuery);
if (query.isNotEmpty) {
final like = '%${_escapeLikePattern(query)}%';
where.add("l.search_text LIKE ? ESCAPE '\\'");
args.add(like);
}
_appendQueueCommonFilters(
where,
args,
request,
filePathExpr: 'l.file_path',
formatExpr: 'l.format',
qualityExpr: 'NULL',
bitDepthExpr: 'l.bit_depth',
artistExpr: 'l.artist_name',
albumArtistExpr: 'l.album_artist',
releaseDateExpr: 'l.release_date',
genreExpr: 'l.genre',
trackNumberExpr: 'l.track_number',
discNumberExpr: 'l.disc_number',
isrcExpr: 'l.isrc',
labelExpr: 'l.label',
);
}
void _appendQueueCommonFilters(
List<String> where,
List<Object?> args,
QueueLibraryDbQuery request, {
required String filePathExpr,
required String? formatExpr,
required String qualityExpr,
required String bitDepthExpr,
required String artistExpr,
required String albumArtistExpr,
required String releaseDateExpr,
required String genreExpr,
required String trackNumberExpr,
required String discNumberExpr,
required String isrcExpr,
required String labelExpr,
}) {
final quality = request.quality?.trim().toLowerCase();
if (quality != null && quality.isNotEmpty) {
final isHiRes =
'(COALESCE($bitDepthExpr, 0) >= 24 OR LOWER(COALESCE($qualityExpr, \'\')) LIKE \'24%\')';
final isCd =
'(COALESCE($bitDepthExpr, 0) = 16 OR LOWER(COALESCE($qualityExpr, \'\')) LIKE \'16%\')';
switch (quality) {
case 'hires':
where.add(isHiRes);
break;
case 'cd':
where.add(isCd);
break;
case 'lossy':
where.add('NOT ($isHiRes OR $isCd)');
break;
}
}
final format = request.format?.trim().toLowerCase();
if (format != null && format.isNotEmpty) {
if (formatExpr == null) {
where.add('LOWER($filePathExpr) LIKE ?');
args.add('%.$format');
} else {
where.add(
'(LOWER(COALESCE($formatExpr, \'\')) = ? OR LOWER($filePathExpr) LIKE ?)',
);
args.addAll([format, '%.$format']);
}
}
final metadata = request.metadata?.trim();
if (metadata == null || metadata.isEmpty) return;
final hasArtist = 'TRIM(COALESCE($artistExpr, \'\')) != \'\'';
final hasAlbumArtist = 'TRIM(COALESCE($albumArtistExpr, \'\')) != \'\'';
final hasReleaseDate =
'TRIM(COALESCE($releaseDateExpr, \'\')) GLOB \'*[0-9][0-9][0-9][0-9]*\'';
final hasGenre = 'TRIM(COALESCE($genreExpr, \'\')) != \'\'';
final hasTrackNumber = 'COALESCE($trackNumberExpr, 0) > 0';
final hasDiscNumber = 'COALESCE($discNumberExpr, 0) > 0';
final hasLabel = 'TRIM(COALESCE($labelExpr, \'\')) != \'\'';
final normalizedIsrc =
'REPLACE(REPLACE(UPPER(TRIM(COALESCE($isrcExpr, \'\'))), \'-\', \'\'), \' \', \'\')';
final hasIncorrectIsrc =
'TRIM(COALESCE($isrcExpr, \'\')) != \'\' AND LENGTH($normalizedIsrc) != 12';
final isComplete =
'($hasArtist AND $hasAlbumArtist AND $hasReleaseDate AND $hasGenre AND $hasTrackNumber AND $hasDiscNumber AND $hasLabel AND NOT ($hasIncorrectIsrc))';
switch (metadata) {
case 'complete':
where.add(isComplete);
break;
case 'missing-any':
where.add('NOT $isComplete');
break;
case 'missing-year':
where.add('NOT ($hasReleaseDate)');
break;
case 'missing-genre':
where.add('NOT ($hasGenre)');
break;
case 'missing-album-artist':
where.add('NOT ($hasAlbumArtist)');
break;
case 'missing-track-number':
where.add('NOT ($hasTrackNumber)');
break;
case 'missing-disc-number':
where.add('NOT ($hasDiscNumber)');
break;
case 'missing-artist':
where.add('NOT ($hasArtist)');
break;
case 'incorrect-isrc-format':
where.add('($hasIncorrectIsrc)');
break;
case 'missing-isrc':
where.add('TRIM(COALESCE($isrcExpr, \'\')) = \'\'');
break;
case 'missing-label':
where.add('NOT ($hasLabel)');
break;
}
}
String _queueTrackOrderBy(String sortMode) {
return _queueOrderBy(_queueTrackOrderTerms(sortMode));
}
String _queueAlbumOrderBy(String sortMode) {
return _queueOrderBy(_queueAlbumOrderTerms(sortMode));
}
String _boundedQueuePart(
String selectSql,
QueueLibraryDbQuery request,
List<Object?> args,
List<_QueueOrderTerm> orderTerms, {
required bool usesCursor,
}) {
final cursorPredicate = usesCursor
? _queueCursorPredicate(request.cursor, orderTerms, args)
: '';
final branchLimit = usesCursor
? request.limit
: request.limit + request.offset;
args.add(branchLimit);
final branchOrder = orderTerms
.where((term) => term.column != 'queue_source')
.toList(growable: false);
return '''
SELECT * FROM (
SELECT *
FROM ($selectSql)
${cursorPredicate.isEmpty ? '' : 'WHERE $cursorPredicate'}
ORDER BY ${_queueOrderBy(branchOrder)}
LIMIT ?
)
''';
}
List<_QueueOrderTerm> _queueTrackOrderTerms(String sortMode) {
return switch (sortMode) {
'oldest' => const [
_QueueOrderTerm('sort_added'),
_QueueOrderTerm('sort_track'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
'a-z' => const [
_QueueOrderTerm('sort_track'),
_QueueOrderTerm('sort_artist'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
'z-a' => const [
_QueueOrderTerm('sort_track', descending: true),
_QueueOrderTerm('sort_artist', descending: true),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
'artist-asc' => const [
_QueueOrderTerm('sort_artist'),
_QueueOrderTerm('sort_track'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
'artist-desc' => const [
_QueueOrderTerm('sort_artist', descending: true),
_QueueOrderTerm('sort_track'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
'album-asc' => const [
_QueueOrderTerm('sort_album'),
_QueueOrderTerm('sort_track'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
'album-desc' => const [
_QueueOrderTerm('sort_album', descending: true),
_QueueOrderTerm('sort_track'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
'release-oldest' => const [
_QueueOrderTerm('sort_release'),
_QueueOrderTerm('sort_track'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
'release-newest' => const [
_QueueOrderTerm('sort_release', descending: true),
_QueueOrderTerm('sort_track'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
'genre-asc' => const [
_QueueOrderTerm('sort_genre'),
_QueueOrderTerm('sort_track'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
'genre-desc' => const [
_QueueOrderTerm('sort_genre', descending: true),
_QueueOrderTerm('sort_track'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
_ => const [
_QueueOrderTerm('sort_added', descending: true),
_QueueOrderTerm('sort_track'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('id'),
],
};
}
List<_QueueOrderTerm> _queueAlbumOrderTerms(String sortMode) {
return switch (sortMode) {
'oldest' => const [
_QueueOrderTerm('sort_added'),
_QueueOrderTerm('sort_album'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('album_key'),
],
'a-z' || 'album-asc' => const [
_QueueOrderTerm('sort_album'),
_QueueOrderTerm('sort_artist'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('album_key'),
],
'z-a' || 'album-desc' => const [
_QueueOrderTerm('sort_album', descending: true),
_QueueOrderTerm('sort_artist', descending: true),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('album_key'),
],
'artist-asc' => const [
_QueueOrderTerm('sort_artist'),
_QueueOrderTerm('sort_album'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('album_key'),
],
'artist-desc' => const [
_QueueOrderTerm('sort_artist', descending: true),
_QueueOrderTerm('sort_album'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('album_key'),
],
'release-oldest' => const [
_QueueOrderTerm('sort_release'),
_QueueOrderTerm('sort_album'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('album_key'),
],
'release-newest' => const [
_QueueOrderTerm('sort_release', descending: true),
_QueueOrderTerm('sort_album'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('album_key'),
],
'genre-asc' => const [
_QueueOrderTerm('sort_genre'),
_QueueOrderTerm('sort_album'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('album_key'),
],
'genre-desc' => const [
_QueueOrderTerm('sort_genre', descending: true),
_QueueOrderTerm('sort_album'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('album_key'),
],
_ => const [
_QueueOrderTerm('sort_added', descending: true),
_QueueOrderTerm('sort_album'),
_QueueOrderTerm('queue_source'),
_QueueOrderTerm('album_key'),
],
};
}
String _queueOrderBy(List<_QueueOrderTerm> terms) => terms
.map((term) => '${term.column} ${term.descending ? 'DESC' : 'ASC'}')
.join(', ');
String _queueCursorPredicate(
QueueLibraryDbCursor? cursor,
List<_QueueOrderTerm> terms,
List<Object?> args,
) {
if (cursor == null || cursor.values.length != terms.length) return '';
final clauses = <String>[];
final first = terms.first;
final coarseOperator = first.descending ? '<=' : '>=';
args.add(cursor.values.first);
for (var i = 0; i < terms.length; i++) {
final comparisons = <String>[];
for (var j = 0; j < i; j++) {
comparisons.add('${terms[j].column} = ?');
args.add(cursor.values[j]);
}
comparisons.add(
'${terms[i].column} ${terms[i].descending ? '<' : '>'} ?',
);
args.add(cursor.values[i]);
clauses.add('(${comparisons.join(' AND ')})');
}
return '(${first.column} $coarseOperator ?) AND (${clauses.join(' OR ')})';
}
QueueLibraryDbCursor? _queueCursorFromRow(
Map<String, dynamic>? row,
List<_QueueOrderTerm> terms,
) {
if (row == null) return null;
final values = <Object>[];
for (final term in terms) {
final value = row[term.column];
if (value is! Object) return null;
values.add(value);
}
return QueueLibraryDbCursor(List<Object>.unmodifiable(values));
}
Map<String, dynamic> _queueTrackRowToJson(Map<String, dynamic> row) {
final source = row['queue_source'] as String? ?? '';
if (source == 'local') {
return {
'source': source,
'item': {
'id': row['id'],
'trackName': row['track_name'],
'artistName': row['artist_name'],
'albumName': row['album_name'],
'albumArtist': row['album_artist'],
'filePath': row['file_path'],
'coverPath': row['cover_path'],
'scannedAt': row['scanned_at'],
'fileModTime': row['file_mod_time'],
'isrc': row['isrc'],
'trackNumber': row['track_number'],
'totalTracks': row['total_tracks'],
'discNumber': row['disc_number'],
'totalDiscs': row['total_discs'],
'duration': row['duration'],
'releaseDate': row['release_date'],
'bitDepth': row['bit_depth'],
'sampleRate': row['sample_rate'],
'bitrate': row['bitrate'],
'genre': row['genre'],
'composer': row['composer'],
'label': row['label'],
'copyright': row['copyright'],
'format': row['format'],
},
};
}
return {
'source': source,
'item': {
'id': row['id'],
'trackName': row['track_name'],
'artistName': row['artist_name'],
'albumName': row['album_name'],
'albumArtist': row['album_artist'],
'coverUrl': row['cover_url'],
'filePath': row['file_path'],
'storageMode': row['storage_mode'],
'downloadTreeUri': row['download_tree_uri'],
'safRelativeDir': row['saf_relative_dir'],
'safFileName': row['saf_file_name'],
'safRepaired': row['saf_repaired'] == 1 || row['saf_repaired'] == true,
'service': row['service'],
'downloadedAt': row['downloaded_at'],
'isrc': row['isrc'],
'spotifyId': row['spotify_id'],
'trackNumber': row['track_number'],
'totalTracks': row['total_tracks'],
'discNumber': row['disc_number'],
'totalDiscs': row['total_discs'],
'duration': row['duration'],
'releaseDate': row['release_date'],
'quality': row['quality'],
'bitDepth': row['bit_depth'],
'sampleRate': row['sample_rate'],
'bitrate': row['bitrate'],
'format': row['format'],
'genre': row['genre'],
'composer': row['composer'],
'label': row['label'],
'copyright': row['copyright'],
},
};
}
}