505 lines
18 KiB
Dart
505 lines
18 KiB
Dart
// 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: 4, // 升级版本号
|
||
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);
|
||
}
|
||
}
|
||
_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;
|
||
}
|
||
}
|