Resource & Customization Guide

Last Updated: 2026-05-26 • For Data Managers & UI Customizers

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)

  1. Fetch cached dataset from https://hub-mgv.github.io/QuranBotData/data_quran.json
  2. Parse reciters.reciters array and validate required fields (id, name, server)
  3. Generate MP3 URLs for all 114 surahs per reciter using formatSurahUrl()
  4. Estimate durations and assign to global.reciters Map
// 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: 5 per request
Duration Estimation

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:

  1. Try next radio station in list (max 3 attempts)
  2. Switch to surah mode with fallback reciter
  3. Log warning and update guildState.isPaused if 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: 1500MB triggers 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.mp3 to 114.mp3 naming
  • 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:

  • azkarChannelId exists in setupGuilds
  • Channel is text-based and bot has Send Messages permission
  • 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() in embeds.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