@persian-caesar/discord-player
Version:
Type definitions and helpers for a Discord music player (MusicPlayer) without external dependencies
707 lines (604 loc) • 26.9 kB
Markdown
# @persian-caesar/discord-player
A lightweight, type-safe music player for Discord self-bots, built with TypeScript and integrated with `discord.js`. This package provides a robust `MusicPlayer` class for streaming audio from platforms like YouTube, SoundCloud, Spotify, and Deezer, with features like queue management, looping, shuffling, and lyrics retrieval.
## Table of Contents
- [@persian-caesar/discord-player](#persian-caesardiscord-player)
- [Table of Contents](#table-of-contents)
- [Introduction](#introduction)
- [Features](#features)
- [Installation](#installation)
- [Dependencies](#dependencies)
- [Usage Examples](#usage-examples)
- [TypeScript Example](#typescript-example)
- [JavaScript Example](#javascript-example)
- [API Reference](#api-reference)
- [MusicPlayer Class](#musicplayer-class)
- [MusicPlayerEvent Enum](#musicplayerevent-enum)
- [Method Usage and Examples](#method-usage-and-examples)
- [`play(input: string): Promise<void>`](#playinput-string-promisevoid)
- [`pause(): void`](#pause-void)
- [`resume(): void`](#resume-void)
- [`setVolume(percent: number): void`](#setvolumepercent-number-void)
- [`skip(): void`](#skip-void)
- [`previous(): Promise<void>`](#previous-promisevoid)
- [`shuffle(): void`](#shuffle-void)
- [`undoShuffle(): void`](#undoshuffle-void)
- [`toggleLoopQueue(): void`](#toggleloopqueue-void)
- [`toggleLoopTrack(): void`](#togglelooptrack-void)
- [`startRadio(urls: string[]): Promise<void>`](#startradiourls-string-promisevoid)
- [`stop(noLeave?: boolean): void`](#stopnoleave-boolean-void)
- [`disconnect(): void`](#disconnect-void)
- [`getQueue(): TrackMetadata[]`](#getqueue-trackmetadata)
- [`getVolume(): number`](#getvolume-number)
- [`isPlaying(): boolean`](#isplaying-boolean)
- [`isPaused(): boolean`](#ispaused-boolean)
- [`isShuffiled(): boolean`](#isshuffiled-boolean)
- [`searchLyrics(title: string, artist?: string): Promise<string | null>`](#searchlyricstitle-string-artist-string-promisestring--null)
- [Support and Contributions](#support-and-contributions)
- [License](#license)
- [Contact](#contact)
## Introduction
`@persian-caesar/discord-player` is designed to simplify audio playback in Discord bots. It leverages the `@discordjs/voice` library for voice channel interactions and supports streaming from multiple platforms using libraries like `ytdl-core`, `play-dl`, and `soundcloud-downloader`. The package is fully typed, making it ideal for TypeScript projects, and includes JSDoc annotations for JavaScript users. The `MusicPlayer` class handles all aspects of music playback, including queue management, history tracking, and event-driven notifications.
Developed by [Sobhan-SRZA](https://github.com/Sobhan-SRZA) for [Persian Caesar](https://github.com/Persian-Caesar), this package is licensed under MIT and actively maintained.
## Features
- **Multi-Platform Streaming**: Supports YouTube, SoundCloud, Spotify, and Deezer via `ytdl-core`, `play-dl`, and `soundcloud-downloader`.
- **Queue Management**: Add tracks to a queue, shuffle, or revert to the original order.
- **Looping Options**: Toggle looping for a single track or the entire queue.
- **Volume Control**: Adjust playback volume (0–200%).
- **Lyrics Retrieval**: Fetch song lyrics from Google search results using `html-to-text`.
- **Radio Mode**: Play a shuffled list of URLs in a continuous loop.
- **Event System**: Strongly-typed events for playback status, queue changes, errors, and more.
- **Auto-Disconnect**: Configurable options to leave voice channels when the queue is empty or after idle time.
- **Type Safety**: Full TypeScript support with defined interfaces and enums in `types.ts`.
- **Lightweight**: Minimal dependencies with no external framework requirements beyond `discord.js`.
## Installation
Install the package:
```bash
npm install @persian-caesar/discord-player
```
Ensure you have Node.js version 16 or higher, as specified in `package.json`.
## Dependencies
The following dependencies are required for the package to function correctly:
| Package | Version | Purpose |
| ----------------------- | -------- | ------------------------------------------------------------------------- |
| `@discordjs/voice` | ^0.18.0 | Handles voice channel connections and audio playback in Discord. |
| `@discordjs/opus` | ^0.10.0 | Provides Opus audio encoding/decoding for high-quality audio streaming. |
| `ytdl-core` | ^4.11.5 | Streams audio from YouTube videos, with fallback support for reliability. |
| `ytdl-core-discord` | ^1.3.1 | Alternative YouTube streaming library for compatibility. |
| `@distube/ytdl-core` | ^4.16.10 | Enhanced YouTube streaming with additional features and reliability. |
| `play-dl` | ^1.9.7 | Streams audio from Spotify, YouTube, and Deezer with search capabilities. |
| `soundcloud-downloader` | ^1.0.0 | Downloads and streams audio from SoundCloud URLs. |
| `html-to-text` | ^9.0.5 | Converts HTML (from Google lyrics searches) to plain text. |
| `libsodium-wrappers` | ^0.7.15 | Required for secure audio encryption in `@discordjs/voice`. |
| `ffmpeg-static` | (peer) | Provides FFmpeg for audio processing and stream conversion. |
**Why these dependencies?**
- `@discordjs/voice` and `@discordjs/opus` are core to Discord voice functionality, enabling the bot to join channels and stream audio.
- Multiple YouTube streaming libraries (`ytdl-core`, `ytdl-core-discord`, `@distube/ytdl-core`) ensure robust streaming with fallbacks for reliability.
- `play-dl` adds support for Spotify and Deezer, broadening the range of supported platforms.
- `soundcloud-downloader` enables streaming from SoundCloud, a popular music platform.
- `html-to-text` is used for scraping and cleaning lyrics from Google search results.
- `libsodium-wrappers` and `ffmpeg-static` are required for secure and efficient audio processing.
## Usage Examples
Below are two examples demonstrating how to integrate `@persian-caesar/discord-player` with `discord.js` in both TypeScript and JavaScript. These examples assume you have a Discord bot set up with `discord.js`.
### TypeScript Example
This example shows how to create a Discord bot that uses `MusicPlayer` to play music in a voice channel and handle commands.
```typescript
import { Client, GatewayIntentBits, TextChannel, VoiceChannel } from 'discord.js';
import { MusicPlayer, MusicPlayerEvent } from '@persian-caesar/discord-player';
// Initialize Discord client with necessary intents
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
// Bot configuration
const PREFIX = '!';
const TOKEN = 'YOUR_BOT_TOKEN'; // Replace with your bot token
client.on('ready', () => {
console.log(`Logged in as ${client.user?.tag}`);
});
client.on('messageCreate', async (message) => {
if (!message.content.startsWith(PREFIX) || message.author.bot) return;
const args = message.content.slice(PREFIX.length).trim().split(/ +/);
const command = args.shift()?.toLowerCase();
if (!message.guild || !message.member?.voice.channel) return;
const voiceChannel = message.member.voice.channel as VoiceChannel;
const player = new MusicPlayer(voiceChannel, 50, {
autoLeaveOnEmptyQueue: true,
autoLeaveOnIdleMs: 300_000, // 5 minutes
});
// Event listeners for music player
player.on(MusicPlayerEvent.Start, ({ metadata }) => {
message.channel.send(`▶️ Now playing: ${metadata.title || metadata.url}`);
});
player.on(MusicPlayerEvent.QueueAdd, ({ metadata, queue }) => {
message.channel.send(`➕ Added to queue: ${metadata.title || metadata.url} (${queue.length} in queue)`);
});
player.on(MusicPlayerEvent.Error, (error) => {
message.channel.send(`❌ Error: ${error.message}`);
});
player.on(MusicPlayerEvent.Finish, () => {
message.channel.send('⏹️ Playback finished.');
});
// Command handling
if (command === 'play') {
const query = args.join(' ');
if (!query) {
message.channel.send('Please provide a URL or search query.');
return;
}
await player.play(query);
} else if (command === 'pause') {
player.pause();
message.channel.send('⏸️ Paused playback.');
} else if (command === 'resume') {
player.resume();
message.channel.send('▶️ Resumed playback.');
} else if (command === 'skip') {
player.skip();
message.channel.send('⏭️ Skipped to next track.');
} else if (command === 'stop') {
player.stop();
message.channel.send('⏹️ Stopped playback.');
} else if (command === 'lyrics') {
const title = args.join(' ');
if (!title) {
message.channel.send('Please provide a song title.');
return;
}
const lyrics = await player.searchLyrics(title);
message.channel.send(lyrics ? `🎵 Lyrics:\n${lyrics}` : '❌ No lyrics found.');
}
});
client.login(TOKEN);
```
**Steps to run:**
1. Save the above code as `bot.ts`.
2. Replace `YOUR_BOT_TOKEN` with your Discord bot token.
3. Ensure all dependencies are installed.
4. Compile with `tsc bot.ts` and run with `node bot.js`.
### JavaScript Example
This example is similar but uses plain JavaScript with JSDoc annotations for type hints.
```javascript
const { Client, GatewayIntentBits } = require('discord.js');
const { MusicPlayer, MusicPlayerEvent } = require('@persian-caesar/discord-player');
// Initialize Discord client with necessary intents
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
// Bot configuration
const PREFIX = '!';
const TOKEN = 'YOUR_BOT_TOKEN'; // Replace with your bot token
client.on('messageCreate', async (message) => {
if (!message.content.startsWith(PREFIX) || message.author.bot) return;
const args = message.content.slice(PREFIX.length).trim().split(/ +/);
const command = args.shift()?.toLowerCase();
if (!message.guild || !message.member?.voice.channel) return;
/** @type {import('@persian-caesar/discord-player').VoiceChannel} */
const voiceChannel = message.member.voice.channel;
const player = new MusicPlayer(voiceChannel, 50, {
autoLeaveOnEmptyQueue: true,
autoLeaveOnIdleMs: 300_000, // 5 minutes
});
// Event listeners for music player
player.on(MusicPlayerEvent.Start, ({ metadata }) => {
message.channel.send(`▶️ Now playing: ${metadata.title || metadata.url}`);
});
player.on(MusicPlayerEvent.QueueAdd, ({ metadata, queue }) => {
message.channel.send(`➕ Added to queue: ${metadata.title || metadata.url} (${queue.length} in queue)`);
});
player.on(MusicPlayerEvent.Error, (error) => {
message.channel.send(`❌ Error: ${error.message}`);
});
player.on(MusicPlayerEvent.Finish, () => {
message.channel.send('⏹️ Playback finished.');
});
// Command handling
if (command === 'play') {
const query = args.join(' ');
if (!query) {
message.channel.send('Please provide a URL or search query.');
return;
}
await player.play(query);
} else if (command === 'pause') {
player.pause();
message.channel.send('⏸️ Paused playback.');
} else if (command === 'resume') {
player.resume();
message.channel.send('▶️ Resumed playback.');
} else if (command === 'skip') {
player.skip();
message.channel.send('⏭️ Skipped to next track.');
} else if (command === 'stop') {
player.stop();
message.channel.send('⏹️ Stopped playback.');
} else if (command === 'lyrics') {
const title = args.join(' ');
if (!title) {
message.channel.send('Please provide a song title.');
return;
}
const lyrics = await player.searchLyrics(title);
message.channel.send(lyrics ? `🎵 Lyrics:\n${lyrics}` : '❌ No lyrics found.');
}
});
client.login(TOKEN);
```
**Steps to run:**
1. Save the above code as `bot.js`.
2. Replace `YOUR_BOT_TOKEN` with your Discord bot token.
3. Ensure all dependencies are installed.
4. Run with `node bot.js`.
## API Reference
### MusicPlayer Class
**Constructor**:
```typescript
new MusicPlayer(
channel: VoiceChannel,
initialVolume?: number, // Default: 100
options?: MusicPlayerOptions // { autoLeaveOnEmptyQueue?: boolean, autoLeaveOnIdleMs?: number }
)
```
**Methods**:
| Method | Description |
| ---------------------------------------------- | ---------------------------------------------------------- |
| `play(input: string)` | Plays a track by URL or search query, enqueues if playing. |
| `pause()` | Pauses the current track. |
| `resume()` | Resumes playback. |
| `setVolume(percent: number)` | Sets volume (0–200%). |
| `skip()` | Skips to the next track in the queue. |
| `previous()` | Plays the previous track from history. |
| `shuffle()` | Shuffles the queue, saving the original order. |
| `undoShuffle()` | Restores the queue to its pre-shuffle order. |
| `toggleLoopQueue()` | Toggles queue looping. |
| `toggleLoopTrack()` | Toggles single-track looping. |
| `startRadio(urls: string[])` | Starts radio mode with shuffled URLs. |
| `stop(noLeave?: boolean)` | Stops playback, optionally disconnects. |
| `disconnect()` | Disconnects from the voice channel. |
| `getQueue(): TrackMetadata[]` | Returns a copy of the current queue. |
| `getVolume(): number` | Returns the current volume (0–200%). |
| `isPlaying(): boolean` | Checks if a track is playing. |
| `isPaused(): boolean` | Checks if playback is paused. |
| `isShuffiled(): boolean` | Checks if the queue is shuffled. |
| `searchLyrics(title: string, artist?: string)` | Fetches song lyrics from Google. |
### MusicPlayerEvent Enum
```typescript
export enum MusicPlayerEvent {
Start = "start",
QueueAdd = "queueAdd",
Pause = "pause",
Resume = "resume",
Stop = "stop",
Skip = "skip",
Previous = "previous",
Shuffle = "shuffle",
LoopQueue = "loopQueue",
LoopTrack = "loopTrack",
VolumeChange = "volumeChange",
Finish = "finish",
Disconnect = "disconnect",
Error = "error"
}
```
**Event Payloads**:
- `Start`: `{ metadata: TrackMetadata, queue: TrackMetadata[] }`
- `QueueAdd`: `{ metadata: TrackMetadata, queue: TrackMetadata[] }`
- `VolumeChange`: `{ volume: number }`
- `Skip`: `{ queue: TrackMetadata[], history: string[] }`
- `Previous`: `{ metadata: TrackMetadata, queue: TrackMetadata[], history: string[] }`
- `Shuffle`: `{ queue: TrackMetadata[] }`
- `Finish`: `{ queue: TrackMetadata[], history: string[] }`
- `Error`: `Error`
- Others: No payload
See `types.ts` for full type definitions.
### Method Usage and Examples
This section provides detailed explanations and code snippets for each `MusicPlayer` method, demonstrating their usage within a Discord bot context using `discord.js`. The examples assume a `MusicPlayer` instance is created as shown in the [Usage Examples](#usage-examples) section.
#### `play(input: string): Promise<void>`
Plays a track by URL or search query. If a track is already playing, it adds the new track to the queue.
**Example**:
```typescript
// Command: !play <query>
if (command === 'play') {
const query = args.join(' ');
if (!query) {
message.channel.send('Please provide a URL or search query.');
return;
}
await player.play(query);
// The Start or QueueAdd event will handle the response
}
player.on(MusicPlayerEvent.Start, ({ metadata }) => {
message.channel.send(`▶️ Now playing: ${metadata.title || metadata.url}`);
});
player.on(MusicPlayerEvent.QueueAdd, ({ metadata, queue }) => {
message.channel.send(`➕ Added to queue: ${metadata.title || metadata.url} (${queue.length} in queue)`);
});
```
#### `pause(): void`
Pauses the currently playing track.
**Example**:
```typescript
if (command === 'pause') {
player.pause();
message.channel.send('⏸️ Playback paused.');
}
player.on(MusicPlayerEvent.Pause, () => {
message.channel.send('⏸️ Playback paused.');
});
```
#### `resume(): void`
Resumes playback of a paused track.
**Example**:
```typescript
if (command === 'resume') {
player.resume();
message.channel.send('▶️ Playback resumed.');
}
player.on(MusicPlayerEvent.Resume, () => {
message.channel.send('▶️ Playback resumed.');
});
```
#### `setVolume(percent: number): void`
Sets the playback volume (0–200%). Values outside this range are capped at 200%.
**Example**:
```typescript
if (command === 'volume') {
const volume = parseInt(args[0]);
if (isNaN(volume)) {
message.channel.send('Please provide a valid volume (0–200).');
return;
}
player.setVolume(volume);
// The VolumeChange event will handle the response
}
player.on(MusicPlayerEvent.VolumeChange, ({ volume }) => {
message.channel.send(`🔊 Volume set to ${volume}%`);
});
```
#### `skip(): void`
Skips the current track and plays the next track in the queue.
**Example**:
```typescript
if (command === 'skip') {
player.skip();
message.channel.send('⏭️ Skipped to next track.');
}
player.on(MusicPlayerEvent.Skip, ({ queue }) => {
message.channel.send(`⏭️ Skipped. ${queue.length} tracks remaining in queue.`);
});
```
#### `previous(): Promise<void>`
Plays the previous track from the history, if available.
**Example**:
```typescript
if (command === 'previous') {
await player.previous();
// The Previous event will handle the response
}
player.on(MusicPlayerEvent.Previous, ({ metadata }) => {
message.channel.send(`⏮️ Playing previous track: ${metadata.title || metadata.url}`);
});
player.on(MusicPlayerEvent.Error, (error) => {
if (error.message.includes('No track to previous')) {
message.channel.send('❌ No previous track available.');
}
});
```
#### `shuffle(): void`
Shuffles the queue and saves the original order for potential restoration.
**Example**:
```typescript
if (command === 'shuffle') {
player.shuffle();
message.channel.send('🔀 Queue shuffled.');
}
player.on(MusicPlayerEvent.Shuffle, ({ queue }) => {
message.channel.send(`🔀 Queue shuffled. ${queue.length} tracks in new order.`);
});
```
#### `undoShuffle(): void`
Restores the queue to its pre-shuffle order, excluding played tracks.
**Example**:
```typescript
if (command === 'unshuffle') {
player.undoShuffle();
message.channel.send('🔄 Queue restored to original order.');
}
player.on(MusicPlayerEvent.Shuffle, ({ queue }) => {
message.channel.send(`🔄 Queue restored. ${queue.length} tracks in queue.`);
});
```
#### `toggleLoopQueue(): void`
Toggles queue looping on or off.
**Example**:
```typescript
if (command === 'loopqueue') {
player.toggleLoopQueue();
message.channel.send(`🔁 Queue looping ${player.isLoopQueue() ? 'enabled' : 'disabled'}.`);
}
```
#### `toggleLoopTrack(): void`
Toggles single-track looping on or off.
**Example**:
```typescript
if (command === 'looptrack') {
player.toggleLoopTrack();
message.channel.send(`🔂 Track looping ${player.isLoopTrack() ? 'enabled' : 'disabled'}.`);
}
```
#### `startRadio(urls: string[]): Promise<void>`
Starts radio mode by shuffling a list of URLs and playing them continuously.
**Example**:
```typescript
if (command === 'radio') {
const urls = args; // Array of URLs
if (!urls.length) {
message.channel.send('Please provide at least one URL.');
return;
}
await player.startRadio(urls);
// The Start event will handle the response
}
player.on(MusicPlayerEvent.Start, ({ metadata }) => {
message.channel.send(`📻 Radio mode started: ${metadata.title || metadata.url}`);
});
```
#### `stop(noLeave?: boolean): void`
Stops playback, clears the queue and history, and optionally disconnects from the voice channel.
**Example**:
```typescript
if (command === 'stop') {
player.stop(true); // Stay in voice channel
message.channel.send('⏹️ Playback stopped.');
}
player.on(MusicPlayerEvent.Stop, () => {
message.channel.send('⏹️ Playback stopped.');
});
```
#### `disconnect(): void`
Disconnects the bot from the voice channel and clears all resources.
**Example**:
```typescript
if (command === 'disconnect') {
player.disconnect();
message.channel.send('🔌 Disconnected from voice channel.');
}
player.on(MusicPlayerEvent.Disconnect, () => {
message.channel.send('🔌 Disconnected from voice channel.');
});
```
#### `getQueue(): TrackMetadata[]`
Returns a copy of the current queue as an array of `TrackMetadata`.
**Example**:
```typescript
if (command === 'queue') {
const queue = player.getQueue();
if (!queue.length) {
message.channel.send('📃 Queue is empty.');
return;
}
const queueList = queue.map((track, index) => `${index + 1}. ${track.title || track.url}`).join('\n');
message.channel.send(`📃 Queue:\n${queueList}`);
}
```
#### `getVolume(): number`
Returns the current volume as a percentage (0–200%).
**Example**:
```typescript
if (command === 'volume') {
const currentVolume = player.getVolume();
message.channel.send(`🔊 Current volume: ${currentVolume}%`);
}
```
#### `isPlaying(): boolean`
Checks if a track is currently playing.
**Example**:
```typescript
if (command === 'status') {
const status = player.isPlaying() ? 'playing' : 'not playing';
message.channel.send(`🎵 Player is ${status}.`);
}
```
#### `isPaused(): boolean`
Checks if the player is currently paused.
**Example**:
```typescript
if (command === 'status') {
const paused = player.isPaused() ? 'paused' : 'not paused';
message.channel.send(`⏯️ Playback is ${paused}.`);
}
```
#### `isShuffiled(): boolean`
Checks if the queue is shuffled.
**Example**:
```typescript
if (command === 'status') {
const shuffled = player.isShuffiled() ? 'shuffled' : 'not shuffled';
message.channel.send(`🔀 Queue is ${shuffled}.`);
}
```
#### `searchLyrics(title: string, artist?: string): Promise<string | null>`
Fetches song lyrics from Google based on the provided title and optional artist.
**Example**:
```typescript
if (command === 'lyrics') {
const title = args.join(' ');
if (!title) {
message.channel.send('Please provide a song title.');
return;
}
const lyrics = await player.searchLyrics(title);
message.channel.send(lyrics ? `🎵 Lyrics:\n${lyrics}` : '❌ No lyrics found.');
}
```
## Support and Contributions
- **Repository**: [https://github.com/Persian-Caesar/discord-player](https://github.com/Persian-Caesar/discord-player)
- **Issues**: [https://github.com/Persian-Caesar/discord-player/issues](https://github.com/Persian-Caesar/discord-player/issues)
- **Community**: Join the [Persian Caesar Discord](https://dsc.gg/persian-caesar) for support.
- **Contributions**: Pull requests are welcome! Please follow the contribution guidelines in the repository.
## License
This project is licensed under the MIT License. See the [`LICENSE`](./LICENSE) file or the repository for details.
---
⌨️ Built with ❤️ by **[Sobhan-SRZA](https://github.com/Sobhan-SRZA)** for **[Persian Caesar](https://github.com/Persian-Caesar)**. Star the repo if you find it useful!
## Contact
<div align="center">
<a href="https://srza.ir" target="_blank">
<img align="left" src="https://raw.githubusercontent.com/Sobhan-SRZA/Sobhan-SRZA/refs/heads/main/images/social.png" alt="Sobhan-SRZA social" width=400px>
</a>
<a href="https://t.me/d_opa_mine" target="_blank">
<img alt="Telegram"
src="https://raw.githubusercontent.com/Sobhan-SRZA/Sobhan-SRZA/refs/heads/main/images/telegram-ch.svg"
height="30" />
</a>
<a href="https://t.me/Sobhan_SRZA" target="_blank">
<img alt="Telegram"
src="https://raw.githubusercontent.com/Sobhan-SRZA/Sobhan-SRZA/refs/heads/main/images/telegram-ac.svg"
height="30" />
</a>
<a href="https://www.instagram.com/mr.sinre?igsh=cWk1aHdhaGRnOGg%3D&utm_source=qr" target="_blank">
<img alt="Instagram"
src="https://raw.githubusercontent.com/Sobhan-SRZA/Sobhan-SRZA/refs/heads/main/images/instagram.svg"
height="30" />
</a>
<a href="https://www.twitch.tv/sobhan_srza" target="_blank">
<img alt="Twitch"
src="https://raw.githubusercontent.com/Sobhan-SRZA/Sobhan-SRZA/refs/heads/main/images/twitch.svg"
height="30" />
</a>
<a href="https://www.youtube.com/@mr_sinre?app=desktop&sub_confirmation=1" target="_blank">
<img alt="YouTube"
src="https://raw.githubusercontent.com/Sobhan-SRZA/Sobhan-SRZA/refs/heads/main/images/youtube.svg"
height="30" />
</a>
<a href="https://github.com/Sobhan-SRZA" target="_blank">
<img alt="Github"
src="https://raw.githubusercontent.com/Sobhan-SRZA/Sobhan-SRZA/refs/heads/main/images/github.svg"
height="30" />
</a>
<p align="left">
<a href="https://discord.gg/xh2S2h67UW" target="_blank">
<img src="https://discord.com/api/guilds/1054814674979409940/widget.png?style=banner2" alt="pc-development.png">
</a>
</p>
<p align="right">
<a href="https://discord.gg/54zDNTAymF" target="_blank">
<img src="https://discord.com/api/guilds/1181764925874507836/widget.png?style=banner2" alt="pc-club.png">
</a>
</p>
<div align="center">
<a href="https://discord.com/users/865630940361785345" target="_blank">
<img alt="My Discord Account" src="https://discord.c99.nl/widget/theme-1/865630940361785345.png" />
</a>
<a href="https://discord.com/users/986314682547716117" target="_blank" align="right">
<img alt="Team Discord Account" src="https://discord.c99.nl/widget/theme-1/986314682547716117.png" />
</a>
</div>
</div>