Files
QingTing-Player/lib/database/song_database.dart
T
lxh2875931338 46a9add5cb build+fix: debug 包独立为 .dev,并修复元数据缓存永远查不到
包名:
- debug 使用 com.lxh.qingting_player.dev(应用名"清听 Dev"),可与 release 并存
- release 保持 com.lxh.qingting_player
- Android 包名每一段必须以字母开头,因此用 .dev 而非 _dev
- AGP 9 需显式开启 buildFeatures.resValues;移除 manifest 中已废弃的 package 属性

元数据(两个独立原因叠加):
1. song_key 之前用 String.hashCode / 本地临时文件 mtime,都不稳定;
2. songs.remote_path 存的是完整 URL,而所有查询都用相对路径(song.id),
   getSongByPath 永远 miss,UI 只能回退到文件名。

- 新增 utils/song_key.dart:songKeyFor(url,size)=md5(url|size),跨进程稳定
- metadata_service: remote_path 改存 fileId(相对路径),并改用稳定 song_key
- audio_service / file_provider: 同步使用稳定键
- 数据库升级 v5:清空 songs / metadata_cache(可再生缓存;歌单与收藏按路径存储,不受影响)
- device-check.ps1: 支持 -Package,默认校验 dev 包,启动组件改为向 PackageManager 解析

验证:两次独立 dart 进程 song_key 输出一致;真机 debug/release 并存安装,单实例自检通过。
2026-10-22 23:47:32 +08:00

513 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// lib/database/song_database.dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import '../services/audio_service.dart'; // 用于 PlayMode 枚举,但为了解耦我们传字符串
class SongDatabase {
static final SongDatabase _instance = SongDatabase._internal();
factory SongDatabase() => _instance;
SongDatabase._internal();
static Database? _database;
static bool enableLog = true;
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final dir = await getApplicationDocumentsDirectory();
final path = join(dir.path, 'qingting_songs.db');
_log('📂 数据库路径: $path');
return await openDatabase(
path,
version: 5, // v5:song_key 改为稳定的 md5
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
void _log(String message) {
if (enableLog) debugPrint('📦 [DB] $message');
}
void _logQuery(String table, String operation, {Map<String, dynamic>? args}) {
if (enableLog) {
final argsStr = args != null && args.isNotEmpty ? ' $args' : '';
debugPrint('📦 [DB] $table → $operation$argsStr');
}
}
Future<bool> _tableExists(Database db, String tableName) async {
final result = await db.query(
'sqlite_master',
where: 'type = ? AND name = ?',
whereArgs: ['table', tableName],
);
return result.isNotEmpty;
}
Future<bool> _columnExists(
Database db, String tableName, String columnName) async {
final result = await db.rawQuery('PRAGMA table_info($tableName)');
return result.any((col) => col['name'] == columnName);
}
Future<void> _onCreate(Database db, int version) async {
_log('🆕 创建数据库 (version $version)');
await _createSongsTable(db);
await _createMetadataCacheTable(db);
await _createPlaylistsTable(db);
await _createPlaylistSongsTable(db);
await _createFavoritesTable(db);
await _createPlaybackStateTable(db);
await _createIndexes(db);
_log('✅ 数据库创建完成');
}
Future<void> _createSongsTable(Database db) async {
await db.execute('''
CREATE TABLE songs (
song_key TEXT PRIMARY KEY,
remote_path TEXT,
content_hash TEXT,
file_size INTEGER,
modified_time INTEGER,
etag TEXT,
title TEXT,
artist TEXT,
album TEXT,
genre TEXT,
artwork_path TEXT,
confidence REAL DEFAULT 0.0,
validation_count INTEGER DEFAULT 0,
first_scan INTEGER,
last_scan INTEGER,
metadata_status TEXT DEFAULT 'pending'
)
''');
_log('📋 表创建: songs');
}
Future<void> _createMetadataCacheTable(Database db) async {
await db.execute('''
CREATE TABLE metadata_cache (
song_key TEXT PRIMARY KEY,
cache_path TEXT,
cache_created_at INTEGER,
cache_size INTEGER,
FOREIGN KEY (song_key) REFERENCES songs(song_key) ON DELETE CASCADE
)
''');
_log('📋 表创建: metadata_cache');
}
Future<void> _createPlaylistsTable(Database db) async {
await db.execute('''
CREATE TABLE playlists (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
play_mode TEXT DEFAULT 'sequential',
created_at INTEGER,
updated_at INTEGER
)
''');
_log('📋 表创建: playlists');
}
Future<void> _createPlaylistSongsTable(Database db) async {
await db.execute('''
CREATE TABLE playlist_songs (
playlist_id TEXT,
song_id TEXT,
order_index INTEGER,
added_at INTEGER,
PRIMARY KEY (playlist_id, song_id),
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE
)
''');
_log('📋 表创建: playlist_songs');
}
Future<void> _createFavoritesTable(Database db) async {
await db.execute('''
CREATE TABLE favorites (
song_id TEXT PRIMARY KEY,
favorited_at INTEGER,
FOREIGN KEY (song_id) REFERENCES songs(remote_path) ON DELETE CASCADE
)
''');
_log('📋 表创建: favorites');
}
Future<void> _createPlaybackStateTable(Database db) async {
await db.execute('''
CREATE TABLE playback_state (
id INTEGER PRIMARY KEY,
queue_json TEXT,
current_index INTEGER,
play_mode TEXT DEFAULT 'sequential',
current_playlist_id TEXT,
position_ms INTEGER DEFAULT 0,
updated_at INTEGER
)
''');
_log('📋 表创建: playback_state');
}
Future<void> _createIndexes(Database db) async {
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_songs_artist ON songs(artist)');
await db
.execute('CREATE INDEX IF NOT EXISTS idx_songs_title ON songs(title)');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_songs_content_hash ON songs(content_hash)');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_playlist_songs_playlist ON playlist_songs(playlist_id)');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_playlist_songs_order ON playlist_songs(order_index)');
_log('📋 索引创建完成');
}
// ════════════════════════════════════════════════════════════
// 升级逻辑(版本 3 → 4)
// ════════════════════════════════════════════════════════════
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
_log('⬆️ 数据库升级: $oldVersion → $newVersion');
if (oldVersion < 2) {
if (!await _columnExists(db, 'songs', 'content_hash')) {
await db.execute('ALTER TABLE songs ADD COLUMN content_hash TEXT');
_log('🔧 添加列: songs.content_hash');
}
final playlistsExists = await _tableExists(db, 'playlists');
if (!playlistsExists) {
await _createPlaylistsTable(db);
await _createPlaylistSongsTable(db);
await _createFavoritesTable(db);
} else {
if (!await _columnExists(db, 'playlists', 'play_mode')) {
await db.execute(
'ALTER TABLE playlists ADD COLUMN play_mode TEXT DEFAULT "sequential"');
_log('🔧 添加列: playlists.play_mode');
}
}
await _createIndexes(db);
}
if (oldVersion < 3) {
if (!await _columnExists(db, 'songs', 'content_hash')) {
await db.execute('ALTER TABLE songs ADD COLUMN content_hash TEXT');
_log('🔧 添加列: songs.content_hash');
}
final playlistsExists = await _tableExists(db, 'playlists');
if (!playlistsExists) {
await _createPlaylistsTable(db);
await _createPlaylistSongsTable(db);
await _createFavoritesTable(db);
} else {
if (!await _columnExists(db, 'playlists', 'play_mode')) {
await db.execute(
'ALTER TABLE playlists ADD COLUMN play_mode TEXT DEFAULT "sequential"');
_log('🔧 添加列: playlists.play_mode');
}
}
await _createIndexes(db);
}
// ⭐ 升级到版本 4:添加 playback_state 表
if (oldVersion < 4) {
final exists = await _tableExists(db, 'playback_state');
if (!exists) {
await _createPlaybackStateTable(db);
}
}
// ⭐ 升级到版本 5:song_key 由 String.hashCode 改为 md5(url|size|mtime)。
// 旧键带进程级随机种子、无法换算;songs / metadata_cache 都是可再生的缓存,
// 直接清空,下次播放时按需重建。歌单与收藏按路径(song_id)存储,不受影响。
if (oldVersion < 5) {
await db.delete('metadata_cache');
await db.delete('songs');
_log('🧹 已清空 songs / metadata_cache(song_key 算法升级)');
}
_log('✅ 数据库升级完成');
}
// ════════════════════════════════════════════════════════════
// 查询方法(原有)
// ════════════════════════════════════════════════════════════
Future<Map<String, dynamic>?> getSong(String songKey) async {
_logQuery('songs', 'get', args: {'song_key': songKey});
final db = await database;
final result =
await db.query('songs', where: 'song_key = ?', whereArgs: [songKey]);
return result.isNotEmpty ? result.first : null;
}
Future<Map<String, dynamic>?> getSongByPath(String remotePath) async {
_logQuery('songs', 'getByPath', args: {'remote_path': remotePath});
final db = await database;
final result = await db
.query('songs', where: 'remote_path = ?', whereArgs: [remotePath]);
return result.isNotEmpty ? result.first : null;
}
Future<Map<String, dynamic>?> getSongByHash(String contentHash) async {
_logQuery('songs', 'getByHash', args: {'content_hash': contentHash});
final db = await database;
final result = await db
.query('songs', where: 'content_hash = ?', whereArgs: [contentHash]);
return result.isNotEmpty ? result.first : null;
}
Future<List<Map<String, dynamic>>> getSongsByArtist(String artist,
{int limit = 20}) async {
_logQuery('songs', 'getByArtist', args: {'artist': artist, 'limit': limit});
if (artist.isEmpty) return [];
final db = await database;
return await db.query(
'songs',
where: 'artist = ?',
whereArgs: [artist],
limit: limit,
);
}
// ════════════════════════════════════════════════════════════
// 播放列表 CRUD(原有)
// ════════════════════════════════════════════════════════════
Future<List<Map<String, dynamic>>> getAllPlaylists() async {
_logQuery('playlists', 'getAll');
final db = await database;
return await db.query('playlists', orderBy: 'created_at DESC');
}
Future<Map<String, dynamic>?> getPlaylist(String id) async {
_logQuery('playlists', 'get', args: {'id': id});
final db = await database;
final result =
await db.query('playlists', where: 'id = ?', whereArgs: [id]);
return result.isNotEmpty ? result.first : null;
}
Future<void> insertPlaylist(Map<String, dynamic> playlist) async {
_logQuery('playlists', 'insert', args: {'name': playlist['name']});
final db = await database;
await db.insert('playlists', playlist,
conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<void> updatePlaylist(String id, Map<String, dynamic> updates) async {
_logQuery('playlists', 'update',
args: {'id': id, 'updates': updates.keys.join(',')});
final db = await database;
await db.update('playlists', updates, where: 'id = ?', whereArgs: [id]);
}
Future<void> deletePlaylist(String id) async {
_logQuery('playlists', 'delete', args: {'id': id});
final db = await database;
await db.delete('playlists', where: 'id = ?', whereArgs: [id]);
}
// ════════════════════════════════════════════════════════════
// 播放列表歌曲(原有)
// ════════════════════════════════════════════════════════════
Future<List<Map<String, dynamic>>> getPlaylistSongs(String playlistId) async {
_logQuery('playlist_songs', 'get', args: {'playlist_id': playlistId});
final db = await database;
return await db.query(
'playlist_songs',
where: 'playlist_id = ?',
whereArgs: [playlistId],
orderBy: 'order_index ASC',
);
}
Future<void> addSongToPlaylist(String playlistId, String songId,
{int? orderIndex}) async {
_logQuery('playlist_songs', 'add',
args: {'playlist_id': playlistId, 'song_id': songId});
final db = await database;
int finalOrder = orderIndex ?? 0;
if (orderIndex == null) {
final existing = await db.query(
'playlist_songs',
where: 'playlist_id = ?',
whereArgs: [playlistId],
orderBy: 'order_index DESC',
limit: 1,
);
finalOrder =
existing.isNotEmpty ? (existing.first['order_index'] as int) + 1 : 0;
}
await db.insert(
'playlist_songs',
{
'playlist_id': playlistId,
'song_id': songId,
'order_index': finalOrder,
'added_at': DateTime.now().millisecondsSinceEpoch,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<void> removeSongFromPlaylist(String playlistId, String songId) async {
_logQuery('playlist_songs', 'remove',
args: {'playlist_id': playlistId, 'song_id': songId});
final db = await database;
await db.delete(
'playlist_songs',
where: 'playlist_id = ? AND song_id = ?',
whereArgs: [playlistId, songId],
);
}
Future<void> clearPlaylist(String playlistId) async {
_logQuery('playlist_songs', 'clear', args: {'playlist_id': playlistId});
final db = await database;
await db.delete('playlist_songs',
where: 'playlist_id = ?', whereArgs: [playlistId]);
}
// ════════════════════════════════════════════════════════════
// 收藏(原有)
// ════════════════════════════════════════════════════════════
Future<List<Map<String, dynamic>>> getFavorites() async {
_logQuery('favorites', 'getAll');
final db = await database;
return await db.query('favorites', orderBy: 'favorited_at DESC');
}
Future<bool> isFavorite(String songId) async {
_logQuery('favorites', 'isFavorite', args: {'song_id': songId});
final db = await database;
final result =
await db.query('favorites', where: 'song_id = ?', whereArgs: [songId]);
return result.isNotEmpty;
}
Future<void> addFavorite(String songId) async {
_logQuery('favorites', 'add', args: {'song_id': songId});
final db = await database;
await db.insert(
'favorites',
{
'song_id': songId,
'favorited_at': DateTime.now().millisecondsSinceEpoch,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<void> removeFavorite(String songId) async {
_logQuery('favorites', 'remove', args: {'song_id': songId});
final db = await database;
await db.delete('favorites', where: 'song_id = ?', whereArgs: [songId]);
}
Future<void> toggleFavorite(String songId) async {
if (await isFavorite(songId)) {
await removeFavorite(songId);
} else {
await addFavorite(songId);
}
}
// ════════════════════════════════════════════════════════════
// 原有方法(歌曲 CRUD)
// ════════════════════════════════════════════════════════════
Future<void> insertSong(Map<String, dynamic> song) async {
_logQuery('songs', 'insert', args: {'title': song['title']});
final db = await database;
await db.insert('songs', song,
conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<void> updateSong(String songKey, Map<String, dynamic> updates) async {
_logQuery('songs', 'update',
args: {'song_key': songKey, 'fields': updates.keys.join(',')});
final db = await database;
await db
.update('songs', updates, where: 'song_key = ?', whereArgs: [songKey]);
}
Future<void> deleteSong(String songKey) async {
_logQuery('songs', 'delete', args: {'song_key': songKey});
final db = await database;
await db.delete('songs', where: 'song_key = ?', whereArgs: [songKey]);
}
Future<void> deleteCache(String songKey) async {
_logQuery('metadata_cache', 'delete', args: {'song_key': songKey});
final db = await database;
await db
.delete('metadata_cache', where: 'song_key = ?', whereArgs: [songKey]);
}
Future<void> insertCache(Map<String, dynamic> cache) async {
_logQuery('metadata_cache', 'insert',
args: {'song_key': cache['song_key']});
final db = await database;
await db.insert('metadata_cache', cache,
conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<Map<String, dynamic>?> getCache(String songKey) async {
_logQuery('metadata_cache', 'get', args: {'song_key': songKey});
final db = await database;
final result = await db
.query('metadata_cache', where: 'song_key = ?', whereArgs: [songKey]);
return result.isNotEmpty ? result.first : null;
}
// ════════════════════════════════════════════════════════════
// ⭐ 新增:播放状态持久化
// ════════════════════════════════════════════════════════════
/// 保存播放状态
Future<void> savePlaybackState({
required List<Map<String, dynamic>> queueJson, // 预先序列化的列表
required int currentIndex,
required String playMode,
String? currentPlaylistId,
required int positionMs,
}) async {
final db = await database;
await db.insert(
'playback_state',
{
'id': 1,
'queue_json': jsonEncode(queueJson),
'current_index': currentIndex,
'play_mode': playMode,
'current_playlist_id': currentPlaylistId,
'position_ms': positionMs,
'updated_at': DateTime.now().millisecondsSinceEpoch,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
_log('💾 播放状态已保存');
}
/// 获取播放状态
Future<Map<String, dynamic>?> getPlaybackState() async {
final db = await database;
final result =
await db.query('playback_state', where: 'id = ?', whereArgs: [1]);
return result.isNotEmpty ? result.first : null;
}
Future<void> close() async {
_log('🔒 关闭数据库连接');
final db = await database;
await db.close();
_database = null;
}
}