MediaTable.ets 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093
  1. import relationalStore from '@ohos.data.relationalStore';
  2. import { LogUtil, StrUtil } from '@pura/harmony-utils';
  3. import { VideoItem } from '../../viewmodel/VideoItem';
  4. import { WebDavAccount } from '../../viewmodel/WebDavAccount';
  5. import { CommonConstants } from '../constants/CommonConstants';
  6. import Logger from './Logger';
  7. import RdbUtils from './RdbUtils';
  8. import { AudioQuality, Utility } from './Utility';
  9. import { WebDavUrlUtil } from './WebDavUrlUtil';
  10. /**
  11. * 数据库字段常量接口定义
  12. */
  13. interface DBColumnsInterface {
  14. ID: string;
  15. NAME: string;
  16. FILE_PATH: string;
  17. TYPE: string;
  18. VIDEO_SIZE: string;
  19. C_TIME: string;
  20. PARENT_PATH: string;
  21. IS_FAV: string;
  22. PIXEL_MAP_PATH: string;
  23. ARTIST: string;
  24. ALBUM: string;
  25. FILE_NAME: string;
  26. SIZE: string;
  27. DURATION: string;
  28. MIME_TYPE: string;
  29. TRACK_COUNT: string;
  30. SAMPLE_RATE: string;
  31. LAST_PLAYED_STR: string;
  32. PLAY_COUNT: string;
  33. LYRIC_CONTENT: string;
  34. WEBDAV_ACCOUNT_ID: string;
  35. REMOTE_REL_PATH: string;
  36. }
  37. /**
  38. * 数据库字段常量,避免硬编码
  39. */
  40. const DB_COLUMNS: DBColumnsInterface = {
  41. ID: 'id',
  42. NAME: 'name',
  43. FILE_PATH: 'filePath',
  44. TYPE: 'mtype',
  45. VIDEO_SIZE: 'videoSize',
  46. C_TIME: 'cTime',
  47. PARENT_PATH: 'parentPath',
  48. IS_FAV: 'isFav',
  49. PIXEL_MAP_PATH: 'pixelMapPath',
  50. ARTIST: 'artist',
  51. ALBUM: 'album',
  52. FILE_NAME: 'fileName',
  53. SIZE: 'size',
  54. DURATION: 'duration',
  55. MIME_TYPE: 'mimeType',
  56. TRACK_COUNT: 'trackCount',
  57. SAMPLE_RATE: 'sampleRate',
  58. LAST_PLAYED_STR: 'lastPlayedStr',
  59. PLAY_COUNT: 'playCount',
  60. LYRIC_CONTENT: 'lyricContent',
  61. WEBDAV_ACCOUNT_ID: 'webdav_account_id',
  62. REMOTE_REL_PATH: 'remote_rel_path'
  63. };
  64. export default class MediaTable {
  65. private accountTable = new RdbUtils(RdbUtils.MEDIA_TABLE.tableName, RdbUtils.MEDIA_TABLE.sqlCreate,
  66. RdbUtils.MEDIA_TABLE.columns);
  67. constructor(context:Context,callback: Function = () => {
  68. }) {
  69. this.accountTable.getRdbStore(context,callback);
  70. }
  71. /**
  72. * 判断指定 filePath 是否已存在
  73. */
  74. private async existsByFilePath(filePath: string): Promise<boolean> {
  75. return new Promise<boolean>((resolve, reject) => {
  76. try {
  77. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  78. predicates.equalTo(DB_COLUMNS.FILE_PATH, filePath);
  79. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  80. try {
  81. resolve(resultSet.rowCount > 0);
  82. } catch (e) {
  83. reject(e);
  84. } finally {
  85. resultSet.close();
  86. }
  87. });
  88. } catch (err) {
  89. reject(err);
  90. }
  91. });
  92. }
  93. /**
  94. * WebDAV歌曲入库(若不存在则插入)
  95. */
  96. public async upsertWebDavVideoItem(item: VideoItem): Promise<boolean> {
  97. try {
  98. if (!item || !item.filePath) {
  99. return false;
  100. }
  101. // 将完整URL转换为相对路径用于存储
  102. const relativePath = WebDavUrlUtil.toStoragePath(item.filePath);
  103. if (relativePath !== item.filePath) {
  104. // 只有当转换成功时才设置remote_rel_path
  105. item.remote_rel_path = relativePath;
  106. // 更新filePath为相对路径
  107. item.filePath = relativePath;
  108. Logger.info(RdbUtils.RDB_TAG, `WebDAV URL转换: 原始URL -> 存储路径: ${relativePath}`);
  109. }
  110. const exists = await this.existsByFilePath(item.filePath);
  111. if (exists) {
  112. return true; // 已存在不再重复插入
  113. }
  114. return await this.insertWebDavItem(item);
  115. } catch (e) {
  116. Logger.error(RdbUtils.RDB_TAG, 'upsertWebDavVideoItem 失败: ' + (e as Error).message);
  117. return false;
  118. }
  119. }
  120. /**
  121. * 插入WebDAV歌曲的最小字段集合
  122. */
  123. private async insertWebDavItem(item: VideoItem): Promise<boolean> {
  124. return new Promise<boolean>((resolve) => {
  125. try {
  126. const bucket: relationalStore.ValuesBucket = generateBucket(item);
  127. if (!bucket.id) { // 使用filePath作为主键/ID以避免为空
  128. bucket.id = item.filePath;
  129. }
  130. if (!bucket.parentPath) {
  131. // 以路径去掉文件名部分作为 parentPath(已经是相对路径)
  132. const idx = item.filePath.lastIndexOf('/');
  133. if (idx > 0) {
  134. bucket.parentPath = item.filePath.substring(0, idx);
  135. }
  136. }
  137. if (item.remote_rel_path) {
  138. bucket.remote_rel_path = item.remote_rel_path;
  139. }
  140. this.accountTable.insertData(bucket, (success: boolean) => {
  141. resolve(success);
  142. });
  143. } catch (e) {
  144. Logger.error(RdbUtils.RDB_TAG, 'insertWebDavItem 出错: ' + (e as Error).message);
  145. resolve(false);
  146. }
  147. });
  148. }
  149. getRdbStore(context:Context,callback: Function = () => {
  150. }) {
  151. this.accountTable.getRdbStore(context,callback);
  152. }
  153. insert(item: VideoItem, callback: Function,cover_api?:string) {
  154. const valueBucket: relationalStore.ValuesBucket = generateBucket(item);
  155. this.accountTable.insertData(valueBucket, callback,cover_api);
  156. }
  157. deleteData(item: VideoItem, callback: Function) {
  158. let predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  159. predicates.equalTo('id', item.id);
  160. this.accountTable.deleteData(predicates, callback);
  161. }
  162. deleteDataFilePath(filePath: string, callback: Function) {
  163. let predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  164. predicates.equalTo('filePath', filePath);
  165. this.accountTable.deleteData(predicates, callback);
  166. }
  167. deleteDataForParentPath(parentPath:string, callback: Function) {
  168. let predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  169. predicates.equalTo('parentPath', parentPath);
  170. this.accountTable.deleteData(predicates, callback);
  171. }
  172. //更新音乐封面地址
  173. public updatePixelMapPath(filePath: string, newPixelMapPath: string, callback: Function) {
  174. // Step 1: 构建查询条件验证文件存在性
  175. const queryPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  176. queryPredicates.equalTo('filePath', filePath);
  177. // Step 2: 执行存在性验证
  178. this.accountTable.query(queryPredicates, (resultSet: relationalStore.ResultSet) => {
  179. if (resultSet.rowCount === 0) {
  180. callback(false, 'Error: Target file not found in database');
  181. resultSet.close();
  182. return;
  183. }
  184. resultSet.close();
  185. // Step 3: 构建更新条件与数据
  186. const updatePredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  187. updatePredicates.equalTo('filePath', filePath);
  188. const valueBucket: relationalStore.ValuesBucket = {
  189. pixelMapPath: newPixelMapPath
  190. };
  191. // Step 4: 执行原子化更新操作
  192. this.accountTable.updateData(updatePredicates, valueBucket, (success: boolean) => {
  193. callback(success, success ? null : 'Database update operation failed');
  194. });
  195. });
  196. }
  197. updateData(item: VideoItem, callback: Function) {
  198. const valueBucket: relationalStore.ValuesBucket = generateBucket(item);
  199. let predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  200. predicates.equalTo('id', item.id);
  201. this.accountTable.updateData(predicates, valueBucket, callback);
  202. }
  203. //编辑歌曲的信息更新数据库
  204. public updateMediaInfo(filePath: string, title: string, artist: string, album: string,
  205. lyricContent:string,year:string,genre:string,track:string,
  206. ALBUMARTIST:string,COMPOSER:string,LYRICIST:string,COMMENT:string,disc:string,
  207. callback: Function): boolean {
  208. if (!callback || typeof callback !== 'function') {
  209. Logger.info(RdbUtils.RDB_TAG, 'updateMediaInfo() has no valid callback!');
  210. return false;
  211. }
  212. if (!this.accountTable) {
  213. Logger.error(RdbUtils.RDB_TAG, 'RdbStore is not initialized.');
  214. callback(false);
  215. return false;
  216. }
  217. // Step 1: Create a predicate to find the record by filePath
  218. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  219. predicates.equalTo('filePath', filePath);
  220. // Step 2: Query the database to check if the record exists
  221. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  222. if (resultSet.rowCount === 0) {
  223. Logger.info(RdbUtils.RDB_TAG, `No record found with filePath ${filePath}.`);
  224. callback(false);
  225. resultSet.close();
  226. return;
  227. }
  228. // Step 3: Prepare the values to update
  229. const valuesToUpdate: relationalStore.ValuesBucket = {};
  230. if (title !== '') {
  231. valuesToUpdate.name = title;
  232. }
  233. if (artist !== '') {
  234. valuesToUpdate.artist = artist;
  235. }
  236. if (album !== '') {
  237. valuesToUpdate.album = album;
  238. }
  239. if (lyricContent !== '') {
  240. valuesToUpdate.lyricContent = lyricContent;
  241. }
  242. if (year !== '') {
  243. valuesToUpdate.year = year;
  244. }
  245. if (genre !== '') {
  246. valuesToUpdate.genre = genre;
  247. }
  248. if (track !== '') {
  249. valuesToUpdate.track = track;
  250. }
  251. if (ALBUMARTIST !== '') {
  252. valuesToUpdate.ALBUMARTIST = ALBUMARTIST;
  253. }
  254. if (COMPOSER !== '') {
  255. valuesToUpdate.COMPOSER = COMPOSER;
  256. }
  257. if (LYRICIST !== '') {
  258. valuesToUpdate.LYRICIST = LYRICIST;
  259. }
  260. if (COMMENT !== '') {
  261. valuesToUpdate.COMMENT = COMMENT;
  262. }
  263. if (disc !== '') {
  264. valuesToUpdate.disc = disc;
  265. }
  266. resultSet.close();
  267. // Step 4: Update the record if there are values to update
  268. if (Object.keys(valuesToUpdate).length > 0) {
  269. this.accountTable.updateData(predicates, valuesToUpdate, (success: boolean) => {
  270. callback(success, success ? null : 'Database update operation failed');
  271. });
  272. } else {
  273. Logger.info(RdbUtils.RDB_TAG, 'No fields to update.');
  274. callback(false);
  275. }
  276. });
  277. return true; // 表示方法已成功启动
  278. }
  279. //更新重命名数据操作
  280. public updateRename(newName: string, oldPath: string, newPath: string, callback: Function) {
  281. // Step 1: 查询原始记录
  282. const queryPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  283. queryPredicates.equalTo('filePath', oldPath);
  284. this.accountTable.query(queryPredicates, (resultSet: relationalStore.ResultSet) => {
  285. if (resultSet.rowCount === 0) {
  286. callback(false, 'Error: File not found');
  287. return;
  288. }
  289. resultSet.goToFirstRow();
  290. // Step 3: 构建更新数据
  291. const currentName = resultSet.getString(resultSet.getColumnIndex('name'));
  292. const currentFileName = resultSet.getString(resultSet.getColumnIndex('fileName'));
  293. let obj: relationalStore.ValuesBucket = {};
  294. obj.id = newPath
  295. obj.filePath = newPath;
  296. if (currentName === currentFileName) {
  297. obj.name = newName;
  298. obj.fileName = newName;
  299. } else {
  300. obj.fileName = newName;
  301. }
  302. obj.mtype = resultSet.getDouble(resultSet.getColumnIndex('mtype'));
  303. obj.videoSize = resultSet.getDouble(resultSet.getColumnIndex('videoSize'));
  304. obj.cTime = resultSet.getString(resultSet.getColumnIndex('cTime'));
  305. obj.parentPath = resultSet.getString(resultSet.getColumnIndex('parentPath'));
  306. // obj.pixelMapToString = resultSet.getString(resultSet.getColumnIndex('pixelMapToString'));
  307. obj.artist = resultSet.getString(resultSet.getColumnIndex('artist'));
  308. obj.album = resultSet.getString(resultSet.getColumnIndex('album'));
  309. obj.isFav = resultSet.getDouble(resultSet.getColumnIndex('isFav'));
  310. obj.pixelMapPath = resultSet.getString(resultSet.getColumnIndex('pixelMapPath'));
  311. obj.duration = resultSet.getString(resultSet.getColumnIndex('duration'));
  312. obj.mimeType = resultSet.getString(resultSet.getColumnIndex('mimeType'));
  313. obj.trackCount = resultSet.getString(resultSet.getColumnIndex('trackCount'));
  314. obj.sampleRate = resultSet.getString(resultSet.getColumnIndex('sampleRate'));
  315. obj.lastPlayedStr = resultSet.getString(resultSet.getColumnIndex('lastPlayedStr'));
  316. obj.playCount = resultSet.getDouble(resultSet.getColumnIndex('playCount'));
  317. obj.lyricContent = resultSet.getString(resultSet.getColumnIndex('lyricContent'));
  318. obj.md5Str = resultSet.getString(resultSet.getColumnIndex('md5Str'));
  319. obj.extra_json = resultSet.getString(resultSet.getColumnIndex('extra_json'));
  320. obj.pyStr = resultSet.getString(resultSet.getColumnIndex('pyStr'));
  321. obj.bit_rate = resultSet.getString(resultSet.getColumnIndex('bit_rate'));
  322. obj.probe_score = resultSet.getDouble(resultSet.getColumnIndex('probe_score'));
  323. obj.year = resultSet.getString(resultSet.getColumnIndex('year'));
  324. obj.nb_streams = resultSet.getDouble(resultSet.getColumnIndex('nb_streams'));
  325. obj.nb_programs = resultSet.getDouble(resultSet.getColumnIndex('nb_programs'));
  326. obj.genre = resultSet.getString(resultSet.getColumnIndex('genre'));
  327. obj.track = resultSet.getString(resultSet.getColumnIndex('track'));
  328. obj.bits_per_raw_sample = resultSet.getString(resultSet.getColumnIndex('bits_per_raw_sample'));
  329. obj.channels = resultSet.getString(resultSet.getColumnIndex('channels'));
  330. obj.channel_layout = resultSet.getString(resultSet.getColumnIndex('channel_layout'));
  331. obj.start_time = resultSet.getString(resultSet.getColumnIndex('start_time'));
  332. obj.ALBUMARTIST = resultSet.getString(resultSet.getColumnIndex('ALBUMARTIST'));
  333. obj.COMPOSER = resultSet.getString(resultSet.getColumnIndex('COMPOSER'));
  334. obj.LYRICIST = resultSet.getString(resultSet.getColumnIndex('LYRICIST'));
  335. obj.COMMENT = resultSet.getString(resultSet.getColumnIndex('COMMENT'));
  336. obj.disc = resultSet.getString(resultSet.getColumnIndex('disc'));
  337. const valueBucket: relationalStore.ValuesBucket = obj
  338. // Step 4: 执行更新
  339. const updatePredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  340. updatePredicates.equalTo('filePath', oldPath);
  341. this.accountTable.updateData(updatePredicates, valueBucket, (success: boolean) => {
  342. callback(success, success ? null : 'Update failed');
  343. });
  344. resultSet.close()
  345. });
  346. }
  347. // 根据isFav查询数据
  348. public queryByisFav(isFav: number, callback: (result: VideoItem[]) => void) {
  349. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  350. predicates.equalTo('isFav', isFav);
  351. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  352. const result = this.parseResultSetToVideoItems(resultSet);
  353. callback(result);
  354. });
  355. }
  356. // 根据filePath更新isFav的值
  357. public updateIsFavByFilePath(filePath: string, isFav: number, callback: (success: boolean, error?: string) => void) {
  358. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  359. predicates.equalTo('filePath', filePath);
  360. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  361. if (resultSet.rowCount === 0) {
  362. callback(false, 'Error: File not found');
  363. resultSet.close();
  364. return;
  365. }
  366. resultSet.close();
  367. const valueBucket: relationalStore.ValuesBucket = { isFav: isFav };
  368. this.accountTable.updateData(predicates, valueBucket, (success: boolean) => {
  369. callback(success, success ? '' : 'Update failed');
  370. });
  371. });
  372. }
  373. // 查询全部,或者某个id(查询的字段,回调,是否查询全部)
  374. query(id: number, callback: Function, isAll: boolean = true) {
  375. let predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  376. if (!isAll) {
  377. predicates.equalTo('id', id);
  378. }
  379. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  380. let count: number = resultSet.rowCount;
  381. if (count === 0 || typeof count === 'string') {
  382. console.log(`${RdbUtils.RDB_TAG}` + 'Query no results!');
  383. callback([]);
  384. } else {
  385. const result = this.parseResultSetToVideoItems(resultSet);
  386. callback(result);
  387. }
  388. });
  389. }
  390. // 新增方法:根据parentPath查询数据
  391. public queryByParentPath(path: string, callback: (result: VideoItem[]) => void) {
  392. try {
  393. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  394. predicates.equalTo('parentPath', path);
  395. LogUtil.info('onecold parentPath = '+path)
  396. // 2. 执行查询并处理结果
  397. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  398. // 3. 复用已有的解析逻辑
  399. const result = this.parseResultSetToVideoItems(resultSet);
  400. callback(result);
  401. });
  402. }catch (err) {
  403. Logger.error(` onecold testtag queryByParentPath: ${err.code} - ${err.message}`);
  404. }
  405. // 1. 构建查询条件
  406. }
  407. // 查询所有艺术家及其歌曲(返回Map结构:artist -> VideoItem[])
  408. public queryArtistsWithSongs(callback: (result: Map<string, VideoItem[]>) => void) {
  409. // 1. 查询去重的艺术家列表(非空)
  410. const artistPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  411. artistPredicates.isNotNull('artist').distinct();
  412. this.accountTable.query(artistPredicates, (resultSet: relationalStore.ResultSet) => {
  413. const artists: string[] = this.parseDistinctColumn(resultSet, 'artist');
  414. // 2. 遍历每个艺术家,查询其歌曲
  415. const resultMap = new Map<string, VideoItem[]>();
  416. let processedCount = 0;
  417. if (artists.length === 0) {
  418. callback(resultMap);
  419. return;
  420. }
  421. artists.forEach(artist => {
  422. const songPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  423. songPredicates.equalTo('artist', artist);
  424. this.accountTable.query(songPredicates, (songResultSet: relationalStore.ResultSet) => {
  425. const songs = this.parseResultSetToVideoItems(songResultSet);
  426. resultMap.set(artist, songs);
  427. processedCount++;
  428. // 3. 全部查询完成后回调
  429. if (processedCount === artists.length) {
  430. callback(resultMap);
  431. }
  432. });
  433. });
  434. });
  435. }
  436. // 查询所有专辑及其歌曲(返回Map结构:album -> VideoItem[])
  437. public queryAlbumsWithSongs(callback: (result: Map<string, VideoItem[]>) => void) {
  438. // 1. 查询去重的专辑列表(非空)
  439. const albumPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  440. albumPredicates.isNotNull('album').distinct();
  441. this.accountTable.query(albumPredicates, (resultSet: relationalStore.ResultSet) => {
  442. const albums: string[] = this.parseDistinctColumn(resultSet, 'album');
  443. // 2. 遍历每个专辑,查询其歌曲
  444. const resultMap = new Map<string, VideoItem[]>();
  445. let processedCount = 0;
  446. if (albums.length === 0) {
  447. callback(resultMap);
  448. return;
  449. }
  450. albums.forEach(album => {
  451. const songPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  452. songPredicates.equalTo('album', album);
  453. this.accountTable.query(songPredicates, (songResultSet: relationalStore.ResultSet) => {
  454. const songs = this.parseResultSetToVideoItems(songResultSet);
  455. resultMap.set(album, songs);
  456. processedCount++;
  457. // 3. 全部查询完成后回调
  458. if (processedCount === albums.length) {
  459. callback(resultMap);
  460. }
  461. });
  462. });
  463. });
  464. }
  465. // 解析去重列数据(如artist/album)
  466. private parseDistinctColumn(resultSet: relationalStore.ResultSet, columnName: string): string[] {
  467. const uniqueValues = new Set<string>(); // 使用Set特性自动去重
  468. if (resultSet.rowCount > 0) {
  469. resultSet.goToFirstRow();
  470. for (let i = 0; i < resultSet.rowCount; i++) {
  471. const value = resultSet.getString(resultSet.getColumnIndex(columnName))?.trim(); // 处理空格
  472. if (value) { // 过滤空值
  473. uniqueValues.add(value);
  474. }
  475. if (i < resultSet.rowCount - 1) { // 避免最后一行越界
  476. resultSet.goToNextRow();
  477. }
  478. }
  479. }
  480. resultSet.close();
  481. return Array.from(uniqueValues); // Set转数组
  482. }
  483. // 将ResultSet解析为VideoItem数组(复用原有逻辑)
  484. private parseResultSetToVideoItems(resultSet: relationalStore.ResultSet): VideoItem[] {
  485. const items: VideoItem[] = [];
  486. try {
  487. // 检查结果集是否有效
  488. if (resultSet && resultSet.rowCount > 0) {
  489. while (resultSet.goToNextRow()) {
  490. const item = this.buildVideoItem(resultSet);
  491. items.push(item);
  492. }
  493. }
  494. } catch (err) {
  495. Logger.error(`解析结果集出错: ${err.message}`);
  496. } finally {
  497. // 确保结果集被关闭
  498. if (resultSet) {
  499. resultSet.close();
  500. }
  501. }
  502. return items;
  503. }
  504. // 根据filePath更新lastPlayedStr的值同时playCount值加1
  505. public updateLastPlayedStrByFilePath(filePath: string, lastPlayedStr: string, callback: (success: boolean, error?: string) => void) {
  506. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  507. predicates.equalTo('filePath', filePath);
  508. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  509. if (resultSet.rowCount === 0) {
  510. callback(false, 'Error: File not found');
  511. resultSet.close();
  512. return;
  513. }
  514. // 获取当前的 playCount 值
  515. let currentPlayCount = 0;
  516. if (resultSet.goToFirstRow()) {
  517. currentPlayCount = resultSet.getLong(resultSet.getColumnIndex('playCount'));
  518. }
  519. resultSet.close();
  520. // 计算新的 playCount 值
  521. const newPlayCount = currentPlayCount + 1;
  522. // 准备要更新的值
  523. const valueBucket: relationalStore.ValuesBucket = {
  524. lastPlayedStr: lastPlayedStr,
  525. playCount: newPlayCount
  526. };
  527. // 更新数据
  528. this.accountTable.updateData(predicates, valueBucket, (success: boolean) => {
  529. callback(success, success ? '' : 'Update failed');
  530. });
  531. });
  532. }
  533. // 根据最近播放时间查询指定数量的记录
  534. public queryRecentPlayedRecords(count: number, callback: (result: VideoItem[]) => void) {
  535. try {
  536. // 1. 构建查询条件:按lastPlayedStr降序排列,限制返回条数,且lastPlayedStr不为空
  537. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  538. // 添加筛选条件,确保lastPlayedStr不为空
  539. predicates.isNotNull('lastPlayedStr');
  540. predicates.notEqualTo('lastPlayedStr', ''); // 排除空字符串
  541. predicates.notEqualTo('mtype', CommonConstants.TYPE_LOCK);
  542. // 使用 orderByDesc 方法进行降序排序
  543. predicates.orderByDesc('lastPlayedStr');
  544. // 使用 limit 方法限制返回的记录数量
  545. predicates.limitAs(count);
  546. // 2. 执行查询并处理结果
  547. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  548. // 3. 复用已有的解析逻辑
  549. const result = this.parseResultSetToVideoItems(resultSet);
  550. callback(result);
  551. });
  552. } catch (err) {
  553. Logger.error(`queryRecentPlayedRecords error: ${err.code} - ${err.message}`);
  554. callback([]);
  555. }
  556. }
  557. // 清空播放历史记录
  558. public clearPlayHistory(callback: (success: boolean, error?: string) => void) {
  559. // 1. 构建查询条件:筛选lastPlayedStr非空的记录
  560. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  561. predicates.isNotNull('lastPlayedStr');
  562. // 2. 准备更新数据:将lastPlayedStr设为空字符串
  563. const valueBucket: relationalStore.ValuesBucket = {
  564. lastPlayedStr: ''
  565. };
  566. // 3. 执行批量更新操作
  567. this.accountTable.updateData(predicates, valueBucket, (success: boolean) => {
  568. // 将 null 替换为 undefined
  569. callback(success, success ? 'Clear play history success' : 'Clear play history operation failed');
  570. });
  571. }
  572. private buildVideoItem(rs: relationalStore.ResultSet): VideoItem {
  573. // 添加空值保护
  574. const safeGet = (col: string) => {
  575. const index = rs.getColumnIndex(col);
  576. return index >= 0 ? rs.getString(index) || '' : '';
  577. };
  578. const safeGetNumber = (col: string) => {
  579. const index = rs.getColumnIndex(col);
  580. return index >= 0 ? rs.getDouble(index) || 0 : 0;
  581. };
  582. let item = new VideoItem(
  583. safeGet(DB_COLUMNS.NAME),
  584. safeGet(DB_COLUMNS.ID),
  585. safeGet(DB_COLUMNS.FILE_PATH),
  586. safeGetNumber(DB_COLUMNS.TYPE),
  587. safeGetNumber(DB_COLUMNS.VIDEO_SIZE),
  588. safeGet(DB_COLUMNS.C_TIME),
  589. undefined,
  590. safeGet(DB_COLUMNS.SIZE),
  591. safeGet(DB_COLUMNS.PIXEL_MAP_PATH),
  592. safeGet(DB_COLUMNS.ARTIST),
  593. safeGet(DB_COLUMNS.ALBUM),
  594. safeGet(DB_COLUMNS.FILE_NAME)
  595. );
  596. // 设置额外属性,添加安全检查
  597. item.isFav = safeGetNumber(DB_COLUMNS.IS_FAV);
  598. item.duration = safeGet(DB_COLUMNS.DURATION);
  599. item.mimeType = safeGet(DB_COLUMNS.MIME_TYPE);
  600. item.trackCount = safeGet(DB_COLUMNS.TRACK_COUNT);
  601. item.sampleRate = safeGet(DB_COLUMNS.SAMPLE_RATE);
  602. item.lastPlayedStr = safeGet(DB_COLUMNS.LAST_PLAYED_STR);
  603. item.playCount = safeGetNumber(DB_COLUMNS.PLAY_COUNT);
  604. item.lyricContent = safeGet(DB_COLUMNS.LYRIC_CONTENT);
  605. item.md5Str = safeGet('md5Str');
  606. item.extra_json = safeGet('extra_json');
  607. item.pyStr = safeGet('pyStr');
  608. item.bit_rate = safeGet('bit_rate');
  609. item.probe_score = safeGetNumber('probe_score');
  610. item.year = safeGet('year');
  611. item.nb_streams = safeGetNumber('nb_streams');
  612. item.nb_programs = safeGetNumber('nb_programs');
  613. item.genre = safeGet('genre');
  614. item.track = safeGet('track');
  615. item.bits_per_raw_sample = safeGet('bits_per_raw_sample');
  616. item.channels = safeGet('channels');
  617. item.channel_layout = safeGet('channel_layout');
  618. item.start_time = safeGet('start_time');
  619. item.ALBUMARTIST = safeGet('ALBUMARTIST');
  620. item.COMPOSER = safeGet('COMPOSER');
  621. item.LYRICIST = safeGet('LYRICIST');
  622. item.COMMENT = safeGet('COMMENT');
  623. item.disc = safeGet('disc');
  624. item.webdav_account_id = safeGet('webdav_account_id');
  625. item.remote_rel_path = safeGet('remote_rel_path');
  626. return item;
  627. }
  628. /**
  629. * 查询播放次数大于0的记录,并按播放次数降序排列
  630. * @param limitCount 限制返回的记录数量,0表示返回全部
  631. * @param callback 回调函数,返回VideoItem数组
  632. */
  633. public queryByPlayCountDesc(limitCount: number = 0, callback: (result: VideoItem[]) => void): void {
  634. try {
  635. // 1. 构建查询条件:playCount > 0,按playCount降序排列
  636. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  637. predicates.greaterThan('playCount', 0); // 播放次数大于0
  638. predicates.orderByDesc('playCount'); // 按播放次数降序排列
  639. // 只有当limitCount大于0时才设置限制
  640. if (limitCount > 0) {
  641. predicates.limitAs(limitCount);
  642. }
  643. // 2. 执行查询
  644. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  645. // 3. 解析结果集
  646. const result = this.parseResultSetToVideoItems(resultSet);
  647. callback(result);
  648. });
  649. } catch (err) {
  650. Logger.error(`queryByPlayCountDesc error: ${err.code} - ${err.message}`);
  651. callback([]); // 发生错误时返回空数组
  652. }
  653. }
  654. public countSongsByQualityOptimized(callback: (result: Record<AudioQuality, number>) => void): void {
  655. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  656. predicates.in('md5Str', [AudioQuality.LQ, AudioQuality.SQ, AudioQuality.HQ, AudioQuality.HIRES, AudioQuality.HR]);
  657. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  658. const result: Record<AudioQuality, number> = {
  659. [AudioQuality.LQ]: 0,
  660. [AudioQuality.SQ]: 0,
  661. [AudioQuality.HQ]: 0,
  662. [AudioQuality.HIRES]: 0 , // 只保留HR,HIRES的统计将合并到这里
  663. [AudioQuality.HR]: 0 // 只保留HR,HIRES的统计将合并到这里
  664. };
  665. if (resultSet.rowCount > 0) {
  666. resultSet.goToFirstRow();
  667. do {
  668. const quality = resultSet.getString(resultSet.getColumnIndex('md5Str')) as AudioQuality;
  669. if (quality === AudioQuality.LQ ||
  670. quality === AudioQuality.SQ ||
  671. quality === AudioQuality.HQ ||
  672. quality === AudioQuality.HIRES || // HIRES的统计将被合并到HR
  673. quality === AudioQuality.HR) {
  674. // 如果质量是HIRES或HR,都统计到HR中
  675. const targetKey = (quality === AudioQuality.HIRES || quality === AudioQuality.HR)
  676. ? AudioQuality.HR
  677. : quality;
  678. result[targetKey]++;
  679. }
  680. } while (resultSet.goToNextRow());
  681. }
  682. resultSet.close();
  683. callback(result);
  684. });
  685. }
  686. /**
  687. * Get lyric content by file path (Promise version)
  688. * @param filePath The file path to query
  689. * @returns Promise that resolves with the lyric content (string) or null if not found
  690. */
  691. public getLyricContentByFilePath(filePath: string): Promise<string | null> {
  692. return new Promise((resolve, reject) => {
  693. // Create query predicates
  694. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  695. predicates.equalTo(DB_COLUMNS.FILE_PATH, filePath);
  696. // Execute the query
  697. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  698. try {
  699. if (resultSet.rowCount === 0) {
  700. Logger.info(RdbUtils.RDB_TAG, `No record found for filePath: ${filePath}`);
  701. resolve(null);
  702. return;
  703. }
  704. // Get the first row
  705. resultSet.goToFirstRow();
  706. // Get the column index safely
  707. const columnIndex = resultSet.getColumnIndex(DB_COLUMNS.LYRIC_CONTENT);
  708. if (columnIndex < 0) {
  709. Logger.error(RdbUtils.RDB_TAG, `Column ${DB_COLUMNS.LYRIC_CONTENT} not found`);
  710. resolve(null);
  711. return;
  712. }
  713. // Get the lyric content
  714. const lyricContent = resultSet.getString(columnIndex);
  715. resolve(lyricContent || null);
  716. } catch (err) {
  717. Logger.error(RdbUtils.RDB_TAG, `Error getting lyric content: ${err.message}`);
  718. reject(err);
  719. } finally {
  720. // Ensure the result set is closed
  721. resultSet.close();
  722. }
  723. });
  724. });
  725. }
  726. /**
  727. * Check if a record exists in the database by filePath
  728. * @param filePath The file path to check
  729. * @returns Promise that resolves to true if record exists, false otherwise
  730. */
  731. public isRecordExists(filePath: string): Promise<boolean> {
  732. return new Promise((resolve, reject) => {
  733. try {
  734. // Create query predicates
  735. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  736. predicates.equalTo(DB_COLUMNS.FILE_PATH, filePath);
  737. // Execute the query
  738. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  739. try {
  740. // If rowCount > 0, record exists
  741. resolve(resultSet.rowCount > 0);
  742. } catch (err) {
  743. Logger.error(RdbUtils.RDB_TAG, `Error checking record existence: ${err.message}`);
  744. reject(err);
  745. } finally {
  746. // Ensure the result set is closed
  747. resultSet.close();
  748. }
  749. });
  750. } catch (err) {
  751. Logger.error(RdbUtils.RDB_TAG, `Error creating query: ${err.message}`);
  752. reject(err);
  753. }
  754. });
  755. }
  756. /**
  757. * Query a VideoItem by file path
  758. * @param filePath The file path to query
  759. * @returns Promise that resolves with the VideoItem or null if not found
  760. */
  761. public queryVideoByFilePath(filePath: string): Promise<VideoItem | null> {
  762. return new Promise((resolve, reject) => {
  763. try {
  764. // Create query predicates
  765. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  766. predicates.equalTo(DB_COLUMNS.FILE_PATH, filePath);
  767. // Execute the query
  768. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  769. try {
  770. if (resultSet.rowCount === 0) {
  771. Logger.info(RdbUtils.RDB_TAG, `No record found for filePath: ${filePath}`);
  772. resolve(null);
  773. return;
  774. }
  775. // Get the first row
  776. if (resultSet.goToFirstRow()) {
  777. const videoItem = this.buildVideoItem(resultSet);
  778. resolve(videoItem);
  779. } else {
  780. resolve(null);
  781. }
  782. } catch (err) {
  783. Logger.error(RdbUtils.RDB_TAG, `Error querying video by filePath: ${err.message}`);
  784. reject(err);
  785. } finally {
  786. // Ensure the result set is closed
  787. resultSet.close();
  788. }
  789. });
  790. } catch (err) {
  791. Logger.error(RdbUtils.RDB_TAG, `Error creating query: ${err.message}`);
  792. reject(err);
  793. }
  794. });
  795. }
  796. /**
  797. * Query all video items
  798. * @returns Promise that resolves with an array of VideoItem objects
  799. */
  800. public queryAllVideos(): Promise<VideoItem[]> {
  801. return new Promise((resolve, reject) => {
  802. try {
  803. Logger.info(RdbUtils.RDB_TAG, 'queryAllVideos: 开始查询所有音乐文件');
  804. // Create query predicates for all records
  805. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  806. // Only query music files (type = 0)
  807. predicates.equalTo(DB_COLUMNS.TYPE, 0);
  808. // Order by name
  809. predicates.orderByAsc(DB_COLUMNS.NAME);
  810. Logger.info(RdbUtils.RDB_TAG, 'queryAllVideos: 创建查询条件完成');
  811. // Execute the query
  812. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  813. try {
  814. Logger.info(RdbUtils.RDB_TAG, 'queryAllVideos: 查询回调执行');
  815. if (resultSet.rowCount === 0) {
  816. Logger.info(RdbUtils.RDB_TAG, 'queryAllVideos: 没有找到音乐文件');
  817. resolve([]);
  818. return;
  819. }
  820. Logger.info(RdbUtils.RDB_TAG, `queryAllVideos: 找到 ${resultSet.rowCount} 条记录`);
  821. const items: VideoItem[] = [];
  822. // Go to first row
  823. if (resultSet.goToFirstRow()) {
  824. do {
  825. const item = this.buildVideoItem(resultSet);
  826. items.push(item);
  827. } while (resultSet.goToNextRow());
  828. }
  829. Logger.info(RdbUtils.RDB_TAG, `queryAllVideos: 解析完成,返回 ${items.length} 个项目`);
  830. resolve(items);
  831. } catch (err) {
  832. Logger.error(RdbUtils.RDB_TAG, `queryAllVideos: 解析结果集出错: ${err.message}`);
  833. Logger.error(RdbUtils.RDB_TAG, `queryAllVideos: 错误堆栈: ${err.stack || '无堆栈信息'}`);
  834. reject(err);
  835. }
  836. // 注意:不在这里关闭 resultSet,因为 RdbUtils.query 会在回调函数执行后关闭它
  837. });
  838. } catch (err) {
  839. Logger.error(RdbUtils.RDB_TAG, `queryAllVideos: 创建查询出错: ${err.message}`);
  840. Logger.error(RdbUtils.RDB_TAG, `queryAllVideos: 错误堆栈: ${err.stack || '无堆栈信息'}`);
  841. reject(err);
  842. }
  843. });
  844. }
  845. }
  846. function generateBucket(item: VideoItem): relationalStore.ValuesBucket {
  847. let obj: relationalStore.ValuesBucket = {};
  848. obj.id = item.id
  849. obj.name = item.name;
  850. obj.filePath = item.filePath;
  851. obj.mtype = item.type;
  852. obj.videoSize = item.videoSize;
  853. obj.cTime = item.cTime;
  854. obj.parentPath = item.parentPath;
  855. obj.isFav = item.isFav;
  856. // if(item.pixelMapToString){
  857. // obj.pixelMapToString = item.pixelMapToString;
  858. // }
  859. if(item.artist){
  860. obj.artist = item.artist;
  861. }
  862. if(item.album){
  863. obj.album = item.album;
  864. }
  865. if(item.fileName){
  866. obj.fileName = item.fileName;
  867. }
  868. if(item.size){
  869. obj.size = item.size;
  870. }
  871. if(item.pixelMapPath){
  872. obj.pixelMapPath = item.pixelMapPath;
  873. }
  874. if(item.duration){
  875. obj.duration = item.duration;
  876. }
  877. if(item.mimeType){
  878. obj.mimeType = item.mimeType;
  879. }
  880. if(item.trackCount){
  881. obj.trackCount = item.trackCount;
  882. }
  883. if(item.sampleRate){
  884. obj.sampleRate = item.sampleRate;
  885. }
  886. if(item.lastPlayedStr){
  887. obj.lastPlayedStr = item.lastPlayedStr;
  888. }
  889. if(item.playCount){
  890. obj.playCount = item.playCount;
  891. }
  892. if(item.lyricContent){
  893. obj.lyricContent = item.lyricContent;
  894. }
  895. if(item.md5Str){
  896. obj.md5Str = item.md5Str;
  897. }
  898. if(item.extra_json){
  899. obj.extra_json = item.extra_json;
  900. }
  901. if(item.pyStr){
  902. obj.pyStr = item.pyStr;
  903. }
  904. if(item.bit_rate){
  905. obj.bit_rate = item.bit_rate;
  906. }
  907. if(item.probe_score){
  908. obj.probe_score = item.probe_score;
  909. }
  910. if(item.year){
  911. obj.year = item.year;
  912. }
  913. if(item.nb_streams){
  914. obj.nb_streams = item.nb_streams;
  915. }
  916. if(item.nb_programs){
  917. obj.nb_programs = item.nb_programs;
  918. }
  919. if(item.genre){
  920. obj.genre = item.genre;
  921. }
  922. if(item.track){
  923. obj.track = item.track;
  924. }
  925. if(item.bits_per_raw_sample){
  926. obj.bits_per_raw_sample = item.bits_per_raw_sample;
  927. }
  928. if(item.channels){
  929. obj.channels = item.channels;
  930. }
  931. if(item.channel_layout){
  932. obj.channel_layout = item.channel_layout;
  933. }
  934. if(item.start_time){
  935. obj.start_time = item.start_time;
  936. }
  937. if(item.ALBUMARTIST){
  938. obj.ALBUMARTIST = item.ALBUMARTIST;
  939. }
  940. if(item.COMPOSER){
  941. obj.COMPOSER = item.COMPOSER;
  942. }
  943. if(item.LYRICIST){
  944. obj.LYRICIST = item.LYRICIST;
  945. }
  946. if(item.COMMENT){
  947. obj.COMMENT = item.COMMENT;
  948. }
  949. if(item.disc){
  950. obj.disc = item.disc;
  951. }
  952. if(item.webdav_account_id){
  953. obj.webdav_account_id = item.webdav_account_id;
  954. }
  955. if(item.remote_rel_path){
  956. obj.remote_rel_path = item.remote_rel_path;
  957. }
  958. return obj;
  959. }