kashi 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@ This project is a dependency-free library that aims to provide a way to correctl
19
19
  - [x] 💪 Supports both directly inputting the .lrc file and a URL that returns it
20
20
  - [x] ✉️ Implements the Observer pattern and emits events at each step of the process whenever something changes
21
21
  - [x] ✏️ Allows you to enter custom text when the lyrics line is empty
22
- - [ ] 🎤 Synchronizes the lyrics with the music that is playing
22
+ - [x] 🎤 Synchronizes the lyrics with the music that is playing
23
23
  - [ ] 🎩 Supports multiple lyrics for the same song (useful to keep track of the original lyrics and their translation)
24
24
  - [ ] 🧞 Supports the [Walaoke extension](https://en.wikipedia.org/wiki/LRC_(file_format)#Walaoke_extension)
25
25
  - [ ] 🕖️ Supports the [A2 extension](https://en.wikipedia.org/wiki/LRC_(file_format)#A2_extension_(Enhanced_LRC_format))
@@ -151,12 +151,32 @@ Here's an example:
151
151
 
152
152
  ```html
153
153
  <div id="kashi">
154
- <!-- ... -->
155
- <p>Binkusu no sake wo todoke ni yuku yo</p>
156
- <p>Umikaze kimakase namimakase</p>
157
- <p>Shio no mukou de yuuhi mo sawagu</p>
158
- <p>Sora nya wa wo kaku tori no uta</p>
159
- <!-- ... -->
154
+ <sp>
155
+ <span data-time="00:17.55" data-ms-time="17550" data-empty="false" data-aria-current="false">
156
+ Telling myself, "I won't go there"
157
+ </span>
158
+ <br/>
159
+ <span data-time="00:21.24" data-ms-time="21240" data-empty="false" data-aria-current="false">
160
+ Oh, but I know that I won't care
161
+ </span>
162
+ <br/>
163
+ <span data-time="00:24.71" data-ms-time="24710" data-empty="false" data-aria-current="false">
164
+ Tryna wash away all the blood I've spilt
165
+ </span>
166
+ <br/>
167
+ <span data-time="00:31.95" data-ms-time="31950" data-empty="false" data-aria-current="false">
168
+ This lust is a burden that we both share
169
+ </span>
170
+ <br/>
171
+ <span data-time="00:35.60" data-ms-time="35600" data-empty="false" data-aria-current="false">
172
+ Two sinners can't atone from a lone prayer
173
+ </span>
174
+ <br/>
175
+ <span data-time="00:39.28" data-ms-time="39280" data-empty="false" data-aria-current="false">
176
+ Souls tied, intertwined by our pride and guilt
177
+ </span>
178
+ <!-- ... -->
179
+ </sp>
160
180
  </div>
161
181
  ```
162
182
 
package/kashi.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface KashiProps {
10
10
  file?: KashiFile;
11
11
  container: HTMLDivElement;
12
12
  emptyLineText?: string;
13
+ audioPlayer?: HTMLAudioElement;
14
+ enableAutoScroll?: boolean;
13
15
  }
14
16
  export declare class Kashi {
15
17
  #private;
package/kashi.js CHANGED
@@ -1 +1 @@
1
- !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var r=t();for(var i in r)("object"==typeof exports?exports:e)[i]=r[i]}}(self,()=>(()=>{"use strict";var e={d:(t,r)=>{for(var i in r)e.o(r,i)&&!e.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:r[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{Kashi:()=>y});const r=/\[\d{2}:\d{2}.\d{2}\]/,i=/^\[\d{2}:\d{2}.\d{2}\](.*)$/;var n,s,o,a,f,l,h,c,u,d,p=function(e,t,r,i,n){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!n)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===i?n.call(e,r):n?n.value=r:t.set(e,r),r},w=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class y{constructor(e){if(n.add(this),s.set(this,null),o.set(this,null),a.set(this,[]),f.set(this,void 0),l.set(this,new Map),h.set(this,void 0),e.url&&e.file)throw new Error("Cannot specify both url and file.");if(!e.url&&!e.file)throw new Error("Must specify either url or file.");if(!(e.container&&e.container instanceof HTMLDivElement))throw new Error("Container must be an instance of HTMLDivElement.");e.emptyLineText&&this.setEmptyLineText(e.emptyLineText),p(this,f,e.container,"f"),this.subscribe("fileSet",w(this,n,"m",d).bind(this)),this.subscribe("emptyLineTextSet",w(this,n,"m",d).bind(this)),e.url?this.setUrl(e.url):e.file&&this.setFile(e.file)}get url(){return w(this,s,"f")}get file(){return w(this,o,"f")}get emptyLineText(){return w(this,h,"f")??"..."}async setUrl(e){try{const t=await fetch(e);if(!t.ok)throw new Error(`HTTP error! Status: ${t.status}.`);const r=t.headers.get("Content-Type");if(!r||!r.includes("text/plain"))throw new Error("Invalid content type. Expected text/plain.");const i=await t.blob();p(this,s,e,"f"),this.notify("urlSet",{url:e}),this.setFile(i)}catch(e){throw new Error((e instanceof Error&&e.message.length?e.message+" ":"")+"Failed to fetch the lyric file.")}}async setFile(e){const t=await w(this,n,"m",c).call(this,e);p(this,a,w(this,n,"m",u).call(this,t),"f"),p(this,o,e,"f"),this.notify("fileSet",{file:e})}setEmptyLineText(e){p(this,h,e,"f"),this.notify("emptyLineTextSet",{emptyLineText:e})}subscribe(e,t){w(this,l,"f").set(e,[...w(this,l,"f").get(e)??[],t])}unsubscribe(e,t){const r=w(this,l,"f").get(e);r&&r.length>1?w(this,l,"f").set(e,[...r.filter(e=>e!==t)]):w(this,l,"f").delete(e)}notify(e,t){[...w(this,l,"f").get(e)??[]].forEach(e=>{e(t)})}}return s=new WeakMap,o=new WeakMap,a=new WeakMap,f=new WeakMap,l=new WeakMap,h=new WeakMap,n=new WeakSet,c=async function(e){return new Promise((t,r)=>{const i=new FileReader;i.onload=e=>{"string"==typeof e.target?.result&&e.target?.result.trim().length?t(e.target.result):r(new Error("Failed to read file content."))},i.onerror=()=>r(new Error("Error reading file.")),i.readAsText(e)})},u=function(e){return e.split("\n").reduce((e,t)=>{const r=t.trim();return i.test(r)?[...e,r]:e},[])},d=function(){w(this,f,"f").innerHTML=w(this,a,"f").map(e=>`<p>${e.replace(r,"")||(w(this,h,"f")??"...")}</p>`).join(""),this.notify("lyricLinesUpdated",{lyricLines:w(this,a,"f")})},t})());
1
+ !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var r=t();for(var i in r)("object"==typeof exports?exports:e)[i]=r[i]}}(self,()=>(()=>{"use strict";var e={d:(t,r)=>{for(var i in r)e.o(r,i)&&!e.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:r[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{Kashi:()=>E});const r=/\[\d{2}:\d{2}.\d{2}\]/,i=/^\[\d{2}:\d{2}.\d{2}\](.*)$/;var n,s,a,o,l,f,h,c,u,d,p,y,m,w=function(e,t,r,i,n){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!n)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===i?n.call(e,r):n?n.value=r:t.set(e,r),r},b=function(e,t,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(e):i?i.value:t.get(e)};class E{constructor(e){if(n.add(this),s.set(this,null),a.set(this,null),o.set(this,[]),l.set(this,void 0),f.set(this,new Map),h.set(this,void 0),c.set(this,null),u.set(this,!1),e.url&&e.file)throw new Error("Cannot specify both url and file.");if(!e.url&&!e.file)throw new Error("Must specify either url or file.");if(!(e.container&&e.container instanceof HTMLDivElement))throw new Error("Container must be an instance of HTMLDivElement.");if(e.emptyLineText&&this.setEmptyLineText(e.emptyLineText),e.audioPlayer&&!(e.audioPlayer instanceof HTMLAudioElement))throw new Error("Audio player must be an instance of HTMLAudioElement.");e.audioPlayer&&(w(this,c,e.audioPlayer,"f"),b(this,c,"f").addEventListener("timeupdate",()=>{b(this,n,"m",m).call(this)}),b(this,c,"f").addEventListener("seeked",()=>{b(this,l,"f").querySelectorAll("p > span").forEach(e=>{e instanceof HTMLSpanElement&&(e.dataset.ariaCurrent="false")}),b(this,n,"m",m).call(this)})),e.enableAutoScroll&&w(this,u,e.enableAutoScroll,"f"),w(this,l,e.container,"f"),this.subscribe("fileSet",b(this,n,"m",y).bind(this)),this.subscribe("emptyLineTextSet",b(this,n,"m",y).bind(this)),this.subscribe("lyricLinesUpdated",b(this,n,"m",m).bind(this)),e.url?this.setUrl(e.url):e.file&&this.setFile(e.file)}get url(){return b(this,s,"f")}get file(){return b(this,a,"f")}get emptyLineText(){return b(this,h,"f")??"..."}async setUrl(e){try{const t=await fetch(e);if(!t.ok)throw new Error(`HTTP error! Status: ${t.status}.`);const r=t.headers.get("Content-Type");if(!r||!r.includes("text/plain"))throw new Error("Invalid content type. Expected text/plain.");const i=await t.blob();w(this,s,e,"f"),this.notify("urlSet",{url:e}),this.setFile(i)}catch(e){throw new Error((e instanceof Error&&e.message.length?e.message+" ":"")+"Failed to fetch the lyric file.")}}async setFile(e){const t=await b(this,n,"m",d).call(this,e);w(this,o,b(this,n,"m",p).call(this,t),"f"),w(this,a,e,"f"),this.notify("fileSet",{file:e})}setEmptyLineText(e){w(this,h,e,"f"),this.notify("emptyLineTextSet",{emptyLineText:e})}subscribe(e,t){b(this,f,"f").set(e,[...b(this,f,"f").get(e)??[],t])}unsubscribe(e,t){const r=b(this,f,"f").get(e);r&&r.length>1?b(this,f,"f").set(e,[...r.filter(e=>e!==t)]):b(this,f,"f").delete(e)}notify(e,t){[...b(this,f,"f").get(e)??[]].forEach(e=>{e(t)})}}return s=new WeakMap,a=new WeakMap,o=new WeakMap,l=new WeakMap,f=new WeakMap,h=new WeakMap,c=new WeakMap,u=new WeakMap,n=new WeakSet,d=async function(e){return new Promise((t,r)=>{const i=new FileReader;i.onload=e=>{"string"==typeof e.target?.result&&e.target?.result.trim().length?t(e.target.result):r(new Error("Failed to read file content."))},i.onerror=()=>r(new Error("Error reading file.")),i.readAsText(e)})},p=function(e){const t=e.split("\n").reduce((e,t)=>{const r=t.trim();return i.test(r)?[...e,r]:e},[]);if(0===t.length)throw new Error("No valid lyric lines found in the file.");return t},y=function(){const e=b(this,o,"f").map(e=>{const t=e.replace(r,""),i=e.match(r)?.[0]?.slice(1,-1);if(!i)return"";const n=i.split(":"),s=n[1].split(".");return`\n <span \n data-time="${i}" \n data-ms-time="${60*parseInt(n[0],10)*1e3+1e3*parseInt(s[0],10)+10*parseInt(s[1],10)}"\n data-empty="${0===t.length}"\n data-aria-current="false"\n >\n ${t||(b(this,h,"f")??"...")}\n </span>\n `}).join("<br/>");b(this,l,"f").innerHTML=`<p>${e}</p>`,this.notify("lyricLinesUpdated",{lyricLines:b(this,o,"f")})},m=function(){if(!b(this,c,"f"))return;const e=1e3*b(this,c,"f").currentTime,t=b(this,l,"f").querySelectorAll("p > span");let r=null;for(let i=0;i<t.length;i++){const n=t[i],s=parseInt(n.getAttribute("data-ms-time")||"0",10);if(n.dataset.ariaCurrent="false",!(e>=s))break;r=n}r&&(r.dataset.ariaCurrent="true",b(this,u,"f")&&r.scrollIntoView({behavior:"smooth",block:"center"}))},t})());
package/kashi.mjs CHANGED
@@ -1 +1 @@
1
- const t=/\[\d{2}:\d{2}.\d{2}\]/,e=/^\[\d{2}:\d{2}.\d{2}\](.*)$/;var i,r,n,s,o,a,h,l,f,c,u=function(t,e,i,r,n){if("m"===r)throw new TypeError("Private method is not writable");if("a"===r&&!n)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===r?n.call(t,i):n?n.value=i:e.set(t,i),i},w=function(t,e,i,r){if("a"===i&&!r)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!r:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===i?r:"a"===i?r.call(t):r?r.value:e.get(t)};class d{constructor(t){if(i.add(this),r.set(this,null),n.set(this,null),s.set(this,[]),o.set(this,void 0),a.set(this,new Map),h.set(this,void 0),t.url&&t.file)throw new Error("Cannot specify both url and file.");if(!t.url&&!t.file)throw new Error("Must specify either url or file.");if(!(t.container&&t.container instanceof HTMLDivElement))throw new Error("Container must be an instance of HTMLDivElement.");t.emptyLineText&&this.setEmptyLineText(t.emptyLineText),u(this,o,t.container,"f"),this.subscribe("fileSet",w(this,i,"m",c).bind(this)),this.subscribe("emptyLineTextSet",w(this,i,"m",c).bind(this)),t.url?this.setUrl(t.url):t.file&&this.setFile(t.file)}get url(){return w(this,r,"f")}get file(){return w(this,n,"f")}get emptyLineText(){return w(this,h,"f")??"..."}async setUrl(t){try{const e=await fetch(t);if(!e.ok)throw new Error(`HTTP error! Status: ${e.status}.`);const i=e.headers.get("Content-Type");if(!i||!i.includes("text/plain"))throw new Error("Invalid content type. Expected text/plain.");const n=await e.blob();u(this,r,t,"f"),this.notify("urlSet",{url:t}),this.setFile(n)}catch(t){throw new Error((t instanceof Error&&t.message.length?t.message+" ":"")+"Failed to fetch the lyric file.")}}async setFile(t){const e=await w(this,i,"m",l).call(this,t);u(this,s,w(this,i,"m",f).call(this,e),"f"),u(this,n,t,"f"),this.notify("fileSet",{file:t})}setEmptyLineText(t){u(this,h,t,"f"),this.notify("emptyLineTextSet",{emptyLineText:t})}subscribe(t,e){w(this,a,"f").set(t,[...w(this,a,"f").get(t)??[],e])}unsubscribe(t,e){const i=w(this,a,"f").get(t);i&&i.length>1?w(this,a,"f").set(t,[...i.filter(t=>t!==e)]):w(this,a,"f").delete(t)}notify(t,e){[...w(this,a,"f").get(t)??[]].forEach(t=>{t(e)})}}r=new WeakMap,n=new WeakMap,s=new WeakMap,o=new WeakMap,a=new WeakMap,h=new WeakMap,i=new WeakSet,l=async function(t){return new Promise((e,i)=>{const r=new FileReader;r.onload=t=>{"string"==typeof t.target?.result&&t.target?.result.trim().length?e(t.target.result):i(new Error("Failed to read file content."))},r.onerror=()=>i(new Error("Error reading file.")),r.readAsText(t)})},f=function(t){return t.split("\n").reduce((t,i)=>{const r=i.trim();return e.test(r)?[...t,r]:t},[])},c=function(){w(this,o,"f").innerHTML=w(this,s,"f").map(e=>`<p>${e.replace(t,"")||(w(this,h,"f")??"...")}</p>`).join(""),this.notify("lyricLinesUpdated",{lyricLines:w(this,s,"f")})};export{d as Kashi};
1
+ const t=/\[\d{2}:\d{2}.\d{2}\]/,e=/^\[\d{2}:\d{2}.\d{2}\](.*)$/;var i,r,n,s,a,o,l,h,f,c,u,d,p,w=function(t,e,i,r,n){if("m"===r)throw new TypeError("Private method is not writable");if("a"===r&&!n)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===r?n.call(t,i):n?n.value=i:e.set(t,i),i},m=function(t,e,i,r){if("a"===i&&!r)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!r:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===i?r:"a"===i?r.call(t):r?r.value:e.get(t)};class y{constructor(t){if(i.add(this),r.set(this,null),n.set(this,null),s.set(this,[]),a.set(this,void 0),o.set(this,new Map),l.set(this,void 0),h.set(this,null),f.set(this,!1),t.url&&t.file)throw new Error("Cannot specify both url and file.");if(!t.url&&!t.file)throw new Error("Must specify either url or file.");if(!(t.container&&t.container instanceof HTMLDivElement))throw new Error("Container must be an instance of HTMLDivElement.");if(t.emptyLineText&&this.setEmptyLineText(t.emptyLineText),t.audioPlayer&&!(t.audioPlayer instanceof HTMLAudioElement))throw new Error("Audio player must be an instance of HTMLAudioElement.");t.audioPlayer&&(w(this,h,t.audioPlayer,"f"),m(this,h,"f").addEventListener("timeupdate",()=>{m(this,i,"m",p).call(this)}),m(this,h,"f").addEventListener("seeked",()=>{m(this,a,"f").querySelectorAll("p > span").forEach(t=>{t instanceof HTMLSpanElement&&(t.dataset.ariaCurrent="false")}),m(this,i,"m",p).call(this)})),t.enableAutoScroll&&w(this,f,t.enableAutoScroll,"f"),w(this,a,t.container,"f"),this.subscribe("fileSet",m(this,i,"m",d).bind(this)),this.subscribe("emptyLineTextSet",m(this,i,"m",d).bind(this)),this.subscribe("lyricLinesUpdated",m(this,i,"m",p).bind(this)),t.url?this.setUrl(t.url):t.file&&this.setFile(t.file)}get url(){return m(this,r,"f")}get file(){return m(this,n,"f")}get emptyLineText(){return m(this,l,"f")??"..."}async setUrl(t){try{const e=await fetch(t);if(!e.ok)throw new Error(`HTTP error! Status: ${e.status}.`);const i=e.headers.get("Content-Type");if(!i||!i.includes("text/plain"))throw new Error("Invalid content type. Expected text/plain.");const n=await e.blob();w(this,r,t,"f"),this.notify("urlSet",{url:t}),this.setFile(n)}catch(t){throw new Error((t instanceof Error&&t.message.length?t.message+" ":"")+"Failed to fetch the lyric file.")}}async setFile(t){const e=await m(this,i,"m",c).call(this,t);w(this,s,m(this,i,"m",u).call(this,e),"f"),w(this,n,t,"f"),this.notify("fileSet",{file:t})}setEmptyLineText(t){w(this,l,t,"f"),this.notify("emptyLineTextSet",{emptyLineText:t})}subscribe(t,e){m(this,o,"f").set(t,[...m(this,o,"f").get(t)??[],e])}unsubscribe(t,e){const i=m(this,o,"f").get(t);i&&i.length>1?m(this,o,"f").set(t,[...i.filter(t=>t!==e)]):m(this,o,"f").delete(t)}notify(t,e){[...m(this,o,"f").get(t)??[]].forEach(t=>{t(e)})}}r=new WeakMap,n=new WeakMap,s=new WeakMap,a=new WeakMap,o=new WeakMap,l=new WeakMap,h=new WeakMap,f=new WeakMap,i=new WeakSet,c=async function(t){return new Promise((e,i)=>{const r=new FileReader;r.onload=t=>{"string"==typeof t.target?.result&&t.target?.result.trim().length?e(t.target.result):i(new Error("Failed to read file content."))},r.onerror=()=>i(new Error("Error reading file.")),r.readAsText(t)})},u=function(t){const i=t.split("\n").reduce((t,i)=>{const r=i.trim();return e.test(r)?[...t,r]:t},[]);if(0===i.length)throw new Error("No valid lyric lines found in the file.");return i},d=function(){const e=m(this,s,"f").map(e=>{const i=e.replace(t,""),r=e.match(t)?.[0]?.slice(1,-1);if(!r)return"";const n=r.split(":"),s=n[1].split(".");return`\n <span \n data-time="${r}" \n data-ms-time="${60*parseInt(n[0],10)*1e3+1e3*parseInt(s[0],10)+10*parseInt(s[1],10)}"\n data-empty="${0===i.length}"\n data-aria-current="false"\n >\n ${i||(m(this,l,"f")??"...")}\n </span>\n `}).join("<br/>");m(this,a,"f").innerHTML=`<p>${e}</p>`,this.notify("lyricLinesUpdated",{lyricLines:m(this,s,"f")})},p=function(){if(!m(this,h,"f"))return;const t=1e3*m(this,h,"f").currentTime,e=m(this,a,"f").querySelectorAll("p > span");let i=null;for(let r=0;r<e.length;r++){const n=e[r],s=parseInt(n.getAttribute("data-ms-time")||"0",10);if(n.dataset.ariaCurrent="false",!(t>=s))break;i=n}i&&(i.dataset.ariaCurrent="true",m(this,f,"f")&&i.scrollIntoView({behavior:"smooth",block:"center"}))};export{y as Kashi};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kashi",
3
- "version": "1.1.2",
3
+ "version": "2.0.0",
4
4
  "description": "Singing at the top of my lungs",
5
5
  "type": "module",
6
6
  "author": "lucasmc64",
@@ -23,7 +23,17 @@
23
23
  "music",
24
24
  "kashi",
25
25
  "karaoke",
26
- "singer"
26
+ "singer",
27
+ "sing",
28
+ "walaoke",
29
+ "a2",
30
+ "lrc-parser",
31
+ "lyric-synchronization",
32
+ "timed-lyrics",
33
+ "music-player",
34
+ "audio-player",
35
+ "lrc-file",
36
+ "lyrics-formatter"
27
37
  ],
28
38
  "scripts": {
29
39
  "prestart": "npm run build",