Created
October 19, 2024 15:58
-
-
Save abdalrohman/11da7e35b6006744275e2dfc2e918aa8 to your computer and use it in GitHub Desktop.
Handles downloading of Quran audio files from EveryAyah.com.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| 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