kashi
Version:
Singing at the top of my lungs
286 lines (214 loc) • 11.1 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
- [ ] 🎩 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({
file. // Loaded from an 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({
file. // Loaded from an 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,
file: 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 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) {
if (obj1[key] !== obj2[key]) {
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 |
| --------------- | ------------------ | ------------- | ------------ | ----------------------------------------- |
| `file` | `Blob` (or `File`) | - | Yes | Lyrics file |
| `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 file will be wrapped by a `<p></p>` tag and inserted into the `container`.
Here's an example:
```html
<div id="kashi">
<p>
<span data-time="00:17.55" data-ms-time="17550" data-empty="false" data-aria-current="false">
Telling myself, "I won't go there"
</span>
<br/>
<span data-time="00:21.24" data-ms-time="21240" data-empty="false" data-aria-current="false">
Oh, but I know that I won't care
</span>
<br/>
<span data-time="00:24.71" data-ms-time="24710" data-empty="false" data-aria-current="false">
Tryna wash away all the blood I've spilt
</span>
<br/>
<span data-time="00:31.95" data-ms-time="31950" data-empty="false" data-aria-current="false">
This lust is a burden that we both share
</span>
<br/>
<span data-time="00:35.60" data-ms-time="35600" data-empty="false" data-aria-current="false">
Two sinners can't atone from a lone prayer
</span>
<br/>
<span data-time="00:39.28" data-ms-time="39280" data-empty="false" data-aria-current="false">
Souls tied, intertwined by our pride and guilt
</span>
<!-- ... -->
</p>
</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 |
| ------------------ | --------- | ------------------------------------------------------------ |
| `file` | Attribute | Returns the file 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 |
| `setFile` | Method | Function capable of changing the current lyrics file by passing the the new **file** |
| `setEmptyLineText` | Method | Function capable of changing the text defined for empty lines |
| `noLyricsText` | 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 |
| ------------------- | --------------------------- | ------------------------------------------ |
| `fileSet` | `{ file: Blob }` | When calling the `setFile` method |
| `emptyLineTextSet` | `{ emptyLineText: string }` | When calling the `setEmptyLineText` method |
| `noLyricsText` | `{ 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/)