kashi
Version:
Singing at the top of my lungs
298 lines (221 loc) โข 11.2 kB
Markdown
<div align="center">
<a href="https://gitlab.com/lucasmc64/kashi">GitLab</a> โข
<a href="https://www.npmjs.com/package/kashi">NPM</a> โข
<a href="https://unpkg.com/browse/kashi@latest">CDN</a> โข
<a href="https://ko-fi.com/lucasmc64">Ko-fi</a>
</div>

# ๐ค Kashi
## ๐ฏ Goal
This project is a dependency-free library that aims to provide a way to correctly read, format, structure, and display song lyrics.
## ๐ Features
- [x] ๐ต Process and list song lyrics in .lrc files
- [x] ๐ช Easily integrates with React and other frameworks
- [x] โ๏ธ Implements the Observer pattern and emits events at each step of the process whenever something changes
- [x] โ๏ธ Allows you to enter custom text when the lyrics line is empty
- [x] ๐ค Synchronizes the lyrics with the music that is playing
- [x] ๐ฉ Supports multiple lyrics for the same song (useful to keep track of the original lyrics and their translation)
- [ ] ๐ง Supports the [Walaoke extension](<https://en.wikipedia.org/wiki/LRC_(file_format)#Walaoke_extension>)
- [ ] ๐๏ธ Supports the [A2 extension](<https://en.wikipedia.org/wiki/LRC_(file_format)#A2_extension_(Enhanced_LRC_format)>)
> **Note:** Support for fetching lyrics using a URL has been removed because it only supported public URLs.
>
> Therefore, instead of refactoring to handle requests to private APIs, I opted to support only files. If a request is necessary, you can make it externally and then pass the returned file to Kashi!
## ๐จ How to use it in my project?
### <span id="classic-scripts">๐ Classic scripts</span>
The project is also exported using [UMD](https://github.com/umdjs/umd), which means that all variables and classes such as the `Kashi` class is exposed globally, and can be used in any script imported after the library.
> **Note**: You should **change `X.Y.Z` to the library version number** according to your needs, this is just an example. It is recommended to always use the latest version.
>
> It is recommended that you pin to the latest stable version of the lib. However, if you always want to use the latest version, you can also use `latest` instead of a predefined version number. **Be careful** though, breaking changes may be introduced and your project may need to adapt to the changes.
```html
<!DOCTYPE html>
<html>
<head>
<!-- ... -->
<script src="https://unpkg.com/kashi@X.Y.Z/kashi.js" defer></script>
<!-- ... -->
</head>
<body>
<div id="kashi"></div>
</body>
</html>
```
```js
"use strict";
new Kashi({
files. // Loaded from some input[type="file"] or anywhere else
container: document.getElementById("kashi"),
});
```
### ๐ฆ๏ธ ES6 Modules
The HTML is very similar to the classic scripts version, just change the `.js` extension to `.mjs` (and maybe will be necessary to adds a `type="module"` too), which will result in something similar to this:
> **Note**: Please read the [previous section](#classic-scripts) to get the details involved in importing and choosing a package version, they are the same here.
```html
<script
src="https://unpkg.com/kashi@X.Y.Z/kashi.mjs"
type="module"
defer
></script>
```
```javascript
import { Kashi } from "kashi";
// Usage is essentially the same as in the previous section
new Kashi({
files. // Loaded from some input[type="file"] or anywhere else
container: document.getElementById("kashi"),
});
```
### โ๏ธ React
Install the lib using your favorite package manager.
```bash
npm install kashi
```
Since the library was designed primarily to be used with vanilla JS, a _helper_ component needs to be created to encapsulate Kashi's behavior and make it simple to reuse throughout the application.
> **Note**: There is TypeScript support, and even if your project doesn't use the JS superset, it should help VSCode and other editors provide autocomplete/code suggestions.
```tsx
import { memo, useEffect, useRef } from "react";
import { Kashi, KashiProps } from "kashi";
import { api } from "@/services/axios";
// Example using Vite, React and TypeScript
export const KashiWrapper = memo(
(props: Omit<KashiProps, "container"> & { url: string }) => {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
async function loadKashi() {
if (!ref.current) {
return;
}
if (!props.url) {
ref.current.innerHTML = "";
return;
}
try {
const { url, ...rest } = props;
const response = await api.get(url, { responseType: "blob" });
new Kashi({
...rest,
files: [response.data],
container: ref.current,
});
} catch (error) {
console.error("Error loading Kashi:", error);
}
}
loadKashi();
return () => {
// Required to avoid duplication when React is in Strict Mode
if (ref.current) {
ref.current.innerHTML = "";
}
};
}, [ref.current, props]);
return <div className="kashi-wrapper" ref={ref} />;
},
(prevProps, nextProps) => {
function compareArrays(arr1: unknown[], arr2: unknown[]) {
if (arr1.length !== arr2.length) {
return false;
}
return arr1.every((item, index) => item === arr2[index]);
}
function compareObjects(
obj1: Omit<KashiProps, "container">,
obj2: Omit<KashiProps, "container">,
) {
const keys1 = Object.keys(obj1) as Array<
keyof Omit<KashiProps, "container">
>;
const keys2 = Object.keys(obj2) as Array<
keyof Omit<KashiProps, "container">
>;
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
const val1 = obj1[key];
const val2 = obj2[key];
if (Array.isArray(val1) && Array.isArray(val2)) {
if (!compareArrays(val1, val2)) {
return false;
}
} else if (val1 !== val2) {
return false;
}
}
return true;
}
return compareObjects(prevProps, nextProps);
},
);
```
## ๐ง Constructor properties
You must pass some properties to Kashi to define what lyrics display and where. Here are its specifications:
| Property | Type | Default value | Is required? | Description |
| --------------- | ---------------- | ------------- | ------------ | ----------------------------------------- |
| `files` | `Blob[]` | - | Yes | Lyrics files |
| `container` | `HTMLDivElement` | - | Yes | Element where the lyrics will be inserted |
| `emptyLineText` | `string` | `...` | No | Custom text for empty lines of the lyrics |
| `noLyricsText` | `string` | - | No | Custom text for when there are no lyrics |
## ๐พ Generated HTML structure
The `div#kashi` represents the `container` passed to `Kashi` where the song lyrics will be inserted.
Each line of lyrics present in the lrc files will be wrapped by a `<div></div>` tag and inserted into the `container`.
Here's an example:
```html
<div id="kashi">
<div
data-time="00:17.55"
data-ms-time="17550"
data-empty="false"
data-aria-current="false"
>
<span>Telling myself, "I won't go there"</span>
<br />
<span> Dizendo a mim mesmo, "eu nรฃo vou lรก" </span>
</div>
<div
data-time="00:21.24"
data-ms-time="21240"
data-empty="false"
data-aria-current="false"
>
<span>Oh, but I know that I won't care</span>
<br />
<span>Oh, mas eu sei que nรฃo vou me importar</span>
</div>
<!-- ... -->
</div>
```
## ๐ Methods and attributes
The instance generated by `Kashi` has some public methods and attributes that can be used to query or change properties on the fly.
| Name | Type | Description |
| ------------------ | --------- | -------------------------------------------------------------------------------------- |
| `files` | Attribute | Returns the files from the current lyrics |
| `emptyLineText` | Attribute | Returns the text set for empty lines |
| `noLyricsText` | Attribute | Returns the text set for when there are no lyrics |
| `setFiles` | Method | Function capable of changing the current lyrics files by passing the the new **files** |
| `setEmptyLineText` | Method | Function capable of changing the text defined for empty lines |
| `setNoLyricsText` | Method | Function capable of changing the text defined for when there are no lyrics |
| `subscribe` | Method | Function capable of defining a callback to be executed when a given event is triggered |
| `unsubscribe` | Method | Function capable of making a callback to stop listening to an event |
| `notify` | Method | Function capable of triggering an event |
## ๐พ Events
When creating a new instance using `Kashi` you will have access to the `subscribe`, `unsubscribe` and `notify` methods, these methods can be used respectively to listen for an event, stop listening for an event and manually trigger an event. Below is the list of events triggered internally:
| Event | Data | Trigger |
| ------------------- | --------------------------- | ------------------------------------------ |
| `filesSet` | `{ files: Blob[] }` | When calling the `setFiles` method |
| `emptyLineTextSet` | `{ emptyLineText: string }` | When calling the `setEmptyLineText` method |
| `noLyricsTextSet` | `{ noLyricsText: text }` | When calling the `setNoLyricsText` method |
| `lyricLinesUpdated` | `{ lyricLines: string[] }` | When inserting/updating lyrics in HTML |
## ๐ค How do I run the project on my machine?
The first step is to clone the project, either via terminal or even by downloading the compressed file (.zip). After that, go ahead.
### ๐ ๏ธ Requirements
- [NodeJS and NPM](https://nodejs.org)
### โจ Running the project
With the dependencies properly installed, still in the terminal, run `npm start`.
Create a simple demo project using vanilla HTML/JS and use the files in the `dist` folder for testing.
You can also create a demo project using React and use [npm link](https://docs.npmjs.com/cli/v9/commands/npm-link).
### ๐ If everything went well...
Now you are running the project beautifully!
## โ๏ธ License
This project is under the GPL v3 license. See the [LICENSE](https://gitlab.com/lucasmc64/kashi/-/blob/main/LICENSE) for more information.
---
Made with ๐ by lucasmc64 ๐ [Get in touch!](https://www.linkedin.com/in/lucasmc64/)