Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a856106
Update README.md
ChocoMeow May 11, 2025
0329544
Create FR.json
Buco7854 May 11, 2025
27f7f64
Update application.yml to use the latest youtube source version + add…
Azarath7 May 12, 2025
dadf4bd
Merge pull request #66 from Azarath7/vocard
ChocoMeow May 12, 2025
164b4b1
Added customization for music controller buttons
ChocoMeow May 12, 2025
68de2d9
Merge pull request #65 from Buco7854/patch-1
ChocoMeow May 12, 2025
2219820
Supported local languages in placeholder
ChocoMeow May 13, 2025
47972a4
Added peek next in loop type
ChocoMeow May 13, 2025
5b721c5
Fixed an error when passing delete_after into Webhook
ChocoMeow May 13, 2025
60451b2
Supported placeholder in music controller button
ChocoMeow May 13, 2025
6dac72c
Merge pull request #67 from ChocoMeow/Custom-Music-Buttons
ChocoMeow May 13, 2025
f581980
Fixed a bug
ChocoMeow May 13, 2025
4009832
Update controller.py
ChocoMeow May 13, 2025
8b81c05
Disable yt ratelimit if there are no tokens provided
ChocoMeow May 14, 2025
d47201f
Removed unnecessary network from vocard-dashboard service
Azarath7 May 27, 2025
5b84959
Update methods.py
ChocoMeow May 28, 2025
026b262
return empty list if track not found
ChocoMeow Jun 11, 2025
15d0ac3
Dump lavalink and plugin version
ChocoMeow Jun 15, 2025
eac2b02
Update docker-compose.yml
Azarath7 Jun 15, 2025
c3c1f2e
Update docker-image.yml
ChocoMeow Jun 16, 2025
c23aa5c
Update docker-image.yml
ChocoMeow Jun 16, 2025
7c5caa8
Updated the search platform from spotify to youtube
ChocoMeow Jul 3, 2025
c7d19b9
Change variable name
ChocoMeow Jul 3, 2025
955d716
Added musixmatch lyrics
ChocoMeow Jul 7, 2025
e1fdbfe
Merge pull request #71 from ChocoMeow/Added-Musixmatch-lyrics
ChocoMeow Jul 9, 2025
c8fa4e5
Added silent messaging feature
ChocoMeow Jul 23, 2025
d9fb58e
Use snake_case for all database keys
ChocoMeow Aug 4, 2025
e97a604
Added silent mode to the controller
ChocoMeow Aug 4, 2025
0b15f1d
Merge pull request #73 from ChocoMeow/Added-silent-messaging-feature
ChocoMeow Aug 4, 2025
8fda1d2
Add Spanish language support for music bot commands
MiguVT Aug 5, 2025
0ff3e18
Merge branch 'beta' into main
MiguVT Aug 5, 2025
39d5a42
Add silent messaging support to language files for Spanish and fixed …
MiguVT Aug 5, 2025
07b2576
Fix send function context handling and queueType key
ChocoMeow Aug 6, 2025
f9a6185
Merge branch 'beta' into pr/75
ChocoMeow Aug 6, 2025
f763844
Merge branch 'beta' into pr/69
ChocoMeow Aug 7, 2025
6587409
Add Spotify Tokener service and update plugin configs
ChocoMeow Aug 7, 2025
69bea8e
Merge pull request #69 from Azarath7/vocard-docker
ChocoMeow Aug 7, 2025
76ccc7d
Add dotenv support for environment-based settings
ChocoMeow Aug 7, 2025
8edc26a
Standardize permission and message keys in code and locales
ChocoMeow Aug 10, 2025
f6c2511
Add Vietnamese language support
ChocoMeow Aug 10, 2025
e7aa431
Handle Playlist type in track search results
ChocoMeow Aug 11, 2025
42a40d1
Merge branch 'beta' into main
MiguVT Aug 12, 2025
8cd6b33
Updates translations for Spanish
MiguVT Aug 18, 2025
fa0d410
Improves command synchronization handling
MiguVT Aug 18, 2025
cce7ee4
Add new keys to Spanish language files
ChocoMeow Aug 20, 2025
236284b
Merge pull request #75 from MiguVT/main
ChocoMeow Aug 20, 2025
4b01a3f
Update docker-image.yml
ChocoMeow Aug 23, 2025
15ed5b2
Update Lavalink to latest version and plugin
ChocoMeow Aug 23, 2025
24cd6ca
Fix bot mention prefix response logic
ChocoMeow Aug 23, 2025
0e8b3ce
Improve message handling in send function
ChocoMeow Aug 24, 2025
5c973a7
Improve message handling and permission checks
ChocoMeow Aug 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
58 changes: 43 additions & 15 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,56 @@ name: Docker Image CI

on:
push:
branches: [ "main" ]
tags:
- '*' # Trigger on any tag
branches:
- main # Trigger on push to main branch
- beta # Trigger on push to beta branch
paths-ignore:
- '**.md'
- '**.md'

permissions:
packages: write

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Build the Docker image
run: docker build . --file Dockerfile --tag vocard:latest
- uses: actions/checkout@v4

- name: Log in to GitHub Docker Registry
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

- name: Push the Docker image
run: |
docker tag vocard:latest ghcr.io/chocomeow/vocard:latest # Ensure lowercase
docker push ghcr.io/chocomeow/vocard:latest # Ensure lowercase
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Docker Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/chocomeow/vocard
tags: |
# For version tags (v1.2.3)
type=ref,event=tag
# For main branch
type=raw,value=latest,enable={{is_default_branch}}
# For beta branch
type=raw,value=beta,enable=${{ github.ref == 'refs/heads/beta' }}
flavor: |
latest=false

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Vocard is a highly customizable Discord music bot, designed to deliver a user-fr
* [Lavalink Server (Requires 4.0.0+)](https://github.com/freyacodes/Lavalink)

## Setup
Please see the [Setup Page](https://docs.vocard.xyz) in the docs to run this bot yourself!
Please see the [Setup Page](https://docs.vocard.xyz/latest/bot/setup) in the docs to run this bot yourself!

## Need Help?
Join the [Vocard Support Discord](https://discord.gg/wRCgB7vBQv) for help or questions.
Expand Down
147 changes: 139 additions & 8 deletions addons/lyrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,17 @@
SOFTWARE.
"""

import aiohttp, random, bs4, re
import aiohttp
import random
import re
import hmac
import hashlib
import base64
import json
import urllib.parse
import function as func

from datetime import datetime
from abc import ABC, abstractmethod
from urllib.parse import quote
from math import floor
Expand Down Expand Up @@ -71,9 +79,6 @@
Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.6 (KHTML, like Gecko) Chrome/ Safari/530.6
Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5'''

LYRIST_ENDPOINT = "https://lyrist.vercel.app/api/"
LRCLIB_ENDPOINT = "https://lrclib.net/api/"

class LyricsPlatform(ABC):
@abstractmethod
async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
Expand Down Expand Up @@ -207,9 +212,12 @@ async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
return {"default": song.lyrics}

class Lyrist(LyricsPlatform):
def __init__(self):
self.base_url: str = "https://lyrist.vercel.app/api/"

async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
try:
request_url = LYRIST_ENDPOINT + title + "/" + artist
request_url = self.base_url + title + "/" + artist
async with aiohttp.ClientSession() as session:
resp = await session.get(url=request_url, headers={'User-Agent': random.choice(userAgents)})
if resp.status != 200:
Expand All @@ -221,6 +229,9 @@ async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
return None

class Lrclib(LyricsPlatform):
def __init__(self):
self.base_url: str = "https://lrclib.net/api/"

async def get(self, url, params: dict = None) -> list[dict]:
try:
async with aiohttp.ClientSession() as session:
Expand All @@ -231,15 +242,135 @@ async def get(self, url, params: dict = None) -> list[dict]:
except:
return []

async def get_lyrics(self, title, artist):
async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
params = {"q": title}
result = await self.get(LRCLIB_ENDPOINT + "search", params)
result = await self.get(self.base_url + "search", params)
if result:
return {"default": result[0].get("plainLyrics", "")}

"""
Strvm/musicxmatch-api: a reverse engineered API wrapper for MusicXMatch
Copyright (c) 2025 Strvm

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
class MusixMatch(LyricsPlatform):
def __init__(self):
self.base_url = "https://www.musixmatch.com/ws/1.1/"
self.headers = {'User-Agent': random.choice(userAgents)}
self.secret: Optional[str] = None

async def search_tracks(self, track_query: str, page: int = 1) -> dict:
url = f"track.search?app_id=web-desktop-app-v1.0&format=json&q={urllib.parse.quote(track_query)}&f_has_lyrics=true&page_size=5&page={page}"
return await self.make_request(url)

async def get_track_lyrics(self, track_id: Optional[str] = None, track_isrc: Optional[str] = None) -> dict:
if not (track_id or track_isrc):
raise ValueError("Either track_id or track_isrc must be provided.")
param = f"track_id={track_id}" if track_id else f"track_isrc={track_isrc}"
url = f"track.lyrics.get?app_id=web-desktop-app-v1.0&format=json&{param}"
return await self.make_request(url)

async def get_latest_app(self):
url = "https://www.musixmatch.com/search"

async with aiohttp.ClientSession() as session:
async with session.get(url, headers={**self.headers, "Cookie": "mxm_bab=AB"}) as response:
html_content = await response.text()
pattern = r'src="([^"]*/_next/static/chunks/pages/_app-[^"]+\.js)"'
matches = re.findall(pattern, html_content)

if not matches:
raise Exception("_app URL not found in the HTML content.")

return matches[-1]

async def get_secret(self) -> str:
async with aiohttp.ClientSession() as session:
async with session.get(await self.get_latest_app(), headers=self.headers, timeout=5) as response:
javascript_code = await response.text()

pattern = r'from\(\s*"(.*?)"\s*\.split'
match = re.search(pattern, javascript_code)

if match:
encoded_string = match.group(1)
reversed_string = encoded_string[::-1]

decoded_bytes = base64.b64decode(reversed_string)
return decoded_bytes.decode("utf-8")
else:
raise Exception("Encoded string not found in the JavaScript code.")

async def generate_signature(self, url: str) -> str:
current_date = datetime.now()
date_str = f"{current_date.year}{str(current_date.month).zfill(2)}{str(current_date.day).zfill(2)}"
message = (url + date_str).encode()

if not self.secret:
self.secret = await self.get_secret()

key = self.secret.encode()
hash_output = hmac.new(key, message, hashlib.sha256).digest()
signature = (
"&signature="
+ urllib.parse.quote(base64.b64encode(hash_output).decode())
+ "&signature_protocol=sha256"
)
return signature

async def make_request(self, url: str) -> dict:
url = url.replace("%20", "+").replace(" ", "+")
url = self.base_url + url
signed_url = url + await self.generate_signature(url)

async with aiohttp.ClientSession() as session:
async with session.get(signed_url, headers=self.headers, timeout=5) as response:
if response.status != 200:
raise Exception(f"HTTP Error: {response.status} for URL: {signed_url}")

try:
text_data = await response.text()
return json.loads(text_data)
except json.JSONDecodeError:
raise Exception(f"Failed to parse JSON. Response: {text_data}")

async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
results = await self.search_tracks(track_query=f"{artist} {title}" if artist else title)

track_list = results.get("message", {}).get("body", {}).get("track_list")
if not track_list:
return None

track_id = track_list[0]["track"]["track_id"]
lyric = await self.get_track_lyrics(track_id=track_id)

lyrics_body = lyric.get("message", {}).get("body", {}).get("lyrics", {}).get("lyrics_body", "")
if not lyrics_body:
return None

return {"default": lyrics_body}

LYRICS_PLATFORMS: dict[str, Type[LyricsPlatform]] = {
"a_zlyrics": A_ZLyrics,
"genius": Genius,
"lyrist": Lyrist,
"lrclib": Lrclib
"lrclib": Lrclib,
"musixmatch": MusixMatch
}
15 changes: 10 additions & 5 deletions addons/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,25 @@
SOFTWARE.
"""

import os

from dotenv import load_dotenv
from typing import (
Dict,
List,
Any,
Union
)

load_dotenv()

class Settings:
def __init__(self, settings: Dict) -> None:
self.token: str = settings.get("token")
self.client_id: int = int(settings.get("client_id", 0))
self.genius_token: str = settings.get("genius_token")
self.mongodb_url: str = settings.get("mongodb_url")
self.mongodb_name: str = settings.get("mongodb_name")
self.token: str = settings.get("token") or os.getenv("TOKEN")
self.client_id: int = int(settings.get("client_id", 0)) or int(os.getenv("CLIENT_ID"))
self.genius_token: str = settings.get("genius_token") or os.getenv("GENIUS_TOKEN")
self.mongodb_url: str = settings.get("mongodb_url") or os.getenv("MONGODB_URL")
self.mongodb_name: str = settings.get("mongodb_name") or os.getenv("MONGODB_NAME")

self.invite_link: str = "https://discord.gg/wRCgB7vBQv"
self.nodes: Dict[str, Dict[str, Union[str, int, bool]]] = settings.get("nodes", {})
Expand Down
Loading