util.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import dotProp from 'dot-prop';
  2. import chalk from 'chalk';
  3. import {dirname, basename, join} from 'path';
  4. type saveLayoutProps = {
  5. track: {[key: string]: any};
  6. album: {[key: string]: any};
  7. path: string;
  8. minimumIntegerDigits: number;
  9. trackNumber: boolean;
  10. };
  11. export const sanitizeFilename = (input: string, replacement = '_'): string => {
  12. /* eslint-disable-next-line */
  13. const UNSAFE_CHARS = /[\/\?<>\\:\*\|"]/g;
  14. if (typeof input !== 'string') {
  15. return '';
  16. }
  17. if (process.platform === 'win32' && input.endsWith('.')) {
  18. return (input.slice(0, -1) + replacement).replace(UNSAFE_CHARS, replacement).trim();
  19. }
  20. return input.replace(UNSAFE_CHARS, replacement).trim();
  21. };
  22. export const formatSecondsReadable = (time: number) => {
  23. if (time < 60) {
  24. return time + 's';
  25. }
  26. const minutes = time >= 60 ? Math.floor(time / 60) : 0;
  27. const seconds = Math.floor(time - minutes * 60);
  28. return `${minutes >= 10 ? minutes : '0' + minutes}m ${seconds >= 10 ? seconds : '0' + seconds}s`;
  29. };
  30. export const saveLayout = ({track, album, path, minimumIntegerDigits, trackNumber}: saveLayoutProps) => {
  31. // Clone album info
  32. const albumInfo = {...album};
  33. // Use relative path
  34. if (path.startsWith('{')) {
  35. path = './' + path;
  36. }
  37. // Transform values
  38. /* eslint-disable-next-line */
  39. const file = path.match(/(?<=\{)[^\}]*/g);
  40. if (file) {
  41. if (
  42. track.DISK_NUMBER &&
  43. album.NUMBER_DISK &&
  44. album.ALB_TITLE &&
  45. Number(album.NUMBER_DISK) > 1 &&
  46. !album.ALB_TITLE.includes('Disc')
  47. ) {
  48. albumInfo.ALB_TITLE += ` (Disc ${Number(track.DISK_NUMBER).toLocaleString('en-US', {minimumIntegerDigits: 2})})`;
  49. }
  50. for (const key of file) {
  51. const value_album: string | undefined = dotProp.get(albumInfo, key);
  52. const value_track: string | undefined = value_album || dotProp.get(track, key);
  53. if (key === 'TRACK_NUMBER' || key === 'TRACK_POSITION' || key === 'NO_TRACK_NUMBER') {
  54. path = path.replace(
  55. `{${key}}`,
  56. value_track ? Number(value_track).toLocaleString('en-US', {minimumIntegerDigits}) : '',
  57. );
  58. trackNumber = false;
  59. } else {
  60. path = path.replace(`{${key}}`, value_track ? sanitizeFilename(value_track) : '');
  61. }
  62. }
  63. }
  64. if (trackNumber && (track.TRACK_NUMBER || track.TRACK_POSITION)) {
  65. const [dir, base] = [dirname(path), basename(path)];
  66. const position = track.TRACK_POSITION ? track.TRACK_POSITION : Number(track.TRACK_NUMBER);
  67. path = join(dir, position.toLocaleString('en-US', {minimumIntegerDigits}) + ' - ' + base);
  68. } else {
  69. path = join(path);
  70. }
  71. return path.replace(/[?%*|"<>]/g, '').trim();
  72. };
  73. export const progressBar = (total: number, width: number) => {
  74. const incomplete = Array(width).fill('█').join('');
  75. const complete = Array(width).fill('█').join('');
  76. const unit = total / width;
  77. return (value: number) => {
  78. let chars = unit === 0 ? width : Math.floor(value / unit);
  79. if (value >= total) {
  80. chars = complete.length;
  81. }
  82. return chalk.cyanBright(complete.slice(0, chars)) + chalk.gray(incomplete.slice(chars));
  83. };
  84. };
  85. export const commonPath = (paths: string[]) => {
  86. const A = paths.concat().sort(),
  87. a1 = A[0],
  88. a2 = A[A.length - 1],
  89. L = a1.length;
  90. let i = 0;
  91. while (i < L && a1.charAt(i) === a2.charAt(i)) i++;
  92. return a1.substring(0, i);
  93. };