Files
QingTing-Player/lib/database/song_database.dart

505 lines
18 KiB
Dart
Raw Permalink 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: 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;
}
}