Skip to content

Instantly share code, notes, and snippets.

@abdalrohman
Created October 19, 2024 15:58
Show Gist options
  • Select an option

  • Save abdalrohman/11da7e35b6006744275e2dfc2e918aa8 to your computer and use it in GitHub Desktop.

Select an option

Save abdalrohman/11da7e35b6006744275e2dfc2e918aa8 to your computer and use it in GitHub Desktop.
Handles downloading of Quran audio files from EveryAyah.com.
"""
Handles downloading of Quran audio files from EveryAyah.com.
License: MIT
Author: M.Abdulrahman Alnaseer
GitHub: abdalrohman (github.com)
"""
import json
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Optional, Union, List, Iterator
from urllib.parse import urljoin
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class ReciterConfig:
"""Configuration for a Quran reciter.
:param subfolder: The subfolder name for the reciter's audio files
:param name: The reciter's name
:param bitrate: Audio bitrate of the recordings
:param reciter_id: Optional ID of the reciter
"""
subfolder: str
name: str
bitrate: str
reciter_id: Optional[int] = None
def __str__(self) -> str:
"""String representation of the reciter configuration.
:return: Formatted string with reciter details
"""
return f"{self.name} ({self.bitrate})"
def get_info(self) -> Dict[str, str]:
"""Get detailed information about the reciter.
:return: Dictionary containing reciter details
"""
return {
"name": self.name,
"bitrate": self.bitrate,
"subfolder": self.subfolder,
"id": str(self.reciter_id) if self.reciter_id else "Unknown"
}
class RecitersCollection:
"""Manages a collection of Quran reciters.
:param reciters: Dictionary mapping reciter IDs to their configurations
"""
def __init__(self, reciters: Dict[str, Dict]):
self._reciters = {
int(k): ReciterConfig(**v, reciter_id=int(k))
for k, v in reciters.items()
if k.isdigit() # Filter out non-reciter data
}
def get_reciter(self, reciter_id: int) -> Optional[ReciterConfig]:
"""Get a specific reciter by ID.
:param reciter_id: The ID of the reciter
:return: ReciterConfig for the specified reciter or None if not found
"""
return self._reciters.get(reciter_id)
def list_reciters(self) -> List[ReciterConfig]:
"""Get a list of all available reciters.
:return: List of ReciterConfig objects
"""
return sorted(self._reciters.values(), key=lambda x: x.name)
def search_reciters(self, query: str) -> Iterator[ReciterConfig]:
"""Search for reciters by name.
:param query: Search query string
:return: Iterator of matching ReciterConfig objects
"""
query = query.lower()
for reciter in self.list_reciters():
if query in reciter.name.lower():
yield reciter
def get_by_bitrate(self, bitrate: str) -> List[ReciterConfig]:
"""Get all reciters with a specific bitrate.
:param bitrate: Bitrate to filter by (e.g., '128kbps')
:return: List of matching ReciterConfig objects
"""
return [
reciter for reciter in self.list_reciters()
if reciter.bitrate.lower() == bitrate.lower()
]
class QuranAudioDownloader:
"""Handles downloading of Quran audio files from EveryAyah.com."""
BASE_URL = "https://www.everyayah.com/data/"
RECITATIONS_URL = "https://www.everyayah.com/data/recitations.js"
def __init__(self):
"""Initialize the downloader with a configured session."""
self.session = self._create_session()
self._recitation_data: Optional[Dict] = None
self._reciters_collection: Optional[RecitersCollection] = None
@staticmethod
def _create_session() -> requests.Session:
"""Create a configured requests session with retry logic.
:return: Configured requests session
"""
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
def _load_recitation_data(self) -> Dict:
"""Load recitation configuration data.
:return: Dictionary containing recitation data
:raises RequestException: If the data cannot be fetched
:raises JSONDecodeError: If the data cannot be parsed
"""
if self._recitation_data is None:
response = self.session.get(self.RECITATIONS_URL)
response.raise_for_status()
self._recitation_data = json.loads(response.text.strip())
return self._recitation_data
@property
def reciters(self) -> RecitersCollection:
"""Get the collection of available reciters.
:return: RecitersCollection object
"""
if self._reciters_collection is None:
data = self._load_recitation_data()
self._reciters_collection = RecitersCollection(data)
return self._reciters_collection
def get_reciter_config(self, reciter_number: int) -> ReciterConfig:
"""Get configuration for a specific reciter.
:param reciter_number: The reciter's ID number
:return: ReciterConfig object for the specified reciter
:raises ValueError: If reciter_number is invalid
"""
reciter = self.reciters.get_reciter(reciter_number)
if not reciter:
raise ValueError(f"No reciter found for number {reciter_number}")
return reciter
def list_available_reciters(self) -> List[Dict[str, str]]:
"""Get a list of all available reciters with their details.
:return: List of dictionaries containing reciter information
"""
return [reciter.get_info() for reciter in self.reciters.list_reciters()]
def search_reciters(self, query: str) -> List[Dict[str, str]]:
"""Search for reciters by name.
:param query: Search query string
:return: List of matching reciter information dictionaries
"""
return [reciter.get_info() for reciter in self.reciters.search_reciters(query)]
def get_reciters_by_bitrate(self, bitrate: str) -> List[Dict[str, str]]:
"""Get all reciters with a specific bitrate.
:param bitrate: Bitrate to filter by (e.g., '128kbps')
:return: List of matching reciter information dictionaries
"""
return [reciter.get_info() for reciter in self.reciters.get_by_bitrate(bitrate)]
def _validate_surah_ayah(self, surah_number: int, ayah_number: int) -> None:
"""Validate surah and ayah numbers.
:param surah_number: The surah number to validate
:param ayah_number: The ayah number to validate
:raises ValueError: If either number is invalid
"""
data = self._load_recitation_data()
ayah_counts = data.get('ayahCount', [])
if not 1 <= surah_number <= 114:
raise ValueError("Surah number must be between 1 and 114")
if not 1 <= ayah_number <= ayah_counts[surah_number - 1]:
raise ValueError(f"Invalid ayah number for surah {surah_number}")
def _construct_filename(self, surah_number: int, ayah_number: int) -> str:
"""Construct the filename for the audio file.
:param surah_number: The surah number
:param ayah_number: The ayah number
:return: Formatted filename string
"""
return f"{surah_number:03d}{ayah_number:03d}.mp3"
def download_ayah(
self,
reciter_number: int,
surah_number: int,
ayah_number: int,
save_dir: Union[str, Path]
) -> Path:
"""Download a specific ayah from a specific reciter.
:param reciter_number: The reciter's ID number
:param surah_number: The surah number
:param ayah_number: The ayah number
:param save_dir: Directory to save the downloaded file
:return: Path to the downloaded file
:raises ValueError: If any parameters are invalid
:raises RequestException: If download fails
"""
# Validate inputs
self._validate_surah_ayah(surah_number, ayah_number)
reciter_config = self.get_reciter_config(reciter_number)
# Prepare paths
save_path = Path(save_dir)
save_path.mkdir(parents=True, exist_ok=True)
filename = self._construct_filename(surah_number, ayah_number)
file_path = save_path / filename
# Construct URL and download
url = urljoin(self.BASE_URL, f"{reciter_config.subfolder}/{filename}")
logger.info(f"Downloading {filename} from {url}")
response = self.session.get(url)
response.raise_for_status()
# Save file
file_path.write_bytes(response.content)
logger.info(f"Successfully downloaded to {file_path}")
return file_path
# Usage
def main():
"""Example usage of the QuranAudioDownloader."""
try:
downloader = QuranAudioDownloader()
# List all available reciters
print("\nAll available reciters:")
for reciter in downloader.list_available_reciters():
print(f"ID: {reciter['id']}, Name: {reciter['name']}, Bitrate: {reciter['bitrate']}")
# Search for specific reciters
print("\nSearching for 'Abdullah':")
for reciter in downloader.search_reciters("Abdullah"):
print(f"Found: {reciter['name']} (ID: {reciter['id']})")
# Get reciters by bitrate
print("\nReciters with 128kbps:")
for reciter in downloader.get_reciters_by_bitrate("128kbps"):
print(f"- {reciter['name']}")
# Download example
file_path = downloader.download_ayah(
reciter_number=6, # Abdullah Basfar
surah_number=1, # Al-Fatiha
ayah_number=1, # First ayah
save_dir="downloads"
)
print(f"\nSuccessfully downloaded to {file_path}")
except Exception as e:
logger.error(f"Error: {e}")
raise
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment