MediaTable.ets 63 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774
  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. /**
  65. * 统一的 filePath 规范化方法:
  66. * - WebDAV 的 HTTP/HTTPS 地址转为相对路径,便于与数据库匹配
  67. * - 其他路径保持不变
  68. */
  69. function normalizeFilePath(filePath: string): string {
  70. if (!filePath) {
  71. return filePath;
  72. }
  73. if (WebDavUrlUtil.isHttpUrl(filePath)) {
  74. const relative = WebDavUrlUtil.toStoragePath(filePath);
  75. return relative || filePath;
  76. }
  77. return filePath;
  78. }
  79. /**
  80. * 分页查询接口 - 支持条件查询、排序、分页
  81. */
  82. export interface PageQueryOptions {
  83. parentPath?: string; // 文件夹路径(首页模式)
  84. searchKeyword?: string; // 搜索关键词
  85. sortType?: number; // 排序类型
  86. pageIndex: number; // 页码(从0开始)
  87. pageSize: number; // 每页数量
  88. type?: number; // 媒体类型过滤
  89. isShowFileName?: boolean; // 是否显示文件名(影响排序字段选择)
  90. }
  91. export interface PageQueryResult {
  92. items: VideoItem[];
  93. totalCount: number;
  94. hasMore: boolean;
  95. countMap?: Map<string, number>;
  96. }
  97. export interface RandomSongQueryOptions {
  98. type?: number;
  99. excludeFilePaths?: string[];
  100. parentPath?: string; // 文件夹路径(用于首页模式随机播放)
  101. playlistSongFilePaths?: string[]; // 歌单歌曲路径列表(用于歌单模式随机播放)
  102. }
  103. export default class MediaTable {
  104. private accountTable = new RdbUtils(RdbUtils.MEDIA_TABLE.tableName, RdbUtils.MEDIA_TABLE.sqlCreate,
  105. RdbUtils.MEDIA_TABLE.columns);
  106. constructor(context:Context,callback: Function = () => {
  107. }) {
  108. this.accountTable.getRdbStore(context,callback);
  109. }
  110. /**
  111. * 判断指定 filePath 是否已存在
  112. */
  113. private async existsByFilePath(filePath: string): Promise<boolean> {
  114. const normalizedPath = normalizeFilePath(filePath);
  115. return new Promise<boolean>((resolve, reject) => {
  116. try {
  117. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  118. predicates.equalTo(DB_COLUMNS.FILE_PATH, normalizedPath);
  119. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  120. try {
  121. resolve(resultSet.rowCount > 0);
  122. } catch (e) {
  123. reject(e);
  124. } finally {
  125. resultSet.close();
  126. }
  127. });
  128. } catch (err) {
  129. reject(err);
  130. }
  131. });
  132. }
  133. /**
  134. * WebDAV歌曲入库(若不存在则插入)
  135. */
  136. public async upsertWebDavVideoItem(item: VideoItem): Promise<boolean> {
  137. try {
  138. if (!item || !item.filePath) {
  139. return false;
  140. }
  141. const isPureWebDav = item.type === CommonConstants.TYPE_WEBDAV;
  142. if (isPureWebDav) {
  143. // 将完整URL转换为相对路径用于存储
  144. const relativePath = WebDavUrlUtil.toStoragePath(item.filePath);
  145. if (relativePath !== item.filePath) {
  146. // 只有当转换成功时才设置remote_rel_path
  147. item.remote_rel_path = relativePath;
  148. // 更新filePath为相对路径
  149. item.filePath = relativePath;
  150. Logger.info(RdbUtils.RDB_TAG, `WebDAV URL转换: 原始URL -> 存储路径: ${relativePath}`);
  151. }
  152. }
  153. const exists = await this.existsByFilePath(item.filePath);
  154. if (exists) {
  155. return true; // 已存在不再重复插入
  156. }
  157. return await this.insertWebDavItem(item);
  158. } catch (e) {
  159. Logger.error(RdbUtils.RDB_TAG, 'upsertWebDavVideoItem 失败: ' + (e as Error).message);
  160. return false;
  161. }
  162. }
  163. /**
  164. * 插入WebDAV歌曲的最小字段集合
  165. */
  166. private async insertWebDavItem(item: VideoItem): Promise<boolean> {
  167. return new Promise<boolean>((resolve) => {
  168. try {
  169. const bucket: relationalStore.ValuesBucket = generateBucket(item);
  170. if (!bucket.id) { // 使用filePath作为主键/ID以避免为空
  171. bucket.id = item.filePath;
  172. }
  173. if (!bucket.parentPath) {
  174. // 以路径去掉文件名部分作为 parentPath(已经是相对路径)
  175. const idx = item.filePath.lastIndexOf('/');
  176. if (idx > 0) {
  177. bucket.parentPath = item.filePath.substring(0, idx);
  178. }
  179. }
  180. if (item.remote_rel_path) {
  181. bucket.remote_rel_path = item.remote_rel_path;
  182. }
  183. this.accountTable.insertData(bucket, (success: boolean) => {
  184. resolve(success);
  185. });
  186. } catch (e) {
  187. Logger.error(RdbUtils.RDB_TAG, 'insertWebDavItem 出错: ' + (e as Error).message);
  188. resolve(false);
  189. }
  190. });
  191. }
  192. /**
  193. * 保存或更新WebDAV歌曲的完整元数据
  194. */
  195. public async saveOrUpdateWebDavItem(item: VideoItem): Promise<boolean> {
  196. try {
  197. if (!item || !item.filePath) {
  198. return false;
  199. }
  200. item.filePath = normalizeFilePath(item.filePath);
  201. if (!item.remote_rel_path) {
  202. item.remote_rel_path = item.filePath;
  203. }
  204. if (!item.parentPath) {
  205. const idx = item.filePath.lastIndexOf('/');
  206. item.parentPath = idx > 0 ? item.filePath.substring(0, idx) : '';
  207. }
  208. const exists = await this.existsByFilePath(item.filePath);
  209. if (exists) {
  210. return await new Promise<boolean>((resolve) => {
  211. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  212. predicates.equalTo(DB_COLUMNS.FILE_PATH, item.filePath);
  213. const bucket = generateBucket(item);
  214. this.accountTable.updateData(predicates, bucket, (success: boolean) => resolve(success));
  215. });
  216. }
  217. return await this.insertWebDavItem(item);
  218. } catch (error) {
  219. Logger.error(RdbUtils.RDB_TAG, 'saveOrUpdateWebDavItem 失败: ' + (error as Error).message);
  220. return false;
  221. }
  222. }
  223. getRdbStore(context:Context,callback: Function = () => {
  224. }) {
  225. this.accountTable.getRdbStore(context,callback);
  226. }
  227. insert(item: VideoItem, callback: Function,cover_api?:string) {
  228. const valueBucket: relationalStore.ValuesBucket = generateBucket(item);
  229. this.accountTable.insertData(valueBucket, callback,cover_api);
  230. }
  231. deleteData(item: VideoItem, callback: Function) {
  232. let predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  233. predicates.equalTo('id', item.id);
  234. this.accountTable.deleteData(predicates, callback);
  235. }
  236. deleteDataFilePath(filePath: string, callback: Function) {
  237. const normalizedPath = normalizeFilePath(filePath);
  238. let predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  239. predicates.equalTo('filePath', normalizedPath);
  240. this.accountTable.deleteData(predicates, callback);
  241. }
  242. deleteDataForParentPath(parentPath:string, callback: Function) {
  243. let predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  244. predicates.equalTo('parentPath', parentPath);
  245. this.accountTable.deleteData(predicates, callback);
  246. }
  247. //更新音乐封面地址
  248. public updatePixelMapPath(filePath: string, newPixelMapPath: string, callback: Function) {
  249. const normalizedPath = normalizeFilePath(filePath);
  250. // Step 1: 构建查询条件验证文件存在性
  251. const queryPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  252. queryPredicates.equalTo('filePath', normalizedPath);
  253. // Step 2: 执行存在性验证
  254. this.accountTable.query(queryPredicates, (resultSet: relationalStore.ResultSet) => {
  255. if (resultSet.rowCount === 0) {
  256. callback(false, 'Error: Target file not found in database');
  257. resultSet.close();
  258. return;
  259. }
  260. resultSet.close();
  261. // Step 3: 构建更新条件与数据
  262. const updatePredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  263. updatePredicates.equalTo('filePath', normalizedPath);
  264. const valueBucket: relationalStore.ValuesBucket = {
  265. pixelMapPath: newPixelMapPath
  266. };
  267. // Step 4: 执行原子化更新操作
  268. this.accountTable.updateData(updatePredicates, valueBucket, (success: boolean) => {
  269. callback(success, success ? null : 'Database update operation failed');
  270. });
  271. });
  272. }
  273. updateData(item: VideoItem, callback: Function) {
  274. const valueBucket: relationalStore.ValuesBucket = generateBucket(item);
  275. let predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  276. predicates.equalTo('id', item.id);
  277. this.accountTable.updateData(predicates, valueBucket, callback);
  278. }
  279. //编辑歌曲的信息更新数据库
  280. public updateMediaInfo(filePath: string, title: string, artist: string, album: string,
  281. lyricContent:string,year:string,genre:string,track:string,
  282. ALBUMARTIST:string,COMPOSER:string,LYRICIST:string,COMMENT:string,disc:string,
  283. callback: Function): boolean {
  284. if (!callback || typeof callback !== 'function') {
  285. Logger.info(RdbUtils.RDB_TAG, 'updateMediaInfo() has no valid callback!');
  286. return false;
  287. }
  288. if (!this.accountTable) {
  289. Logger.error(RdbUtils.RDB_TAG, 'RdbStore is not initialized.');
  290. callback(false);
  291. return false;
  292. }
  293. const normalizedPath = normalizeFilePath(filePath);
  294. console.info('onecold filePath updateMediaInfo= '+normalizedPath)
  295. // Step 1: Create a predicate to find the record by filePath
  296. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  297. predicates.equalTo('filePath', normalizedPath);
  298. // Step 2: Query the database to check if the record exists
  299. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  300. if (resultSet.rowCount === 0) {
  301. Logger.info(RdbUtils.RDB_TAG, `No record found with filePath ${normalizedPath}.`);
  302. console.info('onecold No record found with filePath '+normalizedPath)
  303. callback(false);
  304. resultSet.close();
  305. return;
  306. }
  307. console.info('onecold filePath updateMediaInfo1= '+normalizedPath)
  308. // Step 3: Prepare the values to update
  309. const valuesToUpdate: relationalStore.ValuesBucket = {};
  310. if (title !== '') {
  311. valuesToUpdate.name = title;
  312. }
  313. if (artist !== '') {
  314. valuesToUpdate.artist = artist;
  315. }
  316. if (album !== '') {
  317. valuesToUpdate.album = album;
  318. }
  319. if (lyricContent !== '') {
  320. valuesToUpdate.lyricContent = lyricContent;
  321. }
  322. if (year !== '') {
  323. valuesToUpdate.year = year;
  324. }
  325. if (genre !== '') {
  326. valuesToUpdate.genre = genre;
  327. }
  328. if (track !== '') {
  329. valuesToUpdate.track = track;
  330. }
  331. if (ALBUMARTIST !== '') {
  332. valuesToUpdate.ALBUMARTIST = ALBUMARTIST;
  333. }
  334. if (COMPOSER !== '') {
  335. valuesToUpdate.COMPOSER = COMPOSER;
  336. }
  337. if (LYRICIST !== '') {
  338. valuesToUpdate.LYRICIST = LYRICIST;
  339. }
  340. if (COMMENT !== '') {
  341. valuesToUpdate.COMMENT = COMMENT;
  342. }
  343. if (disc !== '') {
  344. valuesToUpdate.disc = disc;
  345. }
  346. resultSet.close();
  347. // Step 4: Update the record if there are values to update
  348. if (Object.keys(valuesToUpdate).length > 0) {
  349. this.accountTable.updateData(predicates, valuesToUpdate, (success: boolean) => {
  350. callback(success, success ? null : 'Database update operation failed');
  351. });
  352. } else {
  353. Logger.info(RdbUtils.RDB_TAG, 'No fields to update.');
  354. callback(false);
  355. }
  356. });
  357. return true; // 表示方法已成功启动
  358. }
  359. //更新重命名数据操作
  360. public updateRename(newName: string, oldPath: string, newPath: string, callback: Function) {
  361. const normalizedOldPath = normalizeFilePath(oldPath);
  362. const normalizedNewPath = normalizeFilePath(newPath);
  363. // Step 1: 查询原始记录
  364. const queryPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  365. queryPredicates.equalTo('filePath', normalizedOldPath);
  366. this.accountTable.query(queryPredicates, (resultSet: relationalStore.ResultSet) => {
  367. if (resultSet.rowCount === 0) {
  368. callback(false, 'Error: File not found');
  369. return;
  370. }
  371. resultSet.goToFirstRow();
  372. // Step 3: 构建更新数据
  373. const currentName = resultSet.getString(resultSet.getColumnIndex('name'));
  374. const currentFileName = resultSet.getString(resultSet.getColumnIndex('fileName'));
  375. let obj: relationalStore.ValuesBucket = {};
  376. obj.id = normalizedNewPath
  377. obj.filePath = normalizedNewPath;
  378. if (currentName === currentFileName) {
  379. obj.name = newName;
  380. obj.fileName = newName;
  381. } else {
  382. obj.fileName = newName;
  383. }
  384. obj.mtype = resultSet.getDouble(resultSet.getColumnIndex('mtype'));
  385. obj.videoSize = resultSet.getDouble(resultSet.getColumnIndex('videoSize'));
  386. obj.cTime = resultSet.getString(resultSet.getColumnIndex('cTime'));
  387. obj.parentPath = resultSet.getString(resultSet.getColumnIndex('parentPath'));
  388. // obj.pixelMapToString = resultSet.getString(resultSet.getColumnIndex('pixelMapToString'));
  389. obj.artist = resultSet.getString(resultSet.getColumnIndex('artist'));
  390. obj.album = resultSet.getString(resultSet.getColumnIndex('album'));
  391. obj.isFav = resultSet.getDouble(resultSet.getColumnIndex('isFav'));
  392. obj.pixelMapPath = resultSet.getString(resultSet.getColumnIndex('pixelMapPath'));
  393. obj.duration = resultSet.getString(resultSet.getColumnIndex('duration'));
  394. obj.mimeType = resultSet.getString(resultSet.getColumnIndex('mimeType'));
  395. obj.trackCount = resultSet.getString(resultSet.getColumnIndex('trackCount'));
  396. obj.sampleRate = resultSet.getString(resultSet.getColumnIndex('sampleRate'));
  397. obj.lastPlayedStr = resultSet.getString(resultSet.getColumnIndex('lastPlayedStr'));
  398. obj.playCount = resultSet.getDouble(resultSet.getColumnIndex('playCount'));
  399. obj.lyricContent = resultSet.getString(resultSet.getColumnIndex('lyricContent'));
  400. obj.md5Str = resultSet.getString(resultSet.getColumnIndex('md5Str'));
  401. obj.extra_json = resultSet.getString(resultSet.getColumnIndex('extra_json'));
  402. obj.pyStr = resultSet.getString(resultSet.getColumnIndex('pyStr'));
  403. obj.bit_rate = resultSet.getString(resultSet.getColumnIndex('bit_rate'));
  404. obj.probe_score = resultSet.getDouble(resultSet.getColumnIndex('probe_score'));
  405. obj.year = resultSet.getString(resultSet.getColumnIndex('year'));
  406. obj.nb_streams = resultSet.getDouble(resultSet.getColumnIndex('nb_streams'));
  407. obj.nb_programs = resultSet.getDouble(resultSet.getColumnIndex('nb_programs'));
  408. obj.genre = resultSet.getString(resultSet.getColumnIndex('genre'));
  409. obj.track = resultSet.getString(resultSet.getColumnIndex('track'));
  410. obj.bits_per_raw_sample = resultSet.getString(resultSet.getColumnIndex('bits_per_raw_sample'));
  411. obj.channels = resultSet.getString(resultSet.getColumnIndex('channels'));
  412. obj.channel_layout = resultSet.getString(resultSet.getColumnIndex('channel_layout'));
  413. obj.start_time = resultSet.getString(resultSet.getColumnIndex('start_time'));
  414. obj.ALBUMARTIST = resultSet.getString(resultSet.getColumnIndex('ALBUMARTIST'));
  415. obj.COMPOSER = resultSet.getString(resultSet.getColumnIndex('COMPOSER'));
  416. obj.LYRICIST = resultSet.getString(resultSet.getColumnIndex('LYRICIST'));
  417. obj.COMMENT = resultSet.getString(resultSet.getColumnIndex('COMMENT'));
  418. obj.disc = resultSet.getString(resultSet.getColumnIndex('disc'));
  419. const valueBucket: relationalStore.ValuesBucket = obj
  420. // Step 4: 执行更新
  421. const updatePredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  422. updatePredicates.equalTo('filePath', normalizedOldPath);
  423. this.accountTable.updateData(updatePredicates, valueBucket, (success: boolean) => {
  424. callback(success, success ? null : 'Update failed');
  425. });
  426. resultSet.close()
  427. });
  428. }
  429. // 根据isFav查询数据
  430. public queryByisFav(isFav: number, callback: (result: VideoItem[]) => void) {
  431. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  432. predicates.equalTo('isFav', isFav);
  433. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  434. const result = this.parseResultSetToVideoItems(resultSet);
  435. callback(result);
  436. });
  437. }
  438. // 根据filePath更新isFav的值
  439. public updateIsFavByFilePath(filePath: string, isFav: number, callback: (success: boolean, error?: string) => void) {
  440. const normalizedPath = normalizeFilePath(filePath);
  441. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  442. predicates.equalTo('filePath', normalizedPath);
  443. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  444. if (resultSet.rowCount === 0) {
  445. callback(false, 'Error: File not found');
  446. resultSet.close();
  447. return;
  448. }
  449. resultSet.close();
  450. const valueBucket: relationalStore.ValuesBucket = { isFav: isFav };
  451. this.accountTable.updateData(predicates, valueBucket, (success: boolean) => {
  452. callback(success, success ? '' : 'Update failed');
  453. });
  454. });
  455. }
  456. // 查询全部,或者某个id(查询的字段,回调,是否查询全部)
  457. query(id: number, callback: Function, isAll: boolean = true) {
  458. let predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  459. if (!isAll) {
  460. predicates.equalTo('id', id);
  461. }
  462. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  463. let count: number = resultSet.rowCount;
  464. if (count === 0 || typeof count === 'string') {
  465. console.log(`${RdbUtils.RDB_TAG}` + 'Query no results!');
  466. callback([]);
  467. } else {
  468. const result = this.parseResultSetToVideoItems(resultSet);
  469. callback(result);
  470. }
  471. });
  472. }
  473. // 新增方法:根据parentPath查询数据
  474. public queryByParentPath(path: string, callback: (result: VideoItem[]) => void) {
  475. try {
  476. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  477. predicates.equalTo('parentPath', path);
  478. LogUtil.info('onecold parentPath = '+path)
  479. // 2. 执行查询并处理结果
  480. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  481. // 3. 复用已有的解析逻辑
  482. const result = this.parseResultSetToVideoItems(resultSet);
  483. callback(result);
  484. });
  485. }catch (err) {
  486. Logger.error(` onecold testtag queryByParentPath: ${err.code} - ${err.message}`);
  487. }
  488. // 1. 构建查询条件
  489. }
  490. // 查询所有艺术家及其歌曲(返回Map结构:artist -> VideoItem[])
  491. public queryArtistsWithSongs(callback: (result: Map<string, VideoItem[]>) => void) {
  492. // 1. 查询去重的艺术家列表(非空)
  493. const artistPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  494. artistPredicates.isNotNull('artist').distinct();
  495. this.accountTable.query(artistPredicates, (resultSet: relationalStore.ResultSet) => {
  496. const artists: string[] = this.parseDistinctColumn(resultSet, 'artist');
  497. // 2. 遍历每个艺术家,查询其歌曲
  498. const resultMap = new Map<string, VideoItem[]>();
  499. let processedCount = 0;
  500. if (artists.length === 0) {
  501. callback(resultMap);
  502. return;
  503. }
  504. artists.forEach(artist => {
  505. const songPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  506. songPredicates.equalTo('artist', artist);
  507. this.accountTable.query(songPredicates, (songResultSet: relationalStore.ResultSet) => {
  508. const songs = this.parseResultSetToVideoItems(songResultSet);
  509. resultMap.set(artist, songs);
  510. processedCount++;
  511. // 3. 全部查询完成后回调
  512. if (processedCount === artists.length) {
  513. callback(resultMap);
  514. }
  515. });
  516. });
  517. });
  518. }
  519. // 查询所有专辑及其歌曲(返回Map结构:album -> VideoItem[])
  520. public queryAlbumsWithSongs(callback: (result: Map<string, VideoItem[]>) => void) {
  521. // 1. 查询去重的专辑列表(非空)
  522. const albumPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  523. albumPredicates.isNotNull('album').distinct();
  524. this.accountTable.query(albumPredicates, (resultSet: relationalStore.ResultSet) => {
  525. const albums: string[] = this.parseDistinctColumn(resultSet, 'album');
  526. // 2. 遍历每个专辑,查询其歌曲
  527. const resultMap = new Map<string, VideoItem[]>();
  528. let processedCount = 0;
  529. if (albums.length === 0) {
  530. callback(resultMap);
  531. return;
  532. }
  533. albums.forEach(album => {
  534. const songPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  535. songPredicates.equalTo('album', album);
  536. this.accountTable.query(songPredicates, (songResultSet: relationalStore.ResultSet) => {
  537. const songs = this.parseResultSetToVideoItems(songResultSet);
  538. resultMap.set(album, songs);
  539. processedCount++;
  540. // 3. 全部查询完成后回调
  541. if (processedCount === albums.length) {
  542. callback(resultMap);
  543. }
  544. });
  545. });
  546. });
  547. }
  548. /**
  549. * 按艺术家查询歌曲列表(用于艺术家详情页)
  550. */
  551. public async querySongsByArtist(artist: string, sortType?: number, isShowFileName?: boolean): Promise<VideoItem[]> {
  552. try {
  553. const trimmedArtist = artist?.trim() || '';
  554. if (!trimmedArtist) {
  555. return [];
  556. }
  557. // 使用SQL的TRIM函数匹配,解决数据库中artist字段可能有前后空格的问题
  558. const tableName = RdbUtils.MEDIA_TABLE.tableName;
  559. let orderBy = `${DB_COLUMNS.NAME} COLLATE NOCASE ASC`;
  560. if (isShowFileName) {
  561. orderBy = `${DB_COLUMNS.FILE_NAME} COLLATE NOCASE ASC`;
  562. }
  563. const sql = `SELECT * FROM ${tableName} WHERE TRIM(${DB_COLUMNS.ARTIST}) = ? AND ${DB_COLUMNS.TYPE} = ? ORDER BY ${orderBy}`;
  564. const params: (string | number)[] = [trimmedArtist, CommonConstants.TYPE_LOCAL];
  565. const resultSet = await this.accountTable.querySql(sql, params);
  566. try {
  567. const result = this.parseResultSetToVideoItems(resultSet, sortType);
  568. return result;
  569. } finally {
  570. resultSet.close();
  571. }
  572. } catch (err) {
  573. const error = err as Error;
  574. Logger.error('heanup MediaTable', `querySongsByArtist error: ${error.message}`);
  575. return [];
  576. }
  577. }
  578. /**
  579. * 按专辑查询歌曲列表(用于专辑详情页)
  580. */
  581. public async querySongsByAlbum(album: string, sortType?: number, isShowFileName?: boolean): Promise<VideoItem[]> {
  582. try {
  583. const trimmedAlbum = album?.trim() || '';
  584. if (!trimmedAlbum) {
  585. return [];
  586. }
  587. // 使用SQL的TRIM函数匹配,解决数据库中album字段可能有前后空格的问题
  588. const tableName = RdbUtils.MEDIA_TABLE.tableName;
  589. let orderBy = `${DB_COLUMNS.NAME} COLLATE NOCASE ASC`;
  590. if (isShowFileName) {
  591. orderBy = `${DB_COLUMNS.FILE_NAME} COLLATE NOCASE ASC`;
  592. }
  593. const sql = `SELECT * FROM ${tableName} WHERE TRIM(${DB_COLUMNS.ALBUM}) = ? AND ${DB_COLUMNS.TYPE} = ? ORDER BY ${orderBy}`;
  594. const params: (string | number)[] = [trimmedAlbum, CommonConstants.TYPE_LOCAL];
  595. const resultSet = await this.accountTable.querySql(sql, params);
  596. try {
  597. const result = this.parseResultSetToVideoItems(resultSet, sortType);
  598. return result;
  599. } finally {
  600. resultSet.close();
  601. }
  602. } catch (err) {
  603. const error = err as Error;
  604. Logger.error('heanup MediaTable', `querySongsByAlbum error: ${error.message}`);
  605. return [];
  606. }
  607. }
  608. // 解析去重列数据(如artist/album)
  609. private parseDistinctColumn(resultSet: relationalStore.ResultSet, columnName: string): string[] {
  610. const uniqueValues = new Set<string>(); // 使用Set特性自动去重
  611. if (resultSet.rowCount > 0) {
  612. resultSet.goToFirstRow();
  613. for (let i = 0; i < resultSet.rowCount; i++) {
  614. const value = resultSet.getString(resultSet.getColumnIndex(columnName))?.trim(); // 处理空格
  615. if (value) { // 过滤空值
  616. uniqueValues.add(value);
  617. }
  618. if (i < resultSet.rowCount - 1) { // 避免最后一行越界
  619. resultSet.goToNextRow();
  620. }
  621. }
  622. }
  623. resultSet.close();
  624. return Array.from(uniqueValues); // Set转数组
  625. }
  626. /**
  627. * 解析ResultSet为VideoItem数组(复用原有逻辑)
  628. * 当sortType=10时,会按照disc和track进行二次排序
  629. */
  630. private parseResultSetToVideoItems(resultSet: relationalStore.ResultSet, sortType?: number): VideoItem[] {
  631. const items: VideoItem[] = [];
  632. try {
  633. // 检查结果集是否有效
  634. if (resultSet && resultSet.rowCount > 0) {
  635. while (resultSet.goToNextRow()) {
  636. const item = this.buildVideoItem(resultSet);
  637. items.push(item);
  638. }
  639. }
  640. } catch (err) {
  641. Logger.error(`解析结果集出错: ${err.message}`);
  642. } finally {
  643. // 确保结果集被关闭
  644. if (resultSet) {
  645. resultSet.close();
  646. }
  647. }
  648. // sortType=10: 按碟片编号(disc)和音轨号(track)升序排序
  649. if (sortType === 10) {
  650. items.sort((a, b) => {
  651. // 解析disc字段,处理可能为空或包含非数字字符的情况
  652. const discA = this.parseDiscNumber(a.disc);
  653. const discB = this.parseDiscNumber(b.disc);
  654. // 先按碟片编号排序
  655. if (discA !== discB) {
  656. return discA - discB;
  657. }
  658. // 碟片编号相同,按音轨号排序
  659. const trackA = this.parseTrackNumber(a.track);
  660. const trackB = this.parseTrackNumber(b.track);
  661. return trackA - trackB;
  662. });
  663. Logger.info(RdbUtils.RDB_TAG, `parseResultSetToVideoItems: 已按碟片-音轨升序排序,歌曲数量=${items.length}`);
  664. }
  665. // sortType=11: 按碟片编号(disc)和音轨号(track)降序排序
  666. if (sortType === 11) {
  667. items.sort((a, b) => {
  668. // 解析disc字段,处理可能为空或包含非数字字符的情况
  669. const discA = this.parseDiscNumber(a.disc);
  670. const discB = this.parseDiscNumber(b.disc);
  671. // 先按碟片编号降序排序
  672. if (discA !== discB) {
  673. return discB - discA;
  674. }
  675. // 碟片编号相同,按音轨号降序排序
  676. const trackA = this.parseTrackNumber(a.track);
  677. const trackB = this.parseTrackNumber(b.track);
  678. return trackB - trackA;
  679. });
  680. Logger.info(RdbUtils.RDB_TAG, `parseResultSetToVideoItems: 已按碟片-音轨降序排序,歌曲数量=${items.length}`);
  681. }
  682. return items;
  683. }
  684. /**
  685. * 解析碟片编号,返回数字进行比较
  686. * 支持格式: "1", "1/5", "1.5", "CD1", "Vol1"等
  687. */
  688. private parseDiscNumber(disc: string | undefined | null): number {
  689. if (!disc) return 0;
  690. // 移除空格并转大写
  691. const discStr = disc.toString().trim().toUpperCase();
  692. // 尝试提取数字
  693. const match = discStr.match(/(\d+)/);
  694. if (match) {
  695. return parseInt(match[1], 10);
  696. }
  697. return 0;
  698. }
  699. /**
  700. * 解析音轨号,返回数字进行比较
  701. * 支持格式: "1", "1/10", "1.12", "Track 1"等
  702. */
  703. private parseTrackNumber(track: string | undefined | null): number {
  704. if (!track) return 0;
  705. // 移除空格并转大写
  706. const trackStr = track.toString().trim().toUpperCase();
  707. // 尝试提取数字
  708. const match = trackStr.match(/(\d+)/);
  709. if (match) {
  710. return parseInt(match[1], 10);
  711. }
  712. return 0;
  713. }
  714. // 根据filePath更新lastPlayedStr的值同时playCount值加1
  715. public updateLastPlayedStrByFilePath(filePath: string, lastPlayedStr: string, callback: (success: boolean, error?: string) => void) {
  716. const normalizedPath = normalizeFilePath(filePath);
  717. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  718. predicates.equalTo('filePath', normalizedPath);
  719. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  720. if (resultSet.rowCount === 0) {
  721. callback(false, 'Error: File not found');
  722. resultSet.close();
  723. return;
  724. }
  725. // 获取当前的 playCount 值
  726. let currentPlayCount = 0;
  727. if (resultSet.goToFirstRow()) {
  728. currentPlayCount = resultSet.getLong(resultSet.getColumnIndex('playCount'));
  729. }
  730. resultSet.close();
  731. // 计算新的 playCount 值
  732. const newPlayCount = currentPlayCount + 1;
  733. // 准备要更新的值
  734. const valueBucket: relationalStore.ValuesBucket = {
  735. lastPlayedStr: lastPlayedStr,
  736. playCount: newPlayCount
  737. };
  738. // 更新数据
  739. this.accountTable.updateData(predicates, valueBucket, (success: boolean) => {
  740. callback(success, success ? '' : 'Update failed');
  741. });
  742. });
  743. }
  744. // 根据最近播放时间查询指定数量的记录
  745. public queryRecentPlayedRecords(count: number, callback: (result: VideoItem[]) => void) {
  746. try {
  747. // 1. 构建查询条件:按lastPlayedStr降序排列,限制返回条数,且lastPlayedStr不为空
  748. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  749. // 添加筛选条件,确保lastPlayedStr不为空
  750. predicates.isNotNull('lastPlayedStr');
  751. predicates.notEqualTo('lastPlayedStr', ''); // 排除空字符串
  752. predicates.notEqualTo('mtype', CommonConstants.TYPE_LOCK);
  753. // 使用 orderByDesc 方法进行降序排序
  754. predicates.orderByDesc('lastPlayedStr');
  755. // 使用 limit 方法限制返回的记录数量
  756. predicates.limitAs(count);
  757. // 2. 执行查询并处理结果
  758. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  759. // 3. 复用已有的解析逻辑
  760. const result = this.parseResultSetToVideoItems(resultSet);
  761. callback(result);
  762. });
  763. } catch (err) {
  764. Logger.error(`queryRecentPlayedRecords error: ${err.code} - ${err.message}`);
  765. callback([]);
  766. }
  767. }
  768. // 清空播放历史记录
  769. public clearPlayHistory(callback: (success: boolean, error?: string) => void) {
  770. // 1. 构建查询条件:筛选lastPlayedStr非空的记录
  771. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  772. predicates.isNotNull('lastPlayedStr');
  773. // 2. 准备更新数据:将lastPlayedStr设为空字符串
  774. const valueBucket: relationalStore.ValuesBucket = {
  775. lastPlayedStr: ''
  776. };
  777. // 3. 执行批量更新操作
  778. this.accountTable.updateData(predicates, valueBucket, (success: boolean) => {
  779. // 将 null 替换为 undefined
  780. callback(success, success ? 'Clear play history success' : 'Clear play history operation failed');
  781. });
  782. }
  783. private buildVideoItem(rs: relationalStore.ResultSet): VideoItem {
  784. // 添加空值保护
  785. const safeGet = (col: string) => {
  786. const index = rs.getColumnIndex(col);
  787. return index >= 0 ? rs.getString(index) || '' : '';
  788. };
  789. const safeGetNumber = (col: string) => {
  790. const index = rs.getColumnIndex(col);
  791. return index >= 0 ? rs.getDouble(index) || 0 : 0;
  792. };
  793. let item = new VideoItem(
  794. safeGet(DB_COLUMNS.NAME),
  795. safeGet(DB_COLUMNS.ID),
  796. safeGet(DB_COLUMNS.FILE_PATH),
  797. safeGetNumber(DB_COLUMNS.TYPE),
  798. safeGetNumber(DB_COLUMNS.VIDEO_SIZE),
  799. safeGet(DB_COLUMNS.C_TIME),
  800. safeGet(DB_COLUMNS.SIZE),
  801. safeGet(DB_COLUMNS.PIXEL_MAP_PATH),
  802. safeGet(DB_COLUMNS.ARTIST),
  803. safeGet(DB_COLUMNS.ALBUM),
  804. safeGet(DB_COLUMNS.FILE_NAME)
  805. );
  806. // 设置额外属性,添加安全检查
  807. item.isFav = safeGetNumber(DB_COLUMNS.IS_FAV);
  808. item.duration = safeGet(DB_COLUMNS.DURATION);
  809. item.mimeType = safeGet(DB_COLUMNS.MIME_TYPE);
  810. item.trackCount = safeGet(DB_COLUMNS.TRACK_COUNT);
  811. item.sampleRate = safeGet(DB_COLUMNS.SAMPLE_RATE);
  812. item.lastPlayedStr = safeGet(DB_COLUMNS.LAST_PLAYED_STR);
  813. item.playCount = safeGetNumber(DB_COLUMNS.PLAY_COUNT);
  814. item.lyricContent = safeGet(DB_COLUMNS.LYRIC_CONTENT);
  815. item.md5Str = safeGet('md5Str');
  816. item.extra_json = safeGet('extra_json');
  817. item.pyStr = safeGet('pyStr');
  818. item.bit_rate = safeGet('bit_rate');
  819. item.probe_score = safeGetNumber('probe_score');
  820. item.year = safeGet('year');
  821. item.nb_streams = safeGetNumber('nb_streams');
  822. item.nb_programs = safeGetNumber('nb_programs');
  823. item.genre = safeGet('genre');
  824. item.track = safeGet('track');
  825. item.bits_per_raw_sample = safeGet('bits_per_raw_sample');
  826. item.channels = safeGet('channels');
  827. item.channel_layout = safeGet('channel_layout');
  828. item.start_time = safeGet('start_time');
  829. item.ALBUMARTIST = safeGet('ALBUMARTIST');
  830. item.COMPOSER = safeGet('COMPOSER');
  831. item.LYRICIST = safeGet('LYRICIST');
  832. item.COMMENT = safeGet('COMMENT');
  833. item.disc = safeGet('disc');
  834. item.webdav_account_id = safeGet('webdav_account_id');
  835. item.remote_rel_path = safeGet('remote_rel_path');
  836. return item;
  837. }
  838. /**
  839. * 查询播放次数大于0的记录,并按播放次数降序排列
  840. * @param limitCount 限制返回的记录数量,0表示返回全部
  841. * @param callback 回调函数,返回VideoItem数组
  842. */
  843. public queryByPlayCountDesc(limitCount: number = 0, callback: (result: VideoItem[]) => void): void {
  844. try {
  845. // 1. 构建查询条件:playCount > 0,按playCount降序排列
  846. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  847. predicates.greaterThan('playCount', 0); // 播放次数大于0
  848. predicates.orderByDesc('playCount'); // 按播放次数降序排列
  849. // 只有当limitCount大于0时才设置限制
  850. if (limitCount > 0) {
  851. predicates.limitAs(limitCount);
  852. }
  853. // 2. 执行查询
  854. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  855. // 3. 解析结果集
  856. const result = this.parseResultSetToVideoItems(resultSet);
  857. callback(result);
  858. });
  859. } catch (err) {
  860. Logger.error(`queryByPlayCountDesc error: ${err.code} - ${err.message}`);
  861. callback([]); // 发生错误时返回空数组
  862. }
  863. }
  864. public countSongsByQualityOptimized(callback: (result: Record<AudioQuality, number>) => void): void {
  865. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  866. predicates.in('md5Str', [AudioQuality.LQ, AudioQuality.SQ, AudioQuality.HQ, AudioQuality.HIRES, AudioQuality.HR]);
  867. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  868. const result: Record<AudioQuality, number> = {
  869. [AudioQuality.LQ]: 0,
  870. [AudioQuality.SQ]: 0,
  871. [AudioQuality.HQ]: 0,
  872. [AudioQuality.HIRES]: 0 , // 只保留HR,HIRES的统计将合并到这里
  873. [AudioQuality.HR]: 0 // 只保留HR,HIRES的统计将合并到这里
  874. };
  875. if (resultSet.rowCount > 0) {
  876. resultSet.goToFirstRow();
  877. do {
  878. const quality = resultSet.getString(resultSet.getColumnIndex('md5Str')) as AudioQuality;
  879. if (quality === AudioQuality.LQ ||
  880. quality === AudioQuality.SQ ||
  881. quality === AudioQuality.HQ ||
  882. quality === AudioQuality.HIRES || // HIRES的统计将被合并到HR
  883. quality === AudioQuality.HR) {
  884. // 如果质量是HIRES或HR,都统计到HR中
  885. const targetKey = (quality === AudioQuality.HIRES || quality === AudioQuality.HR)
  886. ? AudioQuality.HR
  887. : quality;
  888. result[targetKey]++;
  889. }
  890. } while (resultSet.goToNextRow());
  891. }
  892. resultSet.close();
  893. callback(result);
  894. });
  895. }
  896. /**
  897. * Get lyric content by file path (Promise version)
  898. * @param filePath The file path to query
  899. * @returns Promise that resolves with the lyric content (string) or null if not found
  900. */
  901. public getLyricContentByFilePath(filePath: string): Promise<string | null> {
  902. const normalizedPath = normalizeFilePath(filePath);
  903. return new Promise((resolve, reject) => {
  904. // Create query predicates
  905. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  906. predicates.equalTo(DB_COLUMNS.FILE_PATH, normalizedPath);
  907. // Execute the query
  908. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  909. try {
  910. if (resultSet.rowCount === 0) {
  911. Logger.info(RdbUtils.RDB_TAG, `No record found for filePath: ${normalizedPath}`);
  912. resolve(null);
  913. return;
  914. }
  915. // Get the first row
  916. resultSet.goToFirstRow();
  917. // Get the column index safely
  918. const columnIndex = resultSet.getColumnIndex(DB_COLUMNS.LYRIC_CONTENT);
  919. if (columnIndex < 0) {
  920. Logger.error(RdbUtils.RDB_TAG, `Column ${DB_COLUMNS.LYRIC_CONTENT} not found`);
  921. resolve(null);
  922. return;
  923. }
  924. // Get the lyric content
  925. const lyricContent = resultSet.getString(columnIndex);
  926. resolve(lyricContent || null);
  927. } catch (err) {
  928. Logger.error(RdbUtils.RDB_TAG, `Error getting lyric content: ${err.message}`);
  929. reject(err);
  930. } finally {
  931. // Ensure the result set is closed
  932. resultSet.close();
  933. }
  934. });
  935. });
  936. }
  937. /**
  938. * Check if a record exists in the database by filePath
  939. * @param filePath The file path to check
  940. * @returns Promise that resolves to true if record exists, false otherwise
  941. */
  942. public isRecordExists(filePath: string): Promise<boolean> {
  943. const normalizedPath = normalizeFilePath(filePath);
  944. return new Promise((resolve, reject) => {
  945. try {
  946. // Create query predicates
  947. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  948. predicates.equalTo(DB_COLUMNS.FILE_PATH, normalizedPath);
  949. // Execute the query
  950. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  951. try {
  952. // If rowCount > 0, record exists
  953. resolve(resultSet.rowCount > 0);
  954. } catch (err) {
  955. Logger.error(RdbUtils.RDB_TAG, `Error checking record existence: ${err.message}`);
  956. reject(err);
  957. } finally {
  958. // Ensure the result set is closed
  959. resultSet.close();
  960. }
  961. });
  962. } catch (err) {
  963. Logger.error(RdbUtils.RDB_TAG, `Error creating query: ${err.message}`);
  964. reject(err);
  965. }
  966. });
  967. }
  968. /**
  969. * Query a VideoItem by file path
  970. * @param filePath The file path to query
  971. * @returns Promise that resolves with the VideoItem or null if not found
  972. */
  973. public queryVideoByFilePath(filePath: string): Promise<VideoItem | null> {
  974. const normalizedPath = normalizeFilePath(filePath);
  975. return new Promise((resolve, reject) => {
  976. try {
  977. // Create query predicates
  978. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  979. predicates.equalTo(DB_COLUMNS.FILE_PATH, normalizedPath);
  980. // Execute the query
  981. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  982. try {
  983. if (resultSet.rowCount === 0) {
  984. Logger.info(RdbUtils.RDB_TAG, `No record found for filePath: ${normalizedPath}`);
  985. resolve(null);
  986. return;
  987. }
  988. // Get the first row
  989. if (resultSet.goToFirstRow()) {
  990. const videoItem = this.buildVideoItem(resultSet);
  991. resolve(videoItem);
  992. } else {
  993. resolve(null);
  994. }
  995. } catch (err) {
  996. Logger.error(RdbUtils.RDB_TAG, `Error querying video by filePath: ${err.message}`);
  997. reject(err);
  998. } finally {
  999. // Ensure the result set is closed
  1000. resultSet.close();
  1001. }
  1002. });
  1003. } catch (err) {
  1004. Logger.error(RdbUtils.RDB_TAG, `Error creating query: ${err.message}`);
  1005. reject(err);
  1006. }
  1007. });
  1008. }
  1009. /**
  1010. * Query all video items
  1011. * @returns Promise that resolves with an array of VideoItem objects
  1012. */
  1013. public queryAllVideos(): Promise<VideoItem[]> {
  1014. return new Promise((resolve, reject) => {
  1015. try {
  1016. Logger.info(RdbUtils.RDB_TAG, 'queryAllVideos: 开始查询所有音乐文件');
  1017. // Create query predicates for all records
  1018. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  1019. // Only query music files (type = 0)
  1020. predicates.equalTo(DB_COLUMNS.TYPE, 0);
  1021. // Order by name
  1022. predicates.orderByAsc(DB_COLUMNS.NAME);
  1023. Logger.info(RdbUtils.RDB_TAG, 'queryAllVideos: 创建查询条件完成');
  1024. // Execute the query
  1025. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  1026. try {
  1027. Logger.info(RdbUtils.RDB_TAG, 'queryAllVideos: 查询回调执行');
  1028. if (resultSet.rowCount === 0) {
  1029. Logger.info(RdbUtils.RDB_TAG, 'queryAllVideos: 没有找到音乐文件');
  1030. resolve([]);
  1031. return;
  1032. }
  1033. Logger.info(RdbUtils.RDB_TAG, `queryAllVideos: 找到 ${resultSet.rowCount} 条记录`);
  1034. const items: VideoItem[] = [];
  1035. // Go to first row
  1036. if (resultSet.goToFirstRow()) {
  1037. do {
  1038. const item = this.buildVideoItem(resultSet);
  1039. items.push(item);
  1040. } while (resultSet.goToNextRow());
  1041. }
  1042. Logger.info(RdbUtils.RDB_TAG, `queryAllVideos: 解析完成,返回 ${items.length} 个项目`);
  1043. resolve(items);
  1044. } catch (err) {
  1045. Logger.error(RdbUtils.RDB_TAG, `queryAllVideos: 解析结果集出错: ${err.message}`);
  1046. Logger.error(RdbUtils.RDB_TAG, `queryAllVideos: 错误堆栈: ${err.stack || '无堆栈信息'}`);
  1047. reject(err);
  1048. }
  1049. // 注意:不在这里关闭 resultSet,因为 RdbUtils.query 会在回调函数执行后关闭它
  1050. });
  1051. } catch (err) {
  1052. Logger.error(RdbUtils.RDB_TAG, `queryAllVideos: 创建查询出错: ${err.message}`);
  1053. Logger.error(RdbUtils.RDB_TAG, `queryAllVideos: 错误堆栈: ${err.stack || '无堆栈信息'}`);
  1054. reject(err);
  1055. }
  1056. });
  1057. }
  1058. /**
  1059. * 从数据库随机获取一首歌曲(可选条件:类型、排除路径、文件夹、歌单)
  1060. */
  1061. public async queryRandomSong(options?: RandomSongQueryOptions): Promise<VideoItem | null> {
  1062. try {
  1063. // 优先处理歌单模式
  1064. if (options?.playlistSongFilePaths && options.playlistSongFilePaths.length > 0) {
  1065. Logger.info(RdbUtils.RDB_TAG, `queryRandomSong: 歌单模式,歌曲数量=${options.playlistSongFilePaths.length}`);
  1066. // 过滤掉排除的路径
  1067. let availablePaths = options.playlistSongFilePaths;
  1068. if (options.excludeFilePaths && options.excludeFilePaths.length > 0) {
  1069. const excludeSet = new Set(options.excludeFilePaths);
  1070. availablePaths = availablePaths.filter(path => !excludeSet.has(path));
  1071. }
  1072. if (availablePaths.length === 0) {
  1073. Logger.warn(RdbUtils.RDB_TAG, 'queryRandomSong: 歌单中没有可用歌曲');
  1074. return null;
  1075. }
  1076. // 随机选择一个路径
  1077. const randomPath = availablePaths[Math.floor(Math.random() * availablePaths.length)];
  1078. return await this.queryVideoByFilePath(randomPath);
  1079. }
  1080. // 文件夹模式或全局模式
  1081. const conditions: string[] = [];
  1082. const params: Array<string | number> = [];
  1083. if (options?.type !== undefined) {
  1084. conditions.push(`${DB_COLUMNS.TYPE} = ?`);
  1085. params.push(options.type);
  1086. }
  1087. // 文件夹条件
  1088. if (options?.parentPath) {
  1089. conditions.push(`${DB_COLUMNS.PARENT_PATH} = ?`);
  1090. params.push(options.parentPath);
  1091. }
  1092. // 排除路径条件
  1093. if (options?.excludeFilePaths && options.excludeFilePaths.length > 0) {
  1094. const normalizedPaths = options.excludeFilePaths.map(path => normalizeFilePath(path));
  1095. const placeholders = normalizedPaths.map(() => '?').join(', ');
  1096. conditions.push(`${DB_COLUMNS.FILE_PATH} NOT IN (${placeholders})`);
  1097. params.push(...normalizedPaths);
  1098. }
  1099. let sql = `SELECT * FROM ${RdbUtils.MEDIA_TABLE.tableName}`;
  1100. if (conditions.length > 0) {
  1101. sql += ` WHERE ${conditions.join(' AND ')}`;
  1102. }
  1103. sql += ' ORDER BY RANDOM() LIMIT 1';
  1104. Logger.info(RdbUtils.RDB_TAG, `queryRandomSong: SQL=${sql}, params=${JSON.stringify(params)}`);
  1105. const resultSet = await this.accountTable.querySql(sql, params);
  1106. try {
  1107. if (resultSet.rowCount === 0) {
  1108. return null;
  1109. }
  1110. resultSet.goToFirstRow();
  1111. return this.buildVideoItem(resultSet);
  1112. } finally {
  1113. resultSet.close();
  1114. }
  1115. } catch (error) {
  1116. Logger.error(RdbUtils.RDB_TAG, `queryRandomSong failed: ${(error as Error).message}`);
  1117. return null;
  1118. }
  1119. }
  1120. /**
  1121. * 应用搜索条件到查询谓词
  1122. * 支持对 name、artist、album、fileName 多字段模糊搜索
  1123. */
  1124. private applySearchConditions(predicates: relationalStore.RdbPredicates, searchKeyword: string): void {
  1125. if (searchKeyword && searchKeyword.trim().length > 0) {
  1126. const keyword = `%${searchKeyword.trim()}%`;
  1127. predicates.beginWrap();
  1128. predicates.like(DB_COLUMNS.NAME, keyword);
  1129. predicates.or();
  1130. predicates.like(DB_COLUMNS.ARTIST, keyword);
  1131. predicates.or();
  1132. predicates.like(DB_COLUMNS.ALBUM, keyword);
  1133. predicates.or();
  1134. predicates.like(DB_COLUMNS.FILE_NAME, keyword);
  1135. predicates.endWrap();
  1136. }
  1137. }
  1138. /**
  1139. * 应用排序条件到查询谓词
  1140. * sortType: 0-艺术家升序 1-艺术家降序 2-专辑升序 3-专辑降序 4-名称升序 5-名称降序 6-时间升序 7-时间降序
  1141. * 10-碟片编号升序+音轨号升序(用于专辑和文件夹)
  1142. * @param predicates 查询谓词
  1143. * @param sortType 排序类型
  1144. * @param isShowFileName 是否显示文件名(当sortType为4或5时,此参数决定按name还是fileName排序)
  1145. */
  1146. private applySortConditions(predicates: relationalStore.RdbPredicates, sortType?: number, isShowFileName?: boolean): void {
  1147. const type = sortType !== undefined ? sortType : 4;
  1148. // 当按名称排序时,根据isShowFileName决定使用name还是fileName字段
  1149. let nameColumn = DB_COLUMNS.NAME;
  1150. if ((type === 4 || type === 5) && isShowFileName === true) {
  1151. nameColumn = DB_COLUMNS.FILE_NAME;
  1152. }
  1153. // sortType=10 或 11 需要特殊处理:按碟片编号+音轨号排序
  1154. // 由于RdbPredicates不支持多字段排序,我们需要标记这个谓词需要使用原生SQL
  1155. if (type === 10) {
  1156. Logger.info(RdbUtils.RDB_TAG, 'applySortConditions: 碟片-音轨升序排序,需要使用内存排序');
  1157. predicates.orderByAsc('disc'); // 先按碟片编号排序
  1158. // 音轨号排序需要在查询后进行内存排序
  1159. return;
  1160. }
  1161. if (type === 11) {
  1162. Logger.info(RdbUtils.RDB_TAG, 'applySortConditions: 碟片-音轨降序排序,需要使用内存排序');
  1163. predicates.orderByDesc('disc'); // 先按碟片编号降序排序
  1164. // 音轨号排序需要在查询后进行内存排序
  1165. return;
  1166. }
  1167. switch (type) {
  1168. case 0:
  1169. predicates.orderByAsc(DB_COLUMNS.ARTIST);
  1170. break;
  1171. case 1:
  1172. predicates.orderByDesc(DB_COLUMNS.ARTIST);
  1173. break;
  1174. case 2:
  1175. predicates.orderByAsc(DB_COLUMNS.ALBUM);
  1176. break;
  1177. case 3:
  1178. predicates.orderByDesc(DB_COLUMNS.ALBUM);
  1179. break;
  1180. case 4:
  1181. predicates.orderByAsc(nameColumn);
  1182. break;
  1183. case 5:
  1184. predicates.orderByDesc(nameColumn);
  1185. break;
  1186. case 6:
  1187. predicates.orderByAsc(DB_COLUMNS.C_TIME);
  1188. break;
  1189. case 7:
  1190. predicates.orderByDesc(DB_COLUMNS.C_TIME);
  1191. break;
  1192. default:
  1193. predicates.orderByAsc(nameColumn);
  1194. break;
  1195. }
  1196. }
  1197. /**
  1198. * 获取总记录数(用于计算分页)
  1199. */
  1200. private async getTotalCount(predicates: relationalStore.RdbPredicates): Promise<number> {
  1201. return new Promise((resolve, reject) => {
  1202. this.accountTable.query(predicates, (resultSet: relationalStore.ResultSet) => {
  1203. try {
  1204. const count = resultSet.rowCount;
  1205. resolve(count);
  1206. } catch (err) {
  1207. const error = err as Error;
  1208. Logger.error(RdbUtils.RDB_TAG, `getTotalCount error: ${error.message}`);
  1209. reject(err);
  1210. } finally {
  1211. resultSet.close();
  1212. }
  1213. });
  1214. });
  1215. }
  1216. /**
  1217. * 首页文件夹内容分页查询(modeType=0)
  1218. * 注意:此方法只返回数据库中的文件记录,不包含文件夹
  1219. * 文件夹需要通过文件系统API单独获取
  1220. */
  1221. public async queryByParentPathPaged(options: PageQueryOptions): Promise<PageQueryResult> {
  1222. try {
  1223. Logger.info('heanup MediaTable', `queryByParentPathPaged: 开始分页查询 parentPath=${options.parentPath}, page=${options.pageIndex}, size=${options.pageSize}`);
  1224. // 1. 构建基础查询条件
  1225. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  1226. if (options.parentPath) {
  1227. predicates.equalTo(DB_COLUMNS.PARENT_PATH, options.parentPath);
  1228. }
  1229. // 2. 应用搜索条件
  1230. if (options.searchKeyword) {
  1231. this.applySearchConditions(predicates, options.searchKeyword);
  1232. }
  1233. // 3. 先获取总数
  1234. const totalCount = await this.getTotalCount(predicates);
  1235. // 4. 构建分页查询条件
  1236. const pagePredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  1237. if (options.parentPath) {
  1238. pagePredicates.equalTo(DB_COLUMNS.PARENT_PATH, options.parentPath);
  1239. }
  1240. if (options.searchKeyword) {
  1241. this.applySearchConditions(pagePredicates, options.searchKeyword);
  1242. }
  1243. // 5. 应用排序
  1244. this.applySortConditions(pagePredicates, options.sortType, options.isShowFileName);
  1245. // 6. 应用分页
  1246. const offset = options.pageIndex * options.pageSize;
  1247. pagePredicates.limitAs(options.pageSize);
  1248. pagePredicates.offsetAs(offset);
  1249. // 7. 执行查询
  1250. return new Promise((resolve, reject) => {
  1251. this.accountTable.query(pagePredicates, (resultSet: relationalStore.ResultSet) => {
  1252. try {
  1253. const items = this.parseResultSetToVideoItems(resultSet, options.sortType);
  1254. const hasMore = (offset + items.length) < totalCount;
  1255. Logger.info('heanup MediaTable', `queryByParentPathPaged: 查询完成 items=${items.length}, total=${totalCount}, hasMore=${hasMore}`);
  1256. resolve({ items, totalCount, hasMore });
  1257. } catch (err) {
  1258. const error = err as Error;
  1259. Logger.error('heanup MediaTable', `queryByParentPathPaged error: ${error.message}`);
  1260. reject(err);
  1261. }
  1262. });
  1263. });
  1264. } catch (err) {
  1265. const error = err as Error;
  1266. Logger.error('heanup MediaTable', `queryByParentPathPaged failed: ${error.message}`);
  1267. throw error;
  1268. }
  1269. }
  1270. /**
  1271. * 媒体库分页查询(modeType=1)
  1272. */
  1273. public async queryMediaLibraryPaged(options: PageQueryOptions): Promise<PageQueryResult> {
  1274. try {
  1275. Logger.info('heanup MediaTable', `queryMediaLibraryPaged: 开始分页查询 page=${options.pageIndex}, size=${options.pageSize}`);
  1276. // 1. 构建基础查询条件 - 查询所有type=0的音乐文件
  1277. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  1278. predicates.equalTo(DB_COLUMNS.TYPE, 0);
  1279. // 2. 应用搜索条件
  1280. if (options.searchKeyword) {
  1281. this.applySearchConditions(predicates, options.searchKeyword);
  1282. }
  1283. // 3. 先获取总数
  1284. const totalCount = await this.getTotalCount(predicates);
  1285. // 4. 构建分页查询条件
  1286. const pagePredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  1287. pagePredicates.equalTo(DB_COLUMNS.TYPE, 0);
  1288. if (options.searchKeyword) {
  1289. this.applySearchConditions(pagePredicates, options.searchKeyword);
  1290. }
  1291. // 5. 应用排序
  1292. this.applySortConditions(pagePredicates, options.sortType, options.isShowFileName);
  1293. // 6. 应用分页
  1294. const offset = options.pageIndex * options.pageSize;
  1295. pagePredicates.limitAs(options.pageSize);
  1296. pagePredicates.offsetAs(offset);
  1297. // 7. 执行查询
  1298. return new Promise((resolve, reject) => {
  1299. this.accountTable.query(pagePredicates, (resultSet: relationalStore.ResultSet) => {
  1300. try {
  1301. const items = this.parseResultSetToVideoItems(resultSet, options.sortType);
  1302. const hasMore = (offset + items.length) < totalCount;
  1303. Logger.info('heanup MediaTable', `queryMediaLibraryPaged: 查询完成 items=${items.length}, total=${totalCount}, hasMore=${hasMore}`);
  1304. resolve({ items, totalCount, hasMore });
  1305. } catch (err) {
  1306. const error = err as Error;
  1307. Logger.error('heanup MediaTable', `queryMediaLibraryPaged error: ${error.message}`);
  1308. reject(err);
  1309. }
  1310. });
  1311. });
  1312. } catch (err) {
  1313. const error = err as Error;
  1314. Logger.error('heanup MediaTable', `queryMediaLibraryPaged failed: ${error.message}`);
  1315. throw error;
  1316. }
  1317. }
  1318. /**
  1319. * 艺术家维度分页查询(modeType=2)
  1320. * 返回艺术家封面列表的代表性歌曲
  1321. */
  1322. public async queryArtistsPaged(options: PageQueryOptions): Promise<PageQueryResult> {
  1323. return await this.queryGroupedPaged(DB_COLUMNS.ARTIST, CommonConstants.TYPE_IS_ARTIST, options);
  1324. }
  1325. /**
  1326. * 专辑维度分页查询(modeType=3)
  1327. * 返回专辑封面列表的代表性歌曲
  1328. */
  1329. public async queryAlbumsPaged(options: PageQueryOptions): Promise<PageQueryResult> {
  1330. return await this.queryGroupedPaged(DB_COLUMNS.ALBUM, CommonConstants.TYPE_IS_ALBUM, options);
  1331. }
  1332. private async queryGroupedPaged(columnName: string, itemType: number, options: PageQueryOptions): Promise<PageQueryResult> {
  1333. try {
  1334. const keyword = options.searchKeyword?.trim();
  1335. const tableName = RdbUtils.MEDIA_TABLE.tableName;
  1336. const whereClauses = [
  1337. `${columnName} IS NOT NULL`,
  1338. `${columnName} != ''`,
  1339. `${DB_COLUMNS.TYPE} = ?`
  1340. ];
  1341. const params: Array<string | number> = [CommonConstants.TYPE_LOCAL];
  1342. if (keyword) {
  1343. whereClauses.push(`${columnName} LIKE ?`);
  1344. params.push(`%${keyword}%`);
  1345. }
  1346. const whereSql = whereClauses.join(' AND ');
  1347. // 使用TRIM函数去除空格,合并相同艺术家/专辑名的数据
  1348. const totalSql = `SELECT COUNT(DISTINCT TRIM(${columnName})) AS totalCount FROM ${tableName} WHERE ${whereSql}`;
  1349. const totalResultSet = await this.accountTable.querySql(totalSql, params);
  1350. let totalCount = 0;
  1351. if (totalResultSet.goToFirstRow()) {
  1352. totalCount = totalResultSet.getLong(totalResultSet.getColumnIndex('totalCount'));
  1353. }
  1354. totalResultSet.close();
  1355. const sortType = options.sortType ?? 9;
  1356. let orderBy = `name ASC`;
  1357. if (sortType === 5) {
  1358. orderBy = `name DESC`;
  1359. } else if (sortType === 8) {
  1360. orderBy = `songCount ASC, name ASC`;
  1361. } else if (sortType === 9) {
  1362. orderBy = `songCount DESC, name ASC`;
  1363. }
  1364. const offset = options.pageIndex * options.pageSize;
  1365. const pageSql =
  1366. `SELECT TRIM(${columnName}) AS name, COUNT(*) AS songCount, ` +
  1367. `MAX(${DB_COLUMNS.PIXEL_MAP_PATH}) AS coverPath ` +
  1368. `FROM ${tableName} WHERE ${whereSql} ` +
  1369. `GROUP BY TRIM(${columnName}) ` +
  1370. `HAVING COUNT(*) > 0 ` +
  1371. `ORDER BY ${orderBy} ` +
  1372. `LIMIT ? OFFSET ?`;
  1373. const pageParams = [...params, options.pageSize, offset];
  1374. const resultSet = await this.accountTable.querySql(pageSql, pageParams);
  1375. const items: VideoItem[] = [];
  1376. const countMap = new Map<string, number>();
  1377. if (resultSet.rowCount > 0) {
  1378. resultSet.goToFirstRow();
  1379. for (let i = 0; i < resultSet.rowCount; i++) {
  1380. const name = resultSet.getString(resultSet.getColumnIndex('name')) || '';
  1381. const songCount = resultSet.getLong(resultSet.getColumnIndex('songCount'));
  1382. const coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath')) || '';
  1383. countMap.set(name, songCount);
  1384. const item = new VideoItem(name, name, name, itemType, 0, '');
  1385. item.pixelMapPath = coverPath;
  1386. if (itemType === CommonConstants.TYPE_IS_ARTIST) {
  1387. item.artist = name;
  1388. } else {
  1389. item.album = name;
  1390. }
  1391. items.push(item);
  1392. if (i < resultSet.rowCount - 1) {
  1393. resultSet.goToNextRow();
  1394. }
  1395. }
  1396. }
  1397. resultSet.close();
  1398. const hasMore = (offset + items.length) < totalCount;
  1399. Logger.info('heanup MediaTable', `queryGroupedPaged: 查询完成 items=${items.length}, total=${totalCount}, hasMore=${hasMore}`);
  1400. return { items, totalCount, hasMore, countMap };
  1401. } catch (err) {
  1402. const error = err as Error;
  1403. Logger.error('heanup MediaTable', `queryGroupedPaged failed: ${error.message}`);
  1404. throw error;
  1405. }
  1406. }
  1407. private async getCountByColumn(columnName: string, value: string): Promise<number> {
  1408. const predicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  1409. predicates.equalTo(columnName, value);
  1410. predicates.equalTo(DB_COLUMNS.TYPE, CommonConstants.TYPE_LOCAL);
  1411. return await this.getTotalCount(predicates);
  1412. }
  1413. /**
  1414. * 更新歌曲的webdav_account_id
  1415. * @param filePath 歌曲文件路径
  1416. * @param accountId 新的账号ID
  1417. * @returns Promise<boolean> 更新是否成功
  1418. */
  1419. public async updateWebDavAccountId(filePath: string, accountId: string): Promise<boolean> {
  1420. const normalizedPath = normalizeFilePath(filePath);
  1421. return new Promise<boolean>((resolve, reject) => {
  1422. try {
  1423. // 先查询记录是否存在
  1424. const queryPredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  1425. queryPredicates.equalTo(DB_COLUMNS.FILE_PATH, normalizedPath);
  1426. this.accountTable.query(queryPredicates, (resultSet: relationalStore.ResultSet) => {
  1427. try {
  1428. if (resultSet.rowCount === 0) {
  1429. Logger.warn(RdbUtils.RDB_TAG, `updateWebDavAccountId: 未找到记录 ${normalizedPath}`);
  1430. resolve(false);
  1431. resultSet.close();
  1432. return;
  1433. }
  1434. resultSet.close();
  1435. // 更新webdav_account_id
  1436. const updatePredicates = new relationalStore.RdbPredicates(RdbUtils.MEDIA_TABLE.tableName);
  1437. updatePredicates.equalTo(DB_COLUMNS.FILE_PATH, normalizedPath);
  1438. const valueBucket: relationalStore.ValuesBucket = {
  1439. webdav_account_id: accountId
  1440. };
  1441. this.accountTable.updateData(updatePredicates, valueBucket, (success: boolean) => {
  1442. if (success) {
  1443. Logger.info(RdbUtils.RDB_TAG, `updateWebDavAccountId: 成功更新 ${normalizedPath} -> ${accountId}`);
  1444. } else {
  1445. Logger.error(RdbUtils.RDB_TAG, `updateWebDavAccountId: 更新失败 ${normalizedPath}`);
  1446. }
  1447. resolve(success);
  1448. });
  1449. } catch (err) {
  1450. Logger.error(RdbUtils.RDB_TAG, `updateWebDavAccountId error: ${(err as Error).message}`);
  1451. reject(err);
  1452. }
  1453. });
  1454. } catch (err) {
  1455. Logger.error(RdbUtils.RDB_TAG, `updateWebDavAccountId error: ${(err as Error).message}`);
  1456. reject(err);
  1457. }
  1458. });
  1459. }
  1460. }
  1461. function generateBucket(item: VideoItem): relationalStore.ValuesBucket {
  1462. let obj: relationalStore.ValuesBucket = {};
  1463. obj.id = item.id
  1464. obj.name = item.name;
  1465. const normalizedPath = normalizeFilePath(item.filePath);
  1466. obj.filePath = normalizedPath;
  1467. obj.mtype = item.type;
  1468. obj.videoSize = item.videoSize;
  1469. obj.cTime = item.cTime;
  1470. obj.parentPath = item.parentPath;
  1471. obj.isFav = item.isFav;
  1472. // if(item.pixelMapToString){
  1473. // obj.pixelMapToString = item.pixelMapToString;
  1474. // }
  1475. if(item.artist){
  1476. obj.artist = item.artist;
  1477. }
  1478. if(item.album){
  1479. obj.album = item.album;
  1480. }
  1481. if(item.fileName){
  1482. obj.fileName = item.fileName;
  1483. }
  1484. if(item.size){
  1485. obj.size = item.size;
  1486. }
  1487. if(item.pixelMapPath){
  1488. obj.pixelMapPath = item.pixelMapPath;
  1489. }
  1490. if(item.duration){
  1491. obj.duration = item.duration;
  1492. }
  1493. if(item.mimeType){
  1494. obj.mimeType = item.mimeType;
  1495. }
  1496. if(item.trackCount){
  1497. obj.trackCount = item.trackCount;
  1498. }
  1499. if(item.sampleRate){
  1500. obj.sampleRate = item.sampleRate;
  1501. }
  1502. if(item.lastPlayedStr){
  1503. obj.lastPlayedStr = item.lastPlayedStr;
  1504. }
  1505. if(item.playCount){
  1506. obj.playCount = item.playCount;
  1507. }
  1508. if(item.lyricContent){
  1509. obj.lyricContent = item.lyricContent;
  1510. }
  1511. if(item.md5Str){
  1512. obj.md5Str = item.md5Str;
  1513. }
  1514. if(item.extra_json){
  1515. obj.extra_json = item.extra_json;
  1516. }
  1517. if(item.pyStr){
  1518. obj.pyStr = item.pyStr;
  1519. }
  1520. if(item.bit_rate){
  1521. obj.bit_rate = item.bit_rate;
  1522. }
  1523. if(item.probe_score){
  1524. obj.probe_score = item.probe_score;
  1525. }
  1526. if(item.year){
  1527. obj.year = item.year;
  1528. }
  1529. if(item.nb_streams){
  1530. obj.nb_streams = item.nb_streams;
  1531. }
  1532. if(item.nb_programs){
  1533. obj.nb_programs = item.nb_programs;
  1534. }
  1535. if(item.genre){
  1536. obj.genre = item.genre;
  1537. }
  1538. if(item.track){
  1539. obj.track = item.track;
  1540. }
  1541. if(item.bits_per_raw_sample){
  1542. obj.bits_per_raw_sample = item.bits_per_raw_sample;
  1543. }
  1544. if(item.channels){
  1545. obj.channels = item.channels;
  1546. }
  1547. if(item.channel_layout){
  1548. obj.channel_layout = item.channel_layout;
  1549. }
  1550. if(item.start_time){
  1551. obj.start_time = item.start_time;
  1552. }
  1553. if(item.ALBUMARTIST){
  1554. obj.ALBUMARTIST = item.ALBUMARTIST;
  1555. }
  1556. if(item.COMPOSER){
  1557. obj.COMPOSER = item.COMPOSER;
  1558. }
  1559. if(item.LYRICIST){
  1560. obj.LYRICIST = item.LYRICIST;
  1561. }
  1562. if(item.COMMENT){
  1563. obj.COMMENT = item.COMMENT;
  1564. }
  1565. if(item.disc){
  1566. obj.disc = item.disc;
  1567. }
  1568. if(item.webdav_account_id){
  1569. obj.webdav_account_id = item.webdav_account_id;
  1570. }
  1571. if(item.remote_rel_path){
  1572. obj.remote_rel_path = item.remote_rel_path;
  1573. }
  1574. return obj;
  1575. }