// lib/database/song_database.dart import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path_provider/path_provider.dart'; class SongDatabase { static final SongDatabase _instance = SongDatabase._internal(); factory SongDatabase() => _instance; SongDatabase._internal(); static Database? _database; Future get database async { if (_database != null) return _database!; _database = await _initDatabase(); return _database!; } Future _initDatabase() async { final dir = await getApplicationDocumentsDirectory(); final path = join(dir.path, 'qingting_songs.db'); return await openDatabase( path, version: 2, onCreate: _onCreate, onUpgrade: _onUpgrade, ); } // ⭐ 新增:检查表是否存在 Future _tableExists(Database db, String tableName) async { final result = await db.query( 'sqlite_master', where: 'type = ? AND name = ?', whereArgs: ['table', tableName], ); return result.isNotEmpty; } Future _onCreate(Database db, int version) async { await _createSongsTable(db); await _createMetadataCacheTable(db); await _createPlaylistsTable(db); await _createPlaylistSongsTable(db); await _createFavoritesTable(db); await _createIndexes(db); } // ⭐ 升级逻辑(从版本 1 升级到 2) Future _onUpgrade(Database db, int oldVersion, int newVersion) async { if (oldVersion < 2) { // 检查 playlists 表是否存在 final playlistsExists = await _tableExists(db, 'playlists'); if (!playlistsExists) { // 表不存在 → 创建所有新表 await _createPlaylistsTable(db); await _createPlaylistSongsTable(db); await _createFavoritesTable(db); await _createIndexes(db); } else { // 表已存在 → 只添加新字段 await db.execute( 'ALTER TABLE playlists ADD COLUMN play_mode TEXT DEFAULT "sequential"'); } } } Future _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' ) '''); } Future _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 ) '''); } Future _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 ) '''); } Future _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 ) '''); } Future _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 ) '''); } Future _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)'); } // ════════════════════════════════════════════════════════════ // 查询方法(保持不变) // ════════════════════════════════════════════════════════════ Future?> getSong(String songKey) async { final db = await database; final result = await db.query('songs', where: 'song_key = ?', whereArgs: [songKey]); return result.isNotEmpty ? result.first : null; } Future?> getSongByPath(String remotePath) async { final db = await database; final result = await db .query('songs', where: 'remote_path = ?', whereArgs: [remotePath]); return result.isNotEmpty ? result.first : null; } Future?> getSongByHash(String contentHash) async { final db = await database; final result = await db .query('songs', where: 'content_hash = ?', whereArgs: [contentHash]); return result.isNotEmpty ? result.first : null; } Future>> getSongsByArtist(String artist, {int limit = 20}) async { if (artist.isEmpty) return []; final db = await database; return await db.query( 'songs', where: 'artist = ?', whereArgs: [artist], limit: limit, ); } // ════════════════════════════════════════════════════════════ // 播放列表 CRUD(保持不变) // ════════════════════════════════════════════════════════════ Future>> getAllPlaylists() async { final db = await database; return await db.query('playlists', orderBy: 'created_at DESC'); } Future?> getPlaylist(String id) async { final db = await database; final result = await db.query('playlists', where: 'id = ?', whereArgs: [id]); return result.isNotEmpty ? result.first : null; } Future insertPlaylist(Map playlist) async { final db = await database; await db.insert('playlists', playlist, conflictAlgorithm: ConflictAlgorithm.replace); } Future updatePlaylist(String id, Map updates) async { final db = await database; await db.update('playlists', updates, where: 'id = ?', whereArgs: [id]); } Future deletePlaylist(String id) async { final db = await database; await db.delete('playlists', where: 'id = ?', whereArgs: [id]); } // ════════════════════════════════════════════════════════════ // 播放列表歌曲操作(保持不变) // ════════════════════════════════════════════════════════════ Future>> getPlaylistSongs(String playlistId) async { final db = await database; return await db.query( 'playlist_songs', where: 'playlist_id = ?', whereArgs: [playlistId], orderBy: 'order_index ASC', ); } Future addSongToPlaylist(String playlistId, String songId, {int? orderIndex}) async { 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 removeSongFromPlaylist(String playlistId, String songId) async { final db = await database; await db.delete( 'playlist_songs', where: 'playlist_id = ? AND song_id = ?', whereArgs: [playlistId, songId], ); } Future clearPlaylist(String playlistId) async { final db = await database; await db.delete('playlist_songs', where: 'playlist_id = ?', whereArgs: [playlistId]); } // ════════════════════════════════════════════════════════════ // 收藏操作(保持不变) // ════════════════════════════════════════════════════════════ Future>> getFavorites() async { final db = await database; return await db.query('favorites', orderBy: 'favorited_at DESC'); } Future isFavorite(String songId) async { final db = await database; final result = await db.query('favorites', where: 'song_id = ?', whereArgs: [songId]); return result.isNotEmpty; } Future addFavorite(String songId) async { final db = await database; await db.insert( 'favorites', { 'song_id': songId, 'favorited_at': DateTime.now().millisecondsSinceEpoch, }, conflictAlgorithm: ConflictAlgorithm.replace, ); } Future removeFavorite(String songId) async { final db = await database; await db.delete('favorites', where: 'song_id = ?', whereArgs: [songId]); } Future toggleFavorite(String songId) async { if (await isFavorite(songId)) { await removeFavorite(songId); } else { await addFavorite(songId); } } // ════════════════════════════════════════════════════════════ // 原有写入/删除方法(保持不变) // ════════════════════════════════════════════════════════════ Future insertSong(Map song) async { final db = await database; await db.insert('songs', song, conflictAlgorithm: ConflictAlgorithm.replace); } Future updateSong(String songKey, Map updates) async { final db = await database; await db .update('songs', updates, where: 'song_key = ?', whereArgs: [songKey]); } Future deleteSong(String songKey) async { final db = await database; await db.delete('songs', where: 'song_key = ?', whereArgs: [songKey]); } Future deleteCache(String songKey) async { final db = await database; await db .delete('metadata_cache', where: 'song_key = ?', whereArgs: [songKey]); } Future insertCache(Map cache) async { final db = await database; await db.insert('metadata_cache', cache, conflictAlgorithm: ConflictAlgorithm.replace); } Future?> getCache(String songKey) async { final db = await database; final result = await db .query('metadata_cache', where: 'song_key = ?', whereArgs: [songKey]); return result.isNotEmpty ? result.first : null; } Future close() async { final db = await database; await db.close(); _database = null; } }