HarmonyOS音乐播放器开发实战:从零到一打造完整应用

引言
随着HarmonyOS生态的不断发展,越来越多的开发者开始关注这一全新的操作系统。今天,我将分享一个完整的HarmonyOS音乐播放器应用的开发过程,涵盖从基础功能到高级特性的实现,希望能为有志于HarmonyOS开发的朋友们提供一些参考。
项目概述
这个音乐播放器应用支持本地音乐扫描、播放控制、歌词显示、主题切换等核心功能,采用了现代化的架构设计和用户界面,为用户提供流畅的音乐播放体验。
技术架构
核心技术栈
- 开发语言: ArkTS
- 开发框架: HarmonyOS UI Kit
- 开发工具: DevEco Studio
- 音频播放: AVPlayer
- 数据存储: SQLite
架构设计
项目采用MVVM架构模式,将业务逻辑与UI层分离,提高代码的可维护性和可测试性:
plaintext
`UI层 (Index.ets, PlayPage.ets)
↓
ViewModel层 (MusicViewModel.ets)
↓
服务层 (AudioPlayer.ets, Database.ets)
↓
数据层 (SQLite数据库)`
核心功能实现
1. 音乐扫描与管理
音乐扫描是播放器的基础功能。我们实现了多种扫描方式:
typescript
`// 支持从系统媒体库扫描
async scanMediaLibrary(): Promise<void> {
// 使用photoAccessHelper扫描音频文件
}
// 支持本地文件夹扫描
async scanLocalMusic(): Promise<void> {
// 遍历指定目录下的音频文件
}
// 支持手动选择文件
async selectMusicFiles(): Promise<void> {
// 使用AudioViewPicker选择音频文件
}`
2. 播放控制与状态管理
播放控制是音乐播放器的核心。我们使用单例模式的AudioPlayer来管理播放状态:
typescript
`// 播放器单例模式
class AudioPlayer {
private static instance: AudioPlayer;
static getInstance(context: UIAbilityContext): AudioPlayer {
if (!AudioPlayer.instance) {
AudioPlayer.instance = new AudioPlayer(context);
}
return AudioPlayer.instance;
}
// 播放控制方法
async play(music: MusicItem): Promise<void> { /* ... */ }
async pause(): Promise<void> { /* ... */ }
async resume(): Promise<void> { /* ... */ }
async seekTo(position: number): Promise<void> { /* ... */ }
}`
3. 歌词同步与显示
歌词功能是提升用户体验的重要特性。我们实现了LRC格式歌词的解析和同步显示:
typescript
`// LRC歌词解析
function parseLyrics(lyricsText: string): LyricLine[] {
const lines = lyricsText.split('\n');
const lyricLines: LyricLine[] = [];
for (const line of lines) {
const matches = line.match(/\[(\d{2}):(\d{2})\.(\d{2})\](.*)/);
if (matches) {
const minutes = parseInt(matches[1]);
const seconds = parseInt(matches[2]);
const milliseconds = parseInt(matches[3]);
const time = minutes * 60000 + seconds * 1000 + milliseconds * 10;
const text = matches[4].trim();
lyricLines.push({ time, text });
}
}
return lyricLines.sort((a, b) => a.time - b.time);
}`
4. 主题切换与UI适配
为了提供更好的视觉体验,我们实现了暗色/浅色主题切换功能:
typescript
`// 主题状态管理
@StorageLink('isDarkTheme') isDarkTheme: boolean = false;
// 根据主题动态设置颜色
.backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.95)' : '#FFFFFF')
.fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333')`
关键技术点解析
1. 进度条优化
进度条是播放器的重要组件。我们实现了防抖动和动态交互效果:
typescript
`// 进度条组件
Slider({
value: this.isDragging ? this.dragValue : this.currentPosition,
min: 0,
max: this.duration > 0 ? this.duration : 1,
step: 1000,
style: SliderStyle.OutSet
})
.height(this.isProgressHovered ? 22 : 18) // 动态高度
.blockSize({ width: 24, height: 24 }) // 滑块尺寸
.blockColor('#FFFFFF') // 白色滑块
.trackColor('rgba(255,255,255,0.3)') // 半透明白色轨道`
防抖动机制通过分离拖动状态和播放状态实现:
typescript
`@State isDragging: boolean = false;
@State dragValue: number = 0;
// 拖动时使用临时值,避免播放器更新冲突
value: this.isDragging ? this.dragValue : this.currentPosition`
2. 全局状态同步
为实现跨页面状态同步,我们使用AppStorage:
typescript
`// 全局状态管理
@StorageLink('currentMusicId') currentMusicId: number = 0;
@StorageLink('currentMusicTitle') currentMusicTitle: string = '';
@StorageLink('isPlaying') isPlaying: boolean = false;
@StorageLink('currentPosition') currentPosition: number = 0;`
3. 播放状态防抖处理
快速点击播放/暂停按钮容易导致状态混乱,我们实现了防抖机制:
typescript
`private isToggling: boolean = false;
private lastToggleTime: number = 0;
private TOGGLE_DEBOUNCE_MS: number = 300;
async togglePlayPause(): Promise<void> {
const now = Date.now();
if (this.isToggling || (now - this.lastToggleTime < this.TOGGLE_DEBOUNCE_MS)) {
return; // 防抖处理
}
this.isToggling = true;
this.lastToggleTime = now;
try {
// 基于播放器真实状态判断
const realIsPlaying = this.audioPlayer.getIsPlaying();
if (realIsPlaying) {
await this.audioPlayer.pause();
} else {
await this.audioPlayer.resume();
}
} finally {
setTimeout(() => {
this.isToggling = false;
}, this.TOGGLE_DEBOUNCE_MS);
}
}`
bash
MyApplication/
├── AppScope/
│ └── resources/
│ └── base/
│ ├── element/
│ │ └── string.json
│ └── media/
│ └── layered_image.json
├── entry/
│ ├── src/
│ │ └── main/
│ │ └── ets/
│ │ ├── entryability/
│ │ │ └── EntryAbility.ets
│ │ ├── entrybackupability/
│ │ │ └── EntryBackupAbility.ets
│ │ └── pages/
│ │ ├── components/
│ │ │ ├── MusicItem.ets
│ │ │ ├── PlayerBar.ets
│ │ │ └── PlaylistDrawer.ets
│ │ ├── models/
│ │ │ └── MusicItem.ets
│ │ ├── services/
│ │ │ ├── AudioPlayer.ets
│ │ │ └── Database.ets
│ │ ├── utils/
│ │ │ ├── FileUtils.ets
│ │ │ └── MusicService.ets
│ │ ├── viewmodels/
│ │ │ └── MusicViewModel.ets
│ │ ├── Index.ets
│ │ └── PlayPage.ets
│ ├── build-profile.json5
│ ├── module.json5
│ └── resources/
│ └── base/
│ ├── element/
│ │ ├── color.json
│ │ ├── float.json
│ │ └── string.json
│ └── profile/
│ └── main_pages.json
├── hvigor/
│ └── hvigor-config.json5
├── .gitignore
├── build-profile.json5
├── code-linter.json5
├── hvigorfile.ts
├── obfuscation-rules.txt
└── oh-package.json5
HarmonyOS音乐播放器项目文件结构及作用说明
项目概览
这是一个基于HarmonyOS NEXT和ArkTS开发的完整音乐播放器应用,采用模块化架构设计,具有良好的可维护性和扩展性。
详细文件作用说明
1. AppScope/ - 应用全局资源
resources/base/element/string.json: 存储应用的全局字符串资源,如应用名称、描述等
resources/base/media/layered_image.json: 定义应用的分层图像资源
2. entry/ - 应用主模块
2.1 src/main/ets/ - 源代码目录
entryability/EntryAbility.ets: 应用入口文件,定义应用生命周期和初始化逻辑
entrybackupability/EntryBackupAbility.ets: 应用备份能力实现
2.2 pages/ - 页面和组件模块
components/ - UI组件库
MusicItem.ets: 音乐列表项组件,显示单个音乐信息
PlayerBar.ets: 底部播放控制栏组件,包含进度条、播放控制按钮等
PlaylistDrawer.ets: 播放列表抽屉组件,显示当前播放队列
models/ - 数据模型
MusicItem.ets: 定义音乐数据模型,包含音乐的基本属性(标题、艺术家、时长等)
services/ - 核心服务
AudioPlayer.ets: 音频播放服务,封装AVPlayer功能,提供播放、暂停、跳转等控制接口
Database.ets: 数据库服务,使用SQLite管理音乐信息、收藏状态等数据
utils/ - 工具类
FileUtils.ets: 文件操作工具,支持歌词文件读取、多编码格式解析
MusicService.ets: 音乐服务工具,提供音乐扫描、处理等功能
viewmodels/ - 视图模型
MusicViewModel.ets: MVVM架构中的ViewModel层,管理业务逻辑和状态
Index.ets: 应用主页面,显示音乐列表、搜索功能等
PlayPage.ets: 播放页面,包含封面显示、歌词展示、播放控制等功能
2.3 resources/ - 应用资源
base/element/color.json: 定义应用颜色主题
base/element/float.json: 定义应用浮点数值资源
base/element/string.json: 应用字符串资源
profile/main_pages.json: 定义应用页面结构和路由
2.4 build-profile.json5: 模块构建配置文件
2.5 module.json5: 模块配置文件,定义模块的基本信息和能力
3. hvigor/ - 构建工具配置
hvigor-config.json5: hvigor构建工具的全局配置
4. 根目录配置文件
.gitignore: Git版本控制忽略文件配置
build-profile.json5: 项目级构建配置,定义编译SDK版本、构建模式等
code-linter.json5: 代码规范检查配置
hvigorfile.ts: hvigor构建脚本
obfuscation-rules.txt: 代码混淆规则
oh-package.json5: OpenHarmony包管理配置
架构特点
1. 模块化设计
通过清晰的目录结构分离不同功能模块
UI组件、业务逻辑、数据服务相互独立
2. MVVM架构
View层: Index.ets, PlayPage.ets
ViewModel层: MusicViewModel.ets
Model层: Database.ets, AudioPlayer.ets
3. 状态管理
使用AppStorage实现全局状态管理
通过@StorageLink实现组件间状态同步
4. 功能特性
音乐播放控制(播放、暂停、上一曲、下一曲)
播放进度控制(拖动进度条)
歌词同步显示
主题切换(暗色/浅色)
音乐扫描和管理
收藏功能
AVSession集成(通知栏控制)
bash
// components/MusicItem.ets
import { MusicItem as MusicItemModel, formatTime } from '../models/MusicItem';
@Component
export struct MusicListItem {
@Prop music: MusicItemModel;
@Prop isCurrent: boolean = false;
@Prop isPlaying: boolean = false;
private onPlay: () => void = () => {};
private onFavorite: () => void = () => {};
build() {
Row() {
// 专辑封面
Stack() {
Image(this.music.cover)
.width(50)
.height(50)
.borderRadius(8)
.objectFit(ImageFit.Cover)
if (this.isCurrent && this.isPlaying) {
Row()
.width(50)
.height(50)
.borderRadius(8)
.backgroundColor('rgba(0,0,0,0.3)')
Image($r('app.media.ic_playing'))
.width(24)
.height(24)
.fillColor('#FFFFFF')
}
}
.margin({ right: 12 })
// 音乐信息
Column() {
Text(this.music.title)
.fontSize(16)
.fontWeight(this.isCurrent ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.isCurrent ? '#FF5722' : '#333333')
.textAlign(TextAlign.Start)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
Row() {
Text(`${this.music.artist}`)
.fontSize(12)
.fontColor(this.isCurrent ? '#FF8A65' : '#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(` · ${formatTime(this.music.duration)}`)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
// 收藏按钮
Image(this.music.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border'))
.width(24)
.height(24)
.fillColor(this.music.isFavorite ? '#FF5722' : '#CCCCCC')
.margin({ right: 8 })
.onClick(() => {
this.onFavorite();
})
// 播放按钮
Image(this.isCurrent && this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play'))
.width(32)
.height(32)
.fillColor(this.isCurrent ? '#FF5722' : '#666666')
.onClick(() => {
this.onPlay();
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor(this.isCurrent ? '#FFF8E1' : '#FFFFFF')
.onClick(() => {
this.onPlay();
})
}
}
bash
// components/PlaylistDrawer.ets - 播放列表抽屉组件
import { MusicItem } from '../models/MusicItem';
@Component
export struct PlaylistDrawer {
@Link showDrawer: boolean;
@Prop playlist: MusicItem[];
@Prop currentMusicId: number;
onPlayMusic?: (music: MusicItem) => void;
// 从 AppStorage 读取主题状态
@StorageLink('isDarkTheme') isDarkTheme: boolean = false;
build() {
Column() {
// 遮罩层
Column()
.width('100%')
.layoutWeight(1)
.backgroundColor(this.isDarkTheme ? 'rgba(15, 52, 96, 0.85)' : 'rgba(0, 0, 0, 0.5)')
.onClick(() => {
this.showDrawer = false;
})
// 播放列表内容
Column() {
// 标题栏
Row() {
Text('播放列表')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333')
Blank()
Text(`${this.playlist.length}首`)
.fontSize(14)
.fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999')
Image($r('app.media.ic_close'))
.width(24)
.height(24)
.fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666')
.margin({ left: 16 })
.onClick(() => {
this.showDrawer = false;
})
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
// 列表
List({ space: 1 }) {
ForEach(this.playlist, (item: MusicItem, index: number) => {
ListItem() {
Row() {
Text(`${index + 1}`)
.fontSize(14)
.fontColor(this.currentMusicId === item.id ?
(this.isDarkTheme ? '#FF4158' : '#667EEA') :
(this.isDarkTheme ? '#AAAAAA' : '#999999')
)
.width(30)
Column() {
Text(item.title)
.fontSize(15)
.fontColor(this.currentMusicId === item.id ?
(this.isDarkTheme ? '#FF4158' : '#667EEA') :
(this.isDarkTheme ? '#FFFFFF' : '#333333')
)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.artist)
.fontSize(12)
.fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
if (this.currentMusicId === item.id) {
Image($r('app.media.ic_playing'))
.width(20)
.height(20)
.fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
}
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(this.currentMusicId === item.id ?
(this.isDarkTheme ? 'rgba(255, 65, 88, 0.15)' : 'rgba(102, 126, 234, 0.1)') :
'transparent'
)
.onClick(() => {
if (this.onPlayMusic) {
this.onPlayMusic(item);
}
})
}
}, (item: MusicItem) => item.id.toString())
}
.width('100%')
.layoutWeight(1)
.backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.3)' : '#FFFFFF')
.divider({
strokeWidth: 0.5,
color: this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0',
startMargin: 46,
endMargin: 16
})
}
.width('100%')
.height('60%')
.backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.95)' : '#FFFFFF')
.borderRadius({ topLeft: 20, topRight: 20 })
.shadow({
radius: 12,
color: this.isDarkTheme ? '#40000000' : '#20000000',
offsetX: 0,
offsetY: -3
})
}
.width('100%')
.height('100%')
}
}
bash
// models/MusicItem.ets
// 歌词行数据
export interface LyricLine {
time: number; // 毫秒
text: string;
}
// 播放模式枚举
export enum PlayMode {
SEQUENCE = 0, // 顺序播放
LOOP = 1, // 列表循环
SINGLE = 2, // 单曲循环
RANDOM = 3 // 随机播放
}
// 音乐项数据模型
export interface MusicItem {
id: number;
title: string;
artist: string;
album: string;
duration: number;
filePath: string;
cover: ResourceStr;
fileName: string;
fileSize: number;
createTime: number;
playCount: number;
isFavorite: boolean; // 是否收藏
lyrics: string; // 歌词内容(LRC格式)
}
// 播放列表
export interface Playlist {
id: number;
name: string;
cover: ResourceStr;
musicCount: number;
createTime: number;
}
// 播放历史
export interface PlayHistory {
id: number;
musicId: number;
playTime: number;
position: number;
}
// 解析LRC歌词
export function parseLyrics(lrcContent: string): LyricLine[] {
const lines = lrcContent.split('\n');
const result: LyricLine[] = [];
for (const line of lines) {
// 匹配 [mm:ss.xx] 或 [mm:ss] 格式
const match = line.match(/\[(\d{2}):(\d{2})([.:]\d{2,3})?\](.*)/);
if (match) {
const minutes = parseInt(match[1]);
const seconds = parseInt(match[2]);
const milliseconds = match[3] ? parseInt(match[3].slice(1)) : 0;
const text = match[4].trim();
if (text) {
const time = minutes * 60 * 1000 + seconds * 1000 + milliseconds;
result.push({ time, text });
}
}
}
// 按时间排序
result.sort((a, b) => a.time - b.time);
return result;
}
// 格式化时间为 mm:ss
export function formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
bash
// services/AudioPlayer.ets
import { media } from '@kit.MediaKit';
import { audio } from '@kit.AudioKit';
import { avSession } from '@kit.AVSessionKit';
import { common } from '@kit.AbilityKit';
import { image } from '@kit.ImageKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { MusicItem, PlayMode } from '../models/MusicItem';
import { BusinessError } from '@kit.BasicServicesKit';
const TAG = 'AudioPlayer';
const DOMAIN = 0x0001;
// 定义播放器状态接口
interface PlayerState {
isPlaying: boolean;
position: number;
duration: number;
}
// 单例实例
let audioPlayerInstance: AudioPlayer | null = null;
export class AudioPlayer {
private avPlayer: media.AVPlayer | null = null;
private session: avSession.AVSession | null = null;
private audioManager: audio.AudioManager | null = null;
private context: common.UIAbilityContext;
private currentMusic: MusicItem | null = null;
private isPlaying: boolean = false;
private currentPosition: number = 0;
private playMode: PlayMode = PlayMode.LOOP;
private playlist: MusicItem[] = [];
private currentIndex: number = 0;
private isInitialized: boolean = false;
private currentFile: fs.File | null = null; // 保存当前文件句柄
private lastProgressUpdate: number = 0; // 上次更新进度的时间戳
private lastMetadataUpdate: string = ''; // 上次更新元数据的音乐ID(避免重复更新)
private lastPlaybackStateUpdate: string = ''; // 上次更新的播放状态(避免重复更新)
// 播放状态变化回调
private onStateChange: (state: string) => void = () => {
};
private onPositionChange: (position: number) => void = () => {
};
private onMusicChange: (music: MusicItem) => void = () => {
};
private onPlayModeChange: (mode: PlayMode) => void = () => {
};
private onDurationChange: (duration: number) => void = () => {
};
private onFavoriteChange: (musicId: number, isFavorite: boolean) => void = () => {
};
constructor(context: common.UIAbilityContext) {
this.context = context;
}
// 获取单例实例
static getInstance(context: common.UIAbilityContext): AudioPlayer {
if (!audioPlayerInstance) {
hilog.info(DOMAIN, TAG, '创建新的单例实例');
audioPlayerInstance = new AudioPlayer(context);
}
return audioPlayerInstance;
}
// 初始化播放器
async init(): Promise<void> {
// 如果已经初始化,直接返回
if (this.isInitialized && this.avPlayer) {
hilog.info(DOMAIN, TAG, '播放器已初始化,跳过');
return;
}
hilog.info(DOMAIN, TAG, '===== 初始化播放器 =====');
try {
// 初始化音频管理器
hilog.info(DOMAIN, TAG, '初始化音频管理器...');
this.audioManager = audio.getAudioManager();
hilog.info(DOMAIN, TAG, '音频管理器创建成功');
hilog.info(DOMAIN, TAG, '调用 media.createAVPlayer()...');
this.avPlayer = await media.createAVPlayer();
hilog.info(DOMAIN, TAG, 'AVPlayer 创建成功');
// 设置音频属性(关键:确保音频输出到蓝牙设备)
this.avPlayer.audioInterruptMode = audio.InterruptMode.SHARE_MODE;
hilog.info(DOMAIN, TAG, '音频中断模式已设置: SHARE_MODE');
this.setEventCallBack();
// 设置默认音量为 1.0(100%)
await this.avPlayer.setVolume(1.0);
hilog.info(DOMAIN, TAG, '默认音量已设置: 100%');
// 初始化 AVSession 媒体会话
await this.initAVSession();
this.isInitialized = true;
hilog.info(DOMAIN, TAG, '✅ 播放器初始化成功');
} catch (error) {
hilog.error(DOMAIN, TAG, '❌ 创建播放器失败: %{public}s', JSON.stringify(error));
throw new Error(`创建播放器失败: ${JSON.stringify(error)}`);
}
}
// 初始化 AVSession 媒体会话(通知栏控制)
private async initAVSession(): Promise<void> {
console.info('[AudioPlayer] ===== 初始化 AVSession =====');
try {
// 创建媒体会话
this.session = await avSession.createAVSession(this.context, 'MusicPlayer', 'audio');
console.info('[AudioPlayer] AVSession 创建成功');
// 设置播放命令回调
this.session.on('play', () => {
console.info('[AudioPlayer] AVSession: 收到播放命令');
this.resume();
});
this.session.on('pause', () => {
console.info('[AudioPlayer] AVSession: 收到暂停命令');
this.pause();
});
this.session.on('playNext', () => {
console.info('[AudioPlayer] AVSession: 收到下一曲命令');
this.playNext();
});
this.session.on('playPrevious', () => {
console.info('[AudioPlayer] AVSession: 收到上一曲命令');
this.playPrevious();
});
this.session.on('seek', (time: number) => {
console.info(`[AudioPlayer] AVSession: 收到跳转命令 time=${time}`);
this.seekTo(time);
});
this.session.on('stop', () => {
console.info('[AudioPlayer] AVSession: 收到停止命令');
this.stop();
});
// 激活会话
await this.session.activate();
console.info('[AudioPlayer] ✅ AVSession 初始化成功');
} catch (error) {
const err = error as BusinessError;
console.error(`[AudioPlayer] ❌ AVSession 初始化失败: code=${err.code}, message=${err.message}`);
// AVSession 失败不影响基本播放功能
}
}
// 更新 AVSession 媒体信息
private async updateSessionMetadata(): Promise<void> {
if (!this.session || !this.currentMusic) {
return;
}
try {
// 避免重复更新相同音乐的元数据
const musicKey = `${this.currentMusic.id}_${this.currentMusic.title}`;
if (this.lastMetadataUpdate === musicKey) {
hilog.info(DOMAIN, TAG, 'AVSession 元数据未变化,跳过更新');
return;
}
const metadata: avSession.AVMetadata = {
assetId: String(this.currentMusic.id),
title: this.currentMusic.title || '未知歌曲',
artist: this.currentMusic.artist || '未知歌手',
album: this.currentMusic.album || '未知专辑',
duration: this.currentMusic.duration || this.avPlayer?.duration || 0
};
// 注意:AVSession 的 mediaImage 只支持网络 URL,不支持本地文件路径
// 本地封面路径不设置,避免 AVSession 警告
// 如需显示封面,可将封面上传到服务器或使用 PixelMap
await this.session.setAVMetadata(metadata);
this.lastMetadataUpdate = musicKey;
hilog.info(DOMAIN, TAG, 'AVSession 元数据已更新: %{public}s - %{public}s',
metadata.title, metadata.artist);
} catch (error) {
hilog.error(DOMAIN, TAG, '更新 AVSession 元数据失败: %{public}s', JSON.stringify(error));
}
}
// 更新 AVSession 播放状态
private async updateSessionPlaybackState(state: string): Promise<void> {
if (!this.session) {
return;
}
try {
// 避免重复更新相同状态
if (this.lastPlaybackStateUpdate === state) {
return;
}
let playbackState: avSession.PlaybackState;
switch (state) {
case 'playing':
playbackState = avSession.PlaybackState.PLAYBACK_STATE_PLAY;
break;
case 'paused':
playbackState = avSession.PlaybackState.PLAYBACK_STATE_PAUSE;
break;
case 'completed':
case 'stopped':
case 'idle':
playbackState = avSession.PlaybackState.PLAYBACK_STATE_STOP;
break;
default:
// 其他状态不更新
return;
}
const avPlaybackState: avSession.AVPlaybackState = {
state: playbackState,
position: {
elapsedTime: this.currentPosition,
updateTime: Date.now()
},
speed: 1.0,
isFavorite: this.currentMusic?.isFavorite || false
};
await this.session.setAVPlaybackState(avPlaybackState);
this.lastPlaybackStateUpdate = state;
hilog.info(DOMAIN, TAG, 'AVSession 播放状态已更新: %{public}s', state);
} catch (error) {
hilog.error(DOMAIN, TAG, '更新 AVSession 播放状态失败: %{public}s', JSON.stringify(error));
}
}
// 检查是否已初始化
isReady(): boolean {
return this.isInitialized && this.avPlayer !== null;
}
// 获取当前播放状态
getIsPlaying(): boolean {
return this.isPlaying;
}
// 设置事件回调
private setEventCallBack(): void {
if (!this.avPlayer) {
hilog.error(DOMAIN, TAG, 'setEventCallBack: avPlayer 为 null');
return;
}
hilog.info(DOMAIN, TAG, '设置事件回调...');
this.avPlayer.on('stateChange', (state: string) => {
hilog.info(DOMAIN, TAG, '>>>>>>> 状态变化: %{public}s <<<<<<<', state);
switch (state) {
case 'idle':
hilog.info(DOMAIN, TAG, ' -> 播放器空闲 (idle)');
this.isPlaying = false;
this.updateGlobalState();
break;
case 'initialized':
hilog.info(DOMAIN, TAG, ' -> 已初始化 (initialized),准备调用 prepare()...');
if (this.avPlayer) {
this.avPlayer.prepare().then(() => {
hilog.info(DOMAIN, TAG, ' -> prepare() 调用成功');
}).catch((err: BusinessError) => {
hilog.error(DOMAIN, TAG, ' -> prepare() 失败: code=%{public}d, msg=%{public}s', err.code, err.message);
});
}
break;
case 'prepared':
hilog.info(DOMAIN, TAG, ' -> 已准备 (prepared),准备调用 play()...');
// 获取实际时长
if (this.avPlayer && this.avPlayer.duration > 0) {
hilog.info(DOMAIN, TAG, ' -> 音乐时长: %{public}d ms', this.avPlayer.duration);
this.onDurationChange(this.avPlayer.duration);
}
if (this.avPlayer) {
this.avPlayer.play().then(() => {
hilog.info(DOMAIN, TAG, ' -> play() 调用成功');
}).catch((err: BusinessError) => {
hilog.error(DOMAIN, TAG, ' -> play() 失败: code=%{public}d, msg=%{public}s', err.code, err.message);
});
}
break;
case 'playing':
hilog.info(DOMAIN, TAG, ' -> ✅ 正在播放 (playing)');
this.isPlaying = true;
this.updateGlobalState();
break;
case 'paused':
hilog.info(DOMAIN, TAG, ' -> 已暂停 (paused)');
this.isPlaying = false;
this.updateGlobalState();
break;
case 'completed':
hilog.info(DOMAIN, TAG, ' -> 播放完成 (completed)');
this.isPlaying = false;
this.currentPosition = 0;
this.updateGlobalState();
this.handlePlayComplete();
break;
case 'stopped':
hilog.info(DOMAIN, TAG, ' -> 已停止 (stopped)');
this.isPlaying = false;
this.updateGlobalState();
break;
case 'released':
hilog.info(DOMAIN, TAG, ' -> 已释放 (released)');
this.isPlaying = false;
this.updateGlobalState();
break;
case 'error':
hilog.error(DOMAIN, TAG, ' -> ❌ 错误状态 (error)');
this.isPlaying = false;
this.updateGlobalState();
break;
default:
hilog.info(DOMAIN, TAG, ' -> 未知状态: %{public}s', state);
break;
}
this.onStateChange(state);
// 更新 AVSession 播放状态
this.updateSessionPlaybackState(state);
});
// 时间更新回调
this.avPlayer.on('timeUpdate', (time: number) => {
this.currentPosition = time;
this.onPositionChange(time);
// 更新全局进度(节流:每秒更新一次)
const now = Date.now();
if (!this.lastProgressUpdate || now - this.lastProgressUpdate > 1000) {
AppStorage.setOrCreate('currentPosition', time);
this.lastProgressUpdate = now;
}
});
// 错误回调 - 详细输出
this.avPlayer.on('error', (err: BusinessError) => {
hilog.error(DOMAIN, TAG, '>>>>>>> 播放错误 <<<<<<<');
hilog.error(DOMAIN, TAG, ' code: %{public}d', err.code);
hilog.error(DOMAIN, TAG, ' message: %{public}s', err.message);
hilog.error(DOMAIN, TAG, ' name: %{public}s', err.name);
hilog.error(DOMAIN, TAG, ' 当前音乐: %{public}s', this.currentMusic?.title || '无');
hilog.error(DOMAIN, TAG, ' 文件路径: %{public}s', this.currentMusic?.filePath || '无');
});
hilog.info(DOMAIN, TAG, '事件回调设置完成');
}
// 更新全局状态到 AppStorage
private updateGlobalState(): void {
AppStorage.setOrCreate('isPlaying', this.isPlaying);
if (this.currentMusic) {
AppStorage.setOrCreate('currentMusicId', this.currentMusic.id);
AppStorage.setOrCreate('currentMusicTitle', this.currentMusic.title);
AppStorage.setOrCreate('currentMusicArtist', this.currentMusic.artist);
AppStorage.setOrCreate('currentMusicCover', this.currentMusic.cover);
AppStorage.setOrCreate('currentMusicLyrics', this.currentMusic.lyrics || ''); // 添加歌词同步
}
AppStorage.setOrCreate('currentPosition', this.currentPosition);
AppStorage.setOrCreate('playMode', this.playMode);
}
// 处理播放完成
private handlePlayComplete(): void {
switch (this.playMode) {
case PlayMode.SINGLE:
// 单曲循环
if (this.currentMusic) {
this.play(this.currentMusic);
}
break;
case PlayMode.LOOP:
// 列表循环
this.playNext();
break;
case PlayMode.RANDOM:
// 随机播放
this.playRandom();
break;
case PlayMode.SEQUENCE:
// 顺序播放
if (this.currentIndex < this.playlist.length - 1) {
this.playNext();
}
break;
default:
break;
}
}
// 设置播放列表
setPlaylist(list: MusicItem[]): void {
this.playlist = list;
}
// 获取播放列表
getPlaylist(): MusicItem[] {
return this.playlist;
}
// 播放音乐
async play(music: MusicItem): Promise<void> {
hilog.info(DOMAIN, TAG, '======================================');
hilog.info(DOMAIN, TAG, '===== 开始播放流程 =====');
hilog.info(DOMAIN, TAG, '======================================');
hilog.info(DOMAIN, TAG, '[1] 音乐信息:');
hilog.info(DOMAIN, TAG, ' - 标题: %{public}s', music.title);
hilog.info(DOMAIN, TAG, ' - 歌手: %{public}s', music.artist);
hilog.info(DOMAIN, TAG, ' - 原始URI: %{public}s', music.filePath);
hilog.info(DOMAIN, TAG, ' - ID: %{public}d', music.id);
hilog.info(DOMAIN, TAG, '[2] 检查播放器状态:');
hilog.info(DOMAIN, TAG, ' - avPlayer 存在: %{public}s', String(this.avPlayer !== null));
hilog.info(DOMAIN, TAG, ' - isInitialized: %{public}s', String(this.isInitialized));
if (!this.avPlayer) {
hilog.info(DOMAIN, TAG, '[3] 播放器未初始化,开始初始化...');
await this.init();
}
if (!this.avPlayer) {
hilog.error(DOMAIN, TAG, '❌ 播放器初始化失败,无法播放');
throw new Error('播放器初始化失败');
}
try {
this.currentMusic = music;
hilog.info(DOMAIN, TAG, '[4] 已设置 currentMusic');
// 更新当前索引
const index = this.playlist.findIndex(m => m.id === music.id);
if (index !== -1) {
this.currentIndex = index;
hilog.info(DOMAIN, TAG, '[5] 当前索引: %{public}d / %{public}d', index, this.playlist.length);
} else {
hilog.info(DOMAIN, TAG, '[5] 音乐不在播放列表中');
}
const currentState = this.avPlayer.state;
hilog.info(DOMAIN, TAG, '[6] 当前播放器状态: %{public}s', currentState);
// 在设置新url前,必须将播放器重置到 idle 状态
if (currentState !== 'idle') {
hilog.info(DOMAIN, TAG, '[7] 重置播放器到 idle 状态...');
try {
await this.avPlayer.reset();
hilog.info(DOMAIN, TAG, '[7] 重置成功,当前状态: %{public}s', this.avPlayer.state);
} catch (resetError) {
const err = resetError as BusinessError;
hilog.error(DOMAIN, TAG, '[7] 重置失败: code=%{public}d, msg=%{public}s', err.code, err.message);
hilog.info(DOMAIN, TAG, '[7] 尝试重新创建播放器...');
await this.avPlayer.release();
this.avPlayer = await media.createAVPlayer();
this.setEventCallBack();
hilog.info(DOMAIN, TAG, '[7] 播放器重新创建成功');
}
} else {
hilog.info(DOMAIN, TAG, '[7] 播放器已在 idle 状态,无需重置');
}
// 将 URI 转换为 fd://
hilog.info(DOMAIN, TAG, '[8] 转换 URI 为 fd://...');
const fdUrl = await this.convertUriToFd(music.filePath);
hilog.info(DOMAIN, TAG, '[8] 转换成功: %{public}s', fdUrl);
// 设置播放源 - 使用 fd:// 格式
hilog.info(DOMAIN, TAG, '[9] 设置播放源 URL...');
this.avPlayer.url = fdUrl;
hilog.info(DOMAIN, TAG, '[10] 播放源已设置,等待 stateChange -> initialized -> prepared -> playing');
this.onMusicChange(music);
hilog.info(DOMAIN, TAG, '[11] 已通知 onMusicChange');
// 更新全局状态
this.updateGlobalState();
hilog.info(DOMAIN, TAG, '[12] 全局状态已更新');
// 更新 AVSession 媒体信息
await this.updateSessionMetadata();
hilog.info(DOMAIN, TAG, '[13] AVSession 元数据已更新');
hilog.info(DOMAIN, TAG, '===== 播放流程设置完成,等待回调 =====');
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG, '======================================');
hilog.error(DOMAIN, TAG, '❌ 播放失败!');
hilog.error(DOMAIN, TAG, ' code: %{public}d', err.code);
hilog.error(DOMAIN, TAG, ' message: %{public}s', err.message);
hilog.error(DOMAIN, TAG, ' 完整错误: %{public}s', JSON.stringify(error));
hilog.error(DOMAIN, TAG, '======================================');
throw new Error(`播放失败: ${err.message}`);
}
}
// 将 URI 转换为 fd:// 格式
private async convertUriToFd(uri: string): Promise<string> {
try {
hilog.info(DOMAIN, TAG, ' 正在打开文件: %{public}s', uri);
// 关闭之前的文件句柄(如果有)
if (this.currentFile !== null) {
try {
hilog.info(DOMAIN, TAG, ' 关闭之前的文件句柄: %{public}d', this.currentFile.fd);
fs.closeSync(this.currentFile);
} catch (closeError) {
hilog.warn(DOMAIN, TAG, ' 关闭文件失败: %{public}s', JSON.stringify(closeError));
}
this.currentFile = null;
}
// 打开新文件并保存句柄
this.currentFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
const fd = this.currentFile.fd;
hilog.info(DOMAIN, TAG, ' 文件描述符: %{public}d', fd);
hilog.info(DOMAIN, TAG, ' 文件句柄已保存,将在播放期间保持打开');
// 返回 fd:// 格式
return `fd://${fd}`;
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG, ' ❌ 打开文件失败: code=%{public}d, msg=%{public}s', err.code, err.message);
throw new Error(`打开文件失败: ${err.message}`);
}
}
// 暂停播放
async pause(): Promise<void> {
console.info('[AudioPlayer] 暂停播放...');
console.info(`[AudioPlayer] 当前状态: isPlaying=${this.isPlaying}, avPlayer.state=${this.avPlayer?.state}`);
try {
if (this.avPlayer && this.avPlayer.state === 'playing') {
await this.avPlayer.pause();
this.isPlaying = false;
console.info('[AudioPlayer] ✅ 暂停成功');
} else {
console.warn('[AudioPlayer] 无法暂停: 播放器不在播放状态');
}
} catch (error) {
console.error(`[AudioPlayer] ❌ 暂停失败: ${JSON.stringify(error)}`);
}
}
// 恢复播放
async resume(): Promise<void> {
console.info('[AudioPlayer] 恢复播放...');
console.info(`[AudioPlayer] 当前状态: isPlaying=${this.isPlaying}, avPlayer.state=${this.avPlayer?.state}`);
try {
if (this.avPlayer) {
const state = this.avPlayer.state;
if (state === 'paused') {
// 从暂停状态恢复
await this.avPlayer.play();
this.isPlaying = true;
console.info('[AudioPlayer] ✅ 恢复播放成功');
} else if (state === 'prepared') {
// 如果已准备但未播放,开始播放
await this.avPlayer.play();
this.isPlaying = true;
console.info('[AudioPlayer] ✅ 开始播放(从 prepared 状态)');
} else {
// 其他状态无法恢复,需要重新播放
console.warn(`[AudioPlayer] 无法恢复: 播放器状态=${state}`);
if (this.currentMusic) {
console.info('[AudioPlayer] 重新播放当前音乐');
await this.play(this.currentMusic);
}
}
} else {
console.warn('[AudioPlayer] 无法恢复: 播放器未初始化');
}
} catch (error) {
console.error(`[AudioPlayer] ❌ 恢复播放失败: ${JSON.stringify(error)}`);
// 尝试重新播放
if (this.currentMusic) {
console.info('[AudioPlayer] 尝试重新播放');
await this.play(this.currentMusic);
}
}
}
// 停止播放
async stop(): Promise<void> {
console.info('[AudioPlayer] 停止播放...');
console.info(`[AudioPlayer] 当前状态: avPlayer.state=${this.avPlayer?.state}`);
try {
if (this.avPlayer) {
const state = this.avPlayer.state;
// 只有在特定状态下才能调用 stop()
if (state === 'playing' || state === 'paused' || state === 'prepared') {
console.info('[AudioPlayer] 调用 avPlayer.stop()...');
await this.avPlayer.stop();
}
// reset 可以在大多数状态下调用
if (state !== 'idle' && state !== 'released') {
console.info('[AudioPlayer] 调用 avPlayer.reset()...');
await this.avPlayer.reset();
}
}
// 关闭文件句柄
if (this.currentFile !== null) {
try {
console.info('[AudioPlayer] 关闭文件句柄: ' + this.currentFile.fd);
fs.closeSync(this.currentFile);
this.currentFile = null;
} catch (closeError) {
console.warn('[AudioPlayer] 关闭文件失败: ' + JSON.stringify(closeError));
}
}
this.isPlaying = false;
this.currentPosition = 0;
console.info('[AudioPlayer] ✅ 停止成功');
} catch (error) {
console.error(`[AudioPlayer] ❌ 停止播放失败: ${JSON.stringify(error)}`);
}
}
// 跳转到指定位置
async seekTo(position: number): Promise<void> {
try {
if (this.avPlayer) {
const state = this.avPlayer.state;
// 只在允许的状态下执行 seek 操作
const allowedStates = ['prepared', 'playing', 'paused', 'completed'];
if (allowedStates.includes(state)) {
await this.avPlayer.seek(position);
this.currentPosition = position;
hilog.info(DOMAIN, TAG, 'Seek 成功: position=%{public}d, state=%{public}s', position, state);
} else {
hilog.warn(DOMAIN, TAG, 'Seek 被忽略: 当前状态 %{public}s 不支持 seek 操作', state);
}
}
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG, 'Seek 失败: code=%{public}d, msg=%{public}s', err.code, err.message);
}
}
// 设置音量
async setVolume(volume: number): Promise<void> {
try {
if (this.avPlayer) {
await this.avPlayer.setVolume(volume);
}
} catch (error) {
console.error(`设置音量失败: ${JSON.stringify(error)}`);
}
}
// 播放下一首
async playNext(): Promise<void> {
if (this.playlist.length === 0) {
return;
}
if (this.playMode === PlayMode.RANDOM) {
await this.playRandom();
} else {
this.currentIndex = (this.currentIndex + 1) % this.playlist.length;
const nextMusic = this.playlist[this.currentIndex];
if (nextMusic) {
await this.play(nextMusic);
}
}
}
// 播放上一首
async playPrevious(): Promise<void> {
if (this.playlist.length === 0) {
return;
}
if (this.playMode === PlayMode.RANDOM) {
await this.playRandom();
} else {
this.currentIndex = (this.currentIndex - 1 + this.playlist.length) % this.playlist.length;
const prevMusic = this.playlist[this.currentIndex];
if (prevMusic) {
await this.play(prevMusic);
}
}
}
// 随机播放
async playRandom(): Promise<void> {
if (this.playlist.length === 0) {
return;
}
let randomIndex = Math.floor(Math.random() * this.playlist.length);
// 避免播放同一首
if (this.playlist.length > 1 && randomIndex === this.currentIndex) {
randomIndex = (randomIndex + 1) % this.playlist.length;
}
this.currentIndex = randomIndex;
const randomMusic = this.playlist[this.currentIndex];
if (randomMusic) {
await this.play(randomMusic);
}
}
// 切换播放模式
togglePlayMode(): PlayMode {
const modes = [PlayMode.SEQUENCE, PlayMode.LOOP, PlayMode.SINGLE, PlayMode.RANDOM];
const currentModeIndex = modes.indexOf(this.playMode);
this.playMode = modes[(currentModeIndex + 1) % modes.length];
this.onPlayModeChange(this.playMode);
return this.playMode;
}
// 设置播放模式
setPlayMode(mode: PlayMode): void {
this.playMode = mode;
this.onPlayModeChange(mode);
}
// 获取当前播放模式
getPlayMode(): PlayMode {
return this.playMode;
}
// 获取当前状态
getCurrentState(): PlayerState {
return {
isPlaying: this.isPlaying,
position: this.currentPosition,
duration: this.currentMusic?.duration || 0
};
}
// 获取当前播放的音乐
getCurrentMusic(): MusicItem | null {
return this.currentMusic;
}
// 获取当前索引
getCurrentIndex(): number {
return this.currentIndex;
}
// 设置状态变化监听
setOnStateChangeListener(callback: (state: string) => void): void {
this.onStateChange = callback;
}
// 设置位置变化监听
setOnPositionChangeListener(callback: (position: number) => void): void {
this.onPositionChange = callback;
}
// 设置音乐变化监听
setOnMusicChangeListener(callback: (music: MusicItem) => void): void {
this.onMusicChange = callback;
}
// 设置播放模式变化监听
setOnPlayModeChangeListener(callback: (mode: PlayMode) => void): void {
this.onPlayModeChange = callback;
}
// 设置时长变化监听
setOnDurationChangeListener(callback: (duration: number) => void): void {
this.onDurationChange = callback;
}
// 设置收藏变化监听
setOnFavoriteChangeListener(callback: (musicId: number, isFavorite: boolean) => void): void {
this.onFavoriteChange = callback;
}
// 通知收藏状态变化(跨页面同步)
notifyFavoriteChanged(musicId: number, isFavorite: boolean): void {
console.info(`[AudioPlayer] 收藏状态变化: musicId=${musicId}, isFavorite=${isFavorite}`);
// 更新当前音乐的收藏状态
if (this.currentMusic && this.currentMusic.id === musicId) {
this.currentMusic.isFavorite = isFavorite;
}
// 更新播放列表中的收藏状态
const index = this.playlist.findIndex(m => m.id === musicId);
if (index !== -1) {
this.playlist[index].isFavorite = isFavorite;
}
// 通知监听器
this.onFavoriteChange(musicId, isFavorite);
}
// 获取当前位置
getCurrentPosition(): number {
return this.currentPosition;
}
// 获取当前播放器状态
getPlayerState(): string {
return this.avPlayer?.state || 'idle';
}
// 获取实际时长
getDuration(): number {
return this.avPlayer?.duration || 0;
}
// 释放资源
async release(): Promise<void> {
try {
await this.stop();
if (this.avPlayer) {
await this.avPlayer.release();
this.avPlayer = null;
}
// 关闭文件句柄
if (this.currentFile !== null) {
try {
console.info('[AudioPlayer] 释放时关闭文件句柄: ' + this.currentFile.fd);
fs.closeSync(this.currentFile);
this.currentFile = null;
} catch (closeError) {
console.warn('[AudioPlayer] 关闭文件失败: ' + JSON.stringify(closeError));
}
}
} catch (error) {
console.error(`释放资源失败: ${JSON.stringify(error)}`);
}
}
}
bash
// services/Database.ets
import { relationalStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { MusicItem, Playlist, PlayHistory } from '../models/MusicItem';
export class MusicDatabase {
private rdbStore: relationalStore.RdbStore | null = null;
private context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext) {
this.context = context;
}
// 初始化数据库
async initDatabase(): Promise<void> {
console.info('[Database] ===== 开始初始化数据库 =====');
const config: relationalStore.StoreConfig = {
name: 'MusicPlayer.db',
securityLevel: relationalStore.SecurityLevel.S1
};
console.info('[Database] 数据库配置: name=MusicPlayer.db, securityLevel=S1');
try {
console.info('[Database] 调用 getRdbStore...');
this.rdbStore = await relationalStore.getRdbStore(this.context, config);
console.info('[Database] getRdbStore 成功');
console.info('[Database] 创建数据表...');
await this.createTables();
console.info('[Database] ✅ 数据库初始化成功');
} catch (error) {
console.error(`[Database] ❌ 数据库初始化失败: ${error.message}`);
throw new Error(`数据库初始化失败: ${error.message}`);
}
console.info('[Database] ===== 数据库初始化结束 =====');
}
// 创建数据表
private async createTables(): Promise<void> {
// 创建音乐表
const createMusicTable = `
CREATE TABLE IF NOT EXISTS music (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
artist TEXT,
album TEXT,
duration INTEGER,
file_path TEXT UNIQUE NOT NULL,
cover_url TEXT,
file_name TEXT,
file_size INTEGER,
create_time INTEGER,
play_count INTEGER DEFAULT 0,
is_favorite INTEGER DEFAULT 0,
lyrics TEXT
)
`;
// 创建播放列表表
const createPlaylistTable = `
CREATE TABLE IF NOT EXISTS playlist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
cover_url TEXT,
music_count INTEGER DEFAULT 0,
create_time INTEGER
)
`;
// 创建播放历史表
const createPlayHistoryTable = `
CREATE TABLE IF NOT EXISTS play_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
music_id INTEGER,
play_time INTEGER,
position INTEGER,
FOREIGN KEY (music_id) REFERENCES music(id)
)
`;
// 创建播放列表-音乐关联表
const createPlaylistMusicTable = `
CREATE TABLE IF NOT EXISTS playlist_music (
playlist_id INTEGER,
music_id INTEGER,
add_time INTEGER,
PRIMARY KEY (playlist_id, music_id),
FOREIGN KEY (playlist_id) REFERENCES playlist(id),
FOREIGN KEY (music_id) REFERENCES music(id)
)
`;
if (!this.rdbStore) {
return;
}
try {
await this.rdbStore.executeSql(createMusicTable);
await this.rdbStore.executeSql(createPlaylistTable);
await this.rdbStore.executeSql(createPlayHistoryTable);
await this.rdbStore.executeSql(createPlaylistMusicTable);
} catch (error) {
console.error(`创建数据表失败: ${JSON.stringify(error)}`);
}
}
// 添加音乐
async addMusic(music: MusicItem): Promise<number> {
console.info(`[Database] 添加音乐: "${music.title}" - ${music.artist}`);
console.info(`[Database] 文件路径: ${music.filePath}`);
const valueBucket: relationalStore.ValuesBucket = {
'title': music.title,
'artist': music.artist || '未知艺术家',
'album': music.album || '未知专辑',
'duration': music.duration,
'file_path': music.filePath,
'cover_url': '',
'file_name': music.fileName,
'file_size': music.fileSize,
'create_time': Date.now(),
'is_favorite': music.isFavorite ? 1 : 0,
'lyrics': music.lyrics || ''
};
if (!this.rdbStore) {
console.error('[Database] ❌ 数据库未初始化');
throw new Error("数据库未初始化");
}
try {
const insertId = await this.rdbStore.insert('music', valueBucket);
console.info(`[Database] ✅ 添加成功, ID=${insertId}`);
return insertId;
} catch (error) {
console.error(`[Database] ❌ 添加音乐失败: ${JSON.stringify(error)}`);
throw new Error(`添加音乐失败: ${JSON.stringify(error)}`);
}
}
// 获取所有音乐
async getAllMusic(): Promise<MusicItem[]> {
console.info('[Database] 获取所有音乐...');
try {
if (!this.rdbStore) {
console.warn('[Database] 数据库未初始化,返回空列表');
return [];
}
const predicates = new relationalStore.RdbPredicates('music');
predicates.orderByDesc('create_time');
const result = await this.rdbStore.query(predicates, [
'id', 'title', 'artist', 'album', 'duration',
'file_path', 'cover_url', 'file_name', 'file_size',
'create_time', 'play_count', 'is_favorite', 'lyrics'
]);
const musicList: MusicItem[] = [];
while (result.goToNextRow()) {
musicList.push({
id: result.getLong(result.getColumnIndex('id')),
title: result.getString(result.getColumnIndex('title')),
artist: result.getString(result.getColumnIndex('artist')),
album: result.getString(result.getColumnIndex('album')),
duration: result.getLong(result.getColumnIndex('duration')),
filePath: result.getString(result.getColumnIndex('file_path')),
cover: result.getString(result.getColumnIndex('cover_url')) || $r('app.media.ic_album'),
fileName: result.getString(result.getColumnIndex('file_name')),
fileSize: result.getLong(result.getColumnIndex('file_size')),
createTime: result.getLong(result.getColumnIndex('create_time')),
playCount: result.getLong(result.getColumnIndex('play_count')),
isFavorite: result.getLong(result.getColumnIndex('is_favorite')) === 1,
lyrics: result.getString(result.getColumnIndex('lyrics')) || ''
});
}
result.close();
console.info(`[Database] 查询完成,共 ${musicList.length} 首音乐`);
return musicList;
} catch (error) {
console.error(`[Database] ❌ 获取音乐列表失败: ${JSON.stringify(error)}`);
return [];
}
}
// 根据 ID 获取单首音乐(包含最新歌词)
async getMusicById(musicId: number): Promise<MusicItem | null> {
console.info(`[Database] 获取音乐: id=${musicId}`);
try {
if (!this.rdbStore) {
console.warn('[Database] 数据库未初始化');
return null;
}
const predicates = new relationalStore.RdbPredicates('music');
predicates.equalTo('id', musicId);
const result = await this.rdbStore.query(predicates, [
'id', 'title', 'artist', 'album', 'duration',
'file_path', 'cover_url', 'file_name', 'file_size',
'create_time', 'play_count', 'is_favorite', 'lyrics'
]);
if (result.goToNextRow()) {
const music: MusicItem = {
id: result.getLong(result.getColumnIndex('id')),
title: result.getString(result.getColumnIndex('title')),
artist: result.getString(result.getColumnIndex('artist')),
album: result.getString(result.getColumnIndex('album')),
duration: result.getLong(result.getColumnIndex('duration')),
filePath: result.getString(result.getColumnIndex('file_path')),
cover: result.getString(result.getColumnIndex('cover_url')) || $r('app.media.ic_album'),
fileName: result.getString(result.getColumnIndex('file_name')),
fileSize: result.getLong(result.getColumnIndex('file_size')),
createTime: result.getLong(result.getColumnIndex('create_time')),
playCount: result.getLong(result.getColumnIndex('play_count')),
isFavorite: result.getLong(result.getColumnIndex('is_favorite')) === 1,
lyrics: result.getString(result.getColumnIndex('lyrics')) || ''
};
result.close();
console.info(`[Database] 找到音乐: ${music.title}, 歌词长度: ${music.lyrics.length}`);
return music;
}
result.close();
console.warn(`[Database] 未找到音乐: id=${musicId}`);
return null;
} catch (error) {
console.error(`[Database] ❌ 获取音乐失败: ${JSON.stringify(error)}`);
return null;
}
}
// 搜索音乐
async searchMusic(keyword: string): Promise<MusicItem[]> {
try {
if (!this.rdbStore) {
return [];
}
const predicates = new relationalStore.RdbPredicates('music');
predicates.contains('title', keyword).or().contains('artist', keyword).or().contains('album', keyword);
const result = await this.rdbStore.query(predicates, [
'id', 'title', 'artist', 'album', 'duration',
'file_path', 'cover_url', 'file_name', 'file_size',
'create_time', 'play_count', 'is_favorite', 'lyrics'
]);
const musicList: MusicItem[] = [];
while (result.goToNextRow()) {
musicList.push({
id: result.getLong(result.getColumnIndex('id')),
title: result.getString(result.getColumnIndex('title')),
artist: result.getString(result.getColumnIndex('artist')),
album: result.getString(result.getColumnIndex('album')),
duration: result.getLong(result.getColumnIndex('duration')),
filePath: result.getString(result.getColumnIndex('file_path')),
cover: result.getString(result.getColumnIndex('cover_url')) || $r('app.media.ic_album'),
fileName: result.getString(result.getColumnIndex('file_name')),
fileSize: result.getLong(result.getColumnIndex('file_size')),
createTime: result.getLong(result.getColumnIndex('create_time')),
playCount: result.getLong(result.getColumnIndex('play_count')),
isFavorite: result.getLong(result.getColumnIndex('is_favorite')) === 1,
lyrics: result.getString(result.getColumnIndex('lyrics')) || ''
});
}
result.close();
return musicList;
} catch (error) {
console.error(`搜索音乐失败: ${JSON.stringify(error)}`);
return [];
}
}
// 切换收藏状态
async toggleFavorite(musicId: number): Promise<boolean> {
try {
// 先获取当前状态
const predicates = new relationalStore.RdbPredicates('music');
predicates.equalTo('id', musicId);
if (!this.rdbStore) {
return false;
}
const result = await this.rdbStore.query(predicates, ['is_favorite']);
if (result.goToNextRow()) {
const currentFavorite = result.getLong(result.getColumnIndex('is_favorite')) === 1;
result.close();
// 更新状态
const updatePredicates = new relationalStore.RdbPredicates('music');
updatePredicates.equalTo('id', musicId);
const valueBucket: relationalStore.ValuesBucket = {
'is_favorite': currentFavorite ? 0 : 1
};
await this.rdbStore.update(valueBucket, updatePredicates);
return !currentFavorite;
}
result.close();
return false;
} catch (error) {
console.error(`切换收藏状态失败: ${JSON.stringify(error)}`);
return false;
}
}
// 获取收藏的音乐
async getFavoriteMusic(): Promise<MusicItem[]> {
try {
if (!this.rdbStore) {
return [];
}
const predicates = new relationalStore.RdbPredicates('music');
predicates.equalTo('is_favorite', 1).orderByDesc('create_time');
const result = await this.rdbStore.query(predicates, [
'id', 'title', 'artist', 'album', 'duration',
'file_path', 'cover_url', 'file_name', 'file_size',
'create_time', 'play_count', 'is_favorite', 'lyrics'
]);
const musicList: MusicItem[] = [];
while (result.goToNextRow()) {
musicList.push({
id: result.getLong(result.getColumnIndex('id')),
title: result.getString(result.getColumnIndex('title')),
artist: result.getString(result.getColumnIndex('artist')),
album: result.getString(result.getColumnIndex('album')),
duration: result.getLong(result.getColumnIndex('duration')),
filePath: result.getString(result.getColumnIndex('file_path')),
cover: result.getString(result.getColumnIndex('cover_url')) || $r('app.media.ic_album'),
fileName: result.getString(result.getColumnIndex('file_name')),
fileSize: result.getLong(result.getColumnIndex('file_size')),
createTime: result.getLong(result.getColumnIndex('create_time')),
playCount: result.getLong(result.getColumnIndex('play_count')),
isFavorite: true,
lyrics: result.getString(result.getColumnIndex('lyrics')) || ''
});
}
result.close();
return musicList;
} catch (error) {
console.error(`获取收藏音乐失败: ${JSON.stringify(error)}`);
return [];
}
}
// 更新播放次数
async updatePlayCount(musicId: number): Promise<void> {
try {
// 先获取当前播放次数
const queryPredicates = new relationalStore.RdbPredicates('music');
queryPredicates.equalTo('id', musicId);
if (!this.rdbStore) {
return;
}
const result = await this.rdbStore.query(queryPredicates, ['play_count']);
if (result.goToNextRow()) {
const currentCount = result.getLong(result.getColumnIndex('play_count'));
result.close();
const updatePredicates = new relationalStore.RdbPredicates('music');
updatePredicates.equalTo('id', musicId);
const valueBucket: relationalStore.ValuesBucket = {
'play_count': currentCount + 1
};
await this.rdbStore.update(valueBucket, updatePredicates);
} else {
result.close();
}
} catch (error) {
console.error(`更新播放次数失败: ${JSON.stringify(error)}`);
}
}
// 添加播放历史
async addPlayHistory(musicId: number, position: number): Promise<void> {
const valueBucket: relationalStore.ValuesBucket = {
'music_id': musicId,
'play_time': Date.now(),
'position': position
};
if (!this.rdbStore) {
return;
}
try {
await this.rdbStore.insert('play_history', valueBucket);
} catch (error) {
console.error(`添加播放历史失败: ${error.message}`);
}
}
// 创建播放列表
async createPlaylist(name: string): Promise<number> {
const valueBucket: relationalStore.ValuesBucket = {
'name': name,
'cover_url': '',
'create_time': Date.now()
};
if (!this.rdbStore) {
throw new Error("数据库未初始化");
}
try {
return await this.rdbStore.insert('playlist', valueBucket);
} catch (error) {
console.error(`创建播放列表失败: ${error.message}`);
throw new Error(`创建播放列表失败: ${JSON.stringify(error)}`);
}
}
// 获取所有播放列表
async getAllPlaylists(): Promise<Playlist[]> {
try {
if (!this.rdbStore) {
return [];
}
const predicates = new relationalStore.RdbPredicates('playlist');
predicates.orderByDesc('create_time');
const result = await this.rdbStore.query(predicates, [
'id', 'name', 'cover_url', 'music_count', 'create_time'
]);
const playlists: Playlist[] = [];
while (result.goToNextRow()) {
playlists.push({
id: result.getLong(result.getColumnIndex('id')),
name: result.getString(result.getColumnIndex('name')),
cover: result.getString(result.getColumnIndex('cover_url')) || $r('app.media.ic_album'),
musicCount: result.getLong(result.getColumnIndex('music_count')),
createTime: result.getLong(result.getColumnIndex('create_time'))
});
}
result.close();
return playlists;
} catch (error) {
console.error(`获取播放列表失败: ${JSON.stringify(error)}`);
return [];
}
}
// 删除音乐
async deleteMusic(musicId: number): Promise<boolean> {
if (!this.rdbStore) {
return false;
}
try {
const predicates = new relationalStore.RdbPredicates('music');
predicates.equalTo('id', musicId);
const deletedRows = await this.rdbStore.delete(predicates);
return deletedRows > 0;
} catch (error) {
console.error(`删除音乐失败: ${JSON.stringify(error)}`);
return false;
}
}
// 清空所有音乐
async clearAllMusic(): Promise<boolean> {
console.info('[Database] 清空所有音乐...');
if (!this.rdbStore) {
console.error('[Database] ❌ 数据库未初始化');
return false;
}
try {
const predicates = new relationalStore.RdbPredicates('music');
const deletedRows = await this.rdbStore.delete(predicates);
console.info(`[Database] ✅ 清空成功,删除 ${deletedRows} 条记录`);
return true;
} catch (error) {
console.error(`[Database] ❌ 清空音乐失败: ${JSON.stringify(error)}`);
return false;
}
}
// 检查音乐是否已存在(根据文件路径)
async isMusicExists(filePath: string): Promise<boolean> {
if (!this.rdbStore) {
console.warn('[Database] 数据库未初始化,返回 false');
return false;
}
try {
const predicates = new relationalStore.RdbPredicates('music');
predicates.equalTo('file_path', filePath);
const result = await this.rdbStore.query(predicates, ['id']);
const exists = result.goToNextRow();
result.close();
if (exists) {
console.info(`[Database] 音乐已存在: ${filePath}`);
}
return exists;
} catch (error) {
console.error(`[Database] ❌ 检查音乐存在失败: ${JSON.stringify(error)}`);
return false;
}
}
// 更新音乐时长
async updateMusicDuration(musicId: number, duration: number): Promise<void> {
if (!this.rdbStore) {
return;
}
try {
const predicates = new relationalStore.RdbPredicates('music');
predicates.equalTo('id', musicId);
const valueBucket: relationalStore.ValuesBucket = {
'duration': duration
};
await this.rdbStore.update(valueBucket, predicates);
} catch (error) {
console.error(`更新音乐时长失败: ${JSON.stringify(error)}`);
}
}
// 更新音乐歌词
async updateMusicLyrics(musicId: number, lyrics: string): Promise<void> {
if (!this.rdbStore) {
return;
}
try {
const predicates = new relationalStore.RdbPredicates('music');
predicates.equalTo('id', musicId);
const valueBucket: relationalStore.ValuesBucket = {
'lyrics': lyrics
};
await this.rdbStore.update(valueBucket, predicates);
console.info(`[Database] 歌词已更新: musicId=${musicId}`);
} catch (error) {
console.error(`[Database] 更新歌词失败: ${JSON.stringify(error)}`);
}
}
}
没办法贴了,下一篇继续