Resource & Customization Guide
Overview
This guide covers data sourcing, resource configuration, UI component customization, and performance tuning for QuranBot's core systems.
- Reciter & audio source management
- Radio station configuration & health checks
- Azkar scheduling & content updates
- UI, embed, and component customization
Reciter & Audio Management
Reciter data is loaded at startup from remote CDN endpoints and cached in memory for fast access. The system supports dynamic URL generation, duration estimation, and Lavalink-based stream validation.
Loading Pipeline (src/data/data-loader-reciters.js)
-
Fetch cached dataset from
https://hub-mgv.github.io/QuranBotData/data_quran.json -
Parse
reciters.recitersarray and validate required fields (id,name,server) -
Generate MP3 URLs for all 114 surahs per reciter using
formatSurahUrl() - Estimate durations and assign to
global.recitersMap
// src/data/data-loader-formatter.js - URL generation
function formatSurahUrl(serverUrl, surahNumber, reciterType) {
const padded = surahNumber.toString().padStart(3, '0');
// CDN-specific path patterns
if (serverUrl.includes('everyayah.com')) {
return `${serverUrl}Alafasy/mp3/${surahNumber}.mp3`;
}
return `${serverUrl}${padded}.mp3`;
}
Adding a New Reciter
Update the remote JSON source or local cache. The structure must match:
| Field | Type | Description |
|---|---|---|
id |
number |
Unique numeric identifier |
name |
string |
Display name (supports Arabic) |
server |
string |
Base CDN URL (must end with /) |
rewaya_id |
string |
Recitation style (Hafs, Warsh, etc.) |
photo |
string |
Thumbnail URL (optional) |
Audio Data Structure
At runtime, each reciter is stored in global.reciters with this structure:
// Runtime object per reciter
{
id: 1,
name: 'Abdul Basit Abdul Samad',
rewaya: 'hafs',
photo: 'https://cdn.example.com/photo.jpg',
links: ['https://server/001.mp3', 'https://server/002.mp3', ...], // 114 entries
durations: ['12:34', '08:15', ...] // Estimated MM:SS
}
Stream Validation & Fallback
Before playback, createSurahResource() in src/audio/resource.js validates URLs:
- Searches via Lavalink node using
source: 'http' - If track not found, tries alternate surah for same reciter
- If all fail, switches to next reciter with valid HTTP links
- Max fallback attempts:
5per request
Durations are calculated algorithmically at load time using formatDuration() in
data-loader-formatter.js. Actual stream lengths may vary slightly. The bot relies on
Lavalink's real-time position tracking for accurate seek/resume operations.
Radio Station Configuration
Radio stations are loaded from the same CDN dataset and stored in global.quranRadios. Each
entry contains name, URL, and metadata for display.
Structure (src/data/data-loader-radios.js)
// Radio entry format
{
name: 'Quran Radio Makkah',
url: 'https://stream.example.com/quran-makkah'
}
Radio pages display 25 items per page (configurable via
pagination.radio_items_per_page). Navigation updates
guildState.currentRadioPage and recalculates currentRadioIndex.
Playback Logic
- Radio streams are treated as continuous tracks
- No surah index or offset tracking applies
-
Switching radio stations calls
createRadioResource()which validates the URL via Lavalink - Auto-fallback to next station or surah mode if stream fails
Health Check & Fallback
The bot implements automatic stream validation before playback to prevent silent failures or dead links.
Validation Flow (src/audio/resource.js)
// Radio stream validation
async function createRadioResource(url) {
if (!url?.startsWith('http')) throw new Error('Invalid Radio URL');
const client = require('@startup/botSetup').client;
const searchNode = getBestNode(client.lavalink);
const result = await searchNode.search({ query: url, source: 'http' }, null);
if (!result || !result.tracks || result.tracks.length === 0) {
throw new Error('Radio stream invalid');
}
return result.tracks[0];
}
Fallback Chain
If validation fails during playback:
- Try next radio station in list (max 3 attempts)
- Switch to surah mode with fallback reciter
- Log warning and update
guildState.isPausedif all fail
Azkar & Reminder System
The azkar system delivers scheduled reminders every 30 minutes with support for text, images, and audio playback. Data is fetched from a remote JSON endpoint with graceful fallback.
Configuration (src/state/azkar-config.js)
| Constant | Value | Description |
|---|---|---|
azkar_interval_ms |
1800000 |
30-minute delivery interval |
azkar_expiry_ms |
864000000 |
10-day expiry for azkar buttons |
azkar_max_retry_attempts |
5 |
Max send retries per delivery |
azkar_retry_delay_ms |
2000 |
Exponential backoff base delay |
Remote Data Structure
Expected from https://hub-mgv.github.io/QuranBotData/adhkar.json:
[
{
"id": 1,
"category": "أدكار الصباح",
"audio": "/audio/morning.mp3",
"array": [
{
"id": 1,
"text": "أصبحنا وأصبح الملك لله...",
"count": 1,
"audio": "/audio/1.mp3"
}
]
}
]
Delivery & Expiry Handling
Azkar messages are sent via src/state/azkar-sender.js with retry logic and automatic cleanup.
Delivery Methods
- Image Mode - Embeds azkar image with text overlay
- Audio Mode - Sends text embed with "استماع" button linking to MP3
- Category Audio - Plays full section audio when individual tracks unavailable
Button Expiry & Cleanup
Audio buttons include a timestamp to prevent playback of stale messages:
// Azkar expiry check (azkarAudio.js)
const buttonId = interaction.customId;
const timestamp = parseInt(buttonId.split('_').pop());
if (timestamp && Date.now() - timestamp > azkar_request_expiry) {
await safeError(interaction, 'هذا الذكر قديم جداً (أكثر من 10 أيام)');
return;
}
Expired messages are automatically pruned via deleteMessageTimestamp() after 10 days. Failed
deliveries increment azkarFail counter; after 10 failures, the timer is disabled and
azkarChannelId is cleared.
Prayer Times Data
Prayer times are fetched from the Aladhan API with region-specific calculation methods and timezone awareness.
API Integration (src/interactions/buttons/prayer-times-navigation.js)
// Calculation method selection
let calculationMethod = 2; // ISNA (default)
if (countryCode === 'EG') calculationMethod = 5; // Egyptian Authority
else if (countryCode === 'SA') calculationMethod = 4; // Umm Al-Qura
else if (countryCode === 'KW' || countryCode === 'QA') calculationMethod = 3; // Kuwait
const apiUrl = `https://api.aladhan.com/v1/timings/${unixTimestamp}?latitude=${lat}&longitude=${lng}&method=${calculationMethod}`;
Time Formatting
Countries in prayer_times_config.time_format_12h_countries display times in 12-hour AM/PM
format. Others use 24-hour format. The formatter handles edge cases like midnight conversion.
UI & Embed Customization
The control panel UI is generated dynamically via src/ui/embeds.js and
src/ui/components.js. All styling follows Discord's native embed limits.
Embed Caching Strategy
Control embeds are cached to reduce API rate limits and improve response time:
// Cache key generation (embeds.js)
const key = `control_embed_${guildId}_${st.currentSurah}_${st.currentReciter}_${st.currentPage}_${st.playbackMode}_${st.controlMode}`;
if (cache.has(key)) {
const c = cache.get(key);
c.active = true;
c.timestamp = Date.now();
return c.embed;
}
// Auto-cleanup: 30s TTL for active, 60s for inactive
const ttl = active ? 30000 : 60000;
Customizing Colors & Footer
| Element | File | Customization |
|---|---|---|
| Embed Color | embeds.js |
Change 0x1e1f22 to any hex value |
| Footer Text | embeds.js |
Pulled from package.json metadata |
| Emojis | helpers/emojis.js |
Replace custom emoji IDs per environment |
| Button Labels | components.js |
Modify setLabel() calls |
Component & Pagination
UI components follow Discord's 5-row limit. The bot dynamically builds rows based on playback mode.
Row Allocation
- Row 1: Reciter/Radio dropdown
- Row 2: Surah dropdown
- Row 3: Playback controls (prev/pause/resume/next/toggle)
- Row 4: Navigation (page prev/next + Lavalink status)
- Row 5: Entry controls (join/leave/mode toggle/support)
Pagination Limits
| Type | Items per Page | Source |
|---|---|---|
| Reciters | 25 |
pagination.default_items |
| Surahs | 25 |
pagination.default_items |
| Radio Stations | 25 |
pagination.radio_items_per_page |
| Prayer Countries | 25 |
prayer_times_config.cities_per_page |
Performance & Caching
QuranBot uses a multi-tier caching strategy to minimize latency and API overhead.
Cache Tiers
// src/config/constants.js - Cache configuration
const cache_config = {
interaction: { max_size: 500, ttl_ms: 3000 },
embed: { ttl_ms: 30000, max_entries: 100 },
data: { ttl_ms: 300000, file_path: 'storage/database/data_url.json' },
dhikr: { ttl_ms: 300000, save_debounce_ms: 10000 },
guild_state: { save_debounce_ms: 60000 }
};
Memory Management
- Heap threshold:
1500MBtriggers aggressive cleanup - Cache pruning removes oldest 50% of entries when threshold exceeded
-
Manual
global.gc()called if available (Node.js--expose-gc) - Destroyed voice connections cleaned hourly
Debounce & Throttle
State persistence uses debounced saves to prevent Firebase rate limits:
| Operation | Delay | Purpose |
|---|---|---|
| Single guild save | 60000ms |
Batch updates before persisting |
| Save throttle | 10000ms |
Prevent rapid sequential saves |
| All states save | 15000ms |
Rate limit bulk persistence |
Resource Troubleshooting
Reciter links showing as unavailable
Check:
- Server URL in dataset ends with
/ - MP3 files follow
001.mp3to114.mp3naming - CDN allows cross-origin requests
- Lavalink node can reach external URLs
Use /سرعة to verify node connectivity and test stream resolution.
Azkar timer stops after restart
The timer state is not persisted by design. It's re-initialized during readyHandler.js via
recoverAzkarTimers(). Ensure:
azkarChannelIdexists insetupGuilds- Channel is text-based and bot has
Send Messagespermission - Remote adhkar.json is accessible
Manual restart: global.startAzkarTimerForGuild(guildId, channelId)
Embed cache not updating after changes
Clear the cache manually or wait for TTL expiry:
clearEmbedCache()inembeds.js- Restart bot (clears memory cache)
- Check if embed key matches current state values
Active cache entries refresh every 30s, inactive every 60s.
Resource Validation Commands
| Check | Method | Expected Output |
|---|---|---|
| Reciter count | console.log(Object.keys(global.reciters).length) |
50+ |
| Radio count | console.log(global.quranRadios.length) |
30+ |
| Azkar categories | console.log(global.azkarData.length) |
10+ |
| Cache status | console.log(embedCache.size) |
0-100 |