MediaTable.ets 65 KB

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