MediaTable.ets 58 KB

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