MusicCardSnapshot.ets 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import { MUSIC_CARD_EMPTY_ARTIST, MUSIC_CARD_EMPTY_TITLE } from './MusicCardConstants'
  2. const DEFAULT_TIME_TEXT = '00:00'
  3. const LYRIC_TIMESTAMP_PATTERN = /\[(\d{2}):(\d{2})(?:\.(\d{2,3}))?\]/g
  4. const isNonEmpty = (value?: string): boolean => {
  5. return !!value && value.trim() !== ''
  6. }
  7. export interface MusicCardSnapshot {
  8. title: string
  9. artist: string
  10. coverPath: string
  11. hasCoverImage: boolean
  12. hasSong: boolean
  13. isPlaying: boolean
  14. currentPositionMs: number
  15. durationMs: number
  16. currentTimeText: string
  17. durationTimeText: string
  18. lyricLine1: string
  19. lyricLine2: string
  20. hasLyric: boolean
  21. filePath: string
  22. updatedAtMs: number
  23. lyricText: string
  24. }
  25. export interface MusicCardLyricLines {
  26. line1: string
  27. line2: string
  28. hasLyric: boolean
  29. }
  30. export class MusicCardBindingData {
  31. title: string = MUSIC_CARD_EMPTY_TITLE
  32. artist: string = MUSIC_CARD_EMPTY_ARTIST
  33. coverPath: string = ''
  34. hasCoverImage: boolean = false
  35. hasSong: boolean = false
  36. isPlaying: boolean = false
  37. currentPositionMs: number = 0
  38. durationMs: number = 0
  39. currentTimeText: string = DEFAULT_TIME_TEXT
  40. durationTimeText: string = DEFAULT_TIME_TEXT
  41. lyricLine1: string = MUSIC_CARD_EMPTY_TITLE
  42. lyricLine2: string = MUSIC_CARD_EMPTY_ARTIST
  43. hasLyric: boolean = false
  44. filePath: string = ''
  45. updatedAtMs: number = 0
  46. }
  47. export function createEmptyMusicCardSnapshot(): MusicCardSnapshot {
  48. return {
  49. title: '',
  50. artist: '',
  51. coverPath: '',
  52. hasCoverImage: false,
  53. hasSong: false,
  54. isPlaying: false,
  55. currentPositionMs: 0,
  56. durationMs: 0,
  57. currentTimeText: DEFAULT_TIME_TEXT,
  58. durationTimeText: DEFAULT_TIME_TEXT,
  59. lyricLine1: MUSIC_CARD_EMPTY_TITLE,
  60. lyricLine2: MUSIC_CARD_EMPTY_ARTIST,
  61. hasLyric: false,
  62. filePath: '',
  63. updatedAtMs: 0,
  64. lyricText: ''
  65. }
  66. }
  67. export function resolveMusicCardLyricLines(
  68. lyricText: string,
  69. positionMs: number
  70. ): MusicCardLyricLines {
  71. const trimmedText = lyricText ? lyricText.trim() : ''
  72. if (trimmedText === '') {
  73. return {
  74. line1: '',
  75. line2: '',
  76. hasLyric: false
  77. }
  78. }
  79. const entries: Array<{ timeMs: number; text: string }> = []
  80. const rawLines = trimmedText.split(/\r?\n/)
  81. for (let i = 0; i < rawLines.length; i++) {
  82. const rawLine = rawLines[i].trim()
  83. if (rawLine === '') {
  84. continue
  85. }
  86. const timestamps: Array<{ timeMs: number }> = []
  87. let lastMatchEnd = 0
  88. LYRIC_TIMESTAMP_PATTERN.lastIndex = 0
  89. let match = LYRIC_TIMESTAMP_PATTERN.exec(rawLine)
  90. while (match) {
  91. const minutes = parseInt(match[1], 10)
  92. const seconds = parseInt(match[2], 10)
  93. const msPart = match[3]
  94. let msValue = 0
  95. if (msPart) {
  96. msValue = msPart.length === 2 ? parseInt(msPart, 10) * 10 : parseInt(msPart, 10)
  97. }
  98. const timeMs = minutes * 60 * 1000 + seconds * 1000 + msValue
  99. timestamps.push({ timeMs })
  100. lastMatchEnd = LYRIC_TIMESTAMP_PATTERN.lastIndex
  101. match = LYRIC_TIMESTAMP_PATTERN.exec(rawLine)
  102. }
  103. if (timestamps.length === 0) {
  104. continue
  105. }
  106. const text = rawLine.slice(lastMatchEnd).trim()
  107. if (text === '') {
  108. continue
  109. }
  110. for (let j = 0; j < timestamps.length; j++) {
  111. entries.push({
  112. timeMs: timestamps[j].timeMs,
  113. text
  114. })
  115. }
  116. }
  117. if (entries.length === 0) {
  118. return {
  119. line1: '',
  120. line2: '',
  121. hasLyric: false
  122. }
  123. }
  124. entries.sort((lhs, rhs) => lhs.timeMs - rhs.timeMs)
  125. let currentIndex = -1
  126. for (let i = 0; i < entries.length; i++) {
  127. if (entries[i].timeMs <= positionMs) {
  128. currentIndex = i
  129. } else {
  130. break
  131. }
  132. }
  133. if (currentIndex === -1) {
  134. currentIndex = 0
  135. }
  136. const currentLine = entries[currentIndex]
  137. const nextLine = entries[currentIndex + 1]
  138. return {
  139. line1: currentLine.text,
  140. line2: nextLine ? nextLine.text : '',
  141. hasLyric: true
  142. }
  143. }
  144. export function buildMusicCardBindingData(snapshot: MusicCardSnapshot): MusicCardBindingData {
  145. const source = snapshot || createEmptyMusicCardSnapshot()
  146. const lyricLines = resolveMusicCardLyricLines(source.lyricText, source.currentPositionMs)
  147. const data = new MusicCardBindingData()
  148. data.hasSong = Boolean(source.hasSong)
  149. data.title =
  150. data.hasSong && isNonEmpty(source.title) ? source.title : MUSIC_CARD_EMPTY_TITLE
  151. data.artist =
  152. data.hasSong && isNonEmpty(source.artist) ? source.artist : MUSIC_CARD_EMPTY_ARTIST
  153. data.coverPath = source.coverPath ?? ''
  154. data.hasCoverImage = Boolean(source.hasCoverImage)
  155. data.isPlaying = Boolean(source.isPlaying)
  156. data.currentPositionMs = source.currentPositionMs ?? 0
  157. data.durationMs = source.durationMs ?? 0
  158. data.currentTimeText = isNonEmpty(source.currentTimeText)
  159. ? source.currentTimeText
  160. : DEFAULT_TIME_TEXT
  161. data.durationTimeText = isNonEmpty(source.durationTimeText)
  162. ? source.durationTimeText
  163. : DEFAULT_TIME_TEXT
  164. data.filePath = source.filePath ?? ''
  165. data.updatedAtMs = source.updatedAtMs ?? 0
  166. let lyricLine1 = data.title
  167. let lyricLine2 = data.artist
  168. let hasLyric = false
  169. if (lyricLines.hasLyric) {
  170. lyricLine1 = lyricLines.line1
  171. lyricLine2 = lyricLines.line2
  172. hasLyric = true
  173. } else if (
  174. source.hasLyric &&
  175. (isNonEmpty(source.lyricLine1) || isNonEmpty(source.lyricLine2))
  176. ) {
  177. lyricLine1 = isNonEmpty(source.lyricLine1) ? source.lyricLine1 : data.title
  178. lyricLine2 = isNonEmpty(source.lyricLine2) ? source.lyricLine2 : data.artist
  179. hasLyric = true
  180. }
  181. data.lyricLine1 = lyricLine1
  182. data.lyricLine2 = lyricLine2
  183. data.hasLyric = hasLyric
  184. return data
  185. }